changeset 990b40c7b2e0 in modules/account_payment_stripe:default
details:
https://hg.tryton.org/modules/account_payment_stripe?cmd=changeset&node=990b40c7b2e0
description:
Add identical party from payments
issue11385
review380731002
diffstat:
__init__.py | 2 +
message.xml | 3 +
party.py | 7 +
payment.py | 104 ++++++++++++++++++-
payment.xml | 5 +
tests/scenario_account_payment_stripe_identical.rst | 109 ++++++++++++++++++++
view/customer_form.xml | 3 +
7 files changed, 232 insertions(+), 1 deletions(-)
diffs (355 lines):
diff -r 5443f5e69731 -r 990b40c7b2e0 __init__.py
--- a/__init__.py Mon May 02 17:23:53 2022 +0200
+++ b/__init__.py Sun Jul 17 19:07:38 2022 +0200
@@ -13,6 +13,8 @@
payment.Account,
payment.Refund,
payment.Customer,
+ payment.CustomerFingerprint,
+ payment.CustomerIdentical,
payment.Journal,
payment.Group,
payment.Payment,
diff -r 5443f5e69731 -r 990b40c7b2e0 message.xml
--- a/message.xml Mon May 02 17:23:53 2022 +0200
+++ b/message.xml Sun Jul 17 19:07:38 2022 +0200
@@ -9,5 +9,8 @@
<record model="ir.message" id="msg_stripe_receivable">
<field name="text">To pay "%(payment)s", you cannot use a stripe
journal "%(journal)s".</field>
</record>
+ <record model="ir.message" id="msg_customer_fingerprint_unique">
+ <field name="text">The fingerprint must be unique by
customer.</field>
+ </record>
</data>
</tryton>
diff -r 5443f5e69731 -r 990b40c7b2e0 party.py
--- a/party.py Mon May 02 17:23:53 2022 +0200
+++ b/party.py Sun Jul 17 19:07:38 2022 +0200
@@ -13,6 +13,13 @@
stripe_customers = fields.One2Many(
'account.payment.stripe.customer', 'party', "Stripe Customers")
+ def _payment_identical_parties(self):
+ parties = super()._payment_identical_parties()
+ for customer in self.stripe_customers:
+ for other_customer in customer.identical_customers:
+ parties.add(other_customer.party)
+ return parties
+
@classmethod
def write(cls, *args):
pool = Pool()
diff -r 5443f5e69731 -r 990b40c7b2e0 payment.py
--- a/payment.py Mon May 02 17:23:53 2022 +0200
+++ b/payment.py Sun Jul 17 19:07:38 2022 +0200
@@ -10,12 +10,14 @@
from operator import attrgetter
import stripe
+from sql import Literal
from trytond.cache import Cache
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
- DeactivableMixin, ModelSQL, ModelView, Workflow, dualmethod, fields)
+ DeactivableMixin, ModelSQL, ModelView, Unique, Workflow, dualmethod,
+ fields)
from trytond.modules.account_payment.exceptions import (
PaymentValidationError, ProcessError)
from trytond.modules.company.model import (
@@ -26,6 +28,7 @@
from trytond.report import Report, get_email
from trytond.rpc import RPC
from trytond.sendmail import sendmail_transactional
+from trytond.tools import sql_pairing
from trytond.tools.email_ import set_from_header
from trytond.transaction import Transaction
from trytond.url import http_host
@@ -436,6 +439,8 @@
The transaction is committed after each payment charge.
"""
+ pool = Pool()
+ Customer = pool.get('account.payment.stripe.customer')
if payments is None:
payments = cls.search([
('state', '=', 'processing'),
@@ -508,6 +513,10 @@
continue
Transaction().commit()
+ customers = [p.stripe_customer for p in payments if p.stripe_customer]
+ if customers:
+ Customer.__queue__.find_identical(customers)
+
def _charge_parameters(self):
source, customer = None, None
if self.stripe_token:
@@ -1332,6 +1341,16 @@
'invisible': ~Eval('stripe_error_param'),
})
+ identical_customers = fields.Many2Many(
+ 'account.payment.stripe.customer.identical',
+ 'source', 'target', "Identical Customers", readonly=True,
+ states={
+ 'invisible': ~Eval('identical_customers'),
+ })
+ fingerprints = fields.One2Many(
+ 'account.payment.stripe.customer.fingerprint', 'customer',
+ "Fingerprints", readonly=True)
+
_sources_cache = Cache(
'account_payment_stripe_customer.sources',
duration=config.getint(
@@ -1355,6 +1374,10 @@
'invisible': ~Eval('stripe_customer_id'),
'depends': ['stripe_customer_id'],
},
+ 'find_identical': {
+ 'invisible': ~Eval('stripe_customer_id'),
+ 'depends': ['stripe_customer_id'],
+ },
})
def get_stripe_checkout_needed(self, name):
@@ -1446,6 +1469,7 @@
# TODO add card
customer.save()
Transaction().commit()
+ cls.__queue__.find_identical(customers)
def _customer_parameters(self):
locales = [pl.lang.code for pl in self.party.langs if pl.lang]
@@ -1695,6 +1719,84 @@
cls._payment_methods_cache.clear()
Transaction().commit()
+ def fetch_fingeprints(self):
+ customer = self.retrieve()
+ if customer:
+ for source in customer.sources:
+ if hasattr(source, 'fingerprint'):
+ yield source.fingerprint
+ try:
+ payment_methods = stripe.PaymentMethod.list(
+ api_key=self.stripe_account.secret_key,
+ customer=customer.id,
+ type='card')
+ except (stripe.error.RateLimitError,
+ stripe.error.APIConnectionError) as e:
+ logger.warning(str(e))
+ payment_methods = []
+ for payment_method in payment_methods:
+ yield payment_method.card.fingerprint
+
+ @classmethod
+ @ModelView.button
+ def find_identical(cls, customers):
+ pool = Pool()
+ Fingerprint = pool.get('account.payment.stripe.customer.fingerprint')
+ new = []
+ for customer in customers:
+ fingerprints = set(customer.fetch_fingeprints())
+ fingerprints -= {f.fingerprint for f in customer.fingerprints}
+ for fingerprint in fingerprints:
+ new.append(Fingerprint(
+ customer=customer,
+ fingerprint=fingerprint))
+ Fingerprint.save(new)
+
+
+class CustomerFingerprint(ModelSQL):
+ "Stripe Customer Fingerprint"
+ __name__ = 'account.payment.stripe.customer.fingerprint'
+ customer = fields.Many2One(
+ 'account.payment.stripe.customer', "Customer",
+ required=True, ondelete='CASCADE')
+ fingerprint = fields.Char("Fingerprint", required=True)
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ t = cls.__table__()
+ cls._sql_constraints += [
+ ('customer_fingerprint_unique',
+ Unique(t, t.customer, t.fingerprint),
+ 'account_payment_stripe.msg_customer_fingerprint_unique'),
+ ]
+
+
+class CustomerIdentical(ModelSQL):
+ "Stripe Customer Identical"
+ __name__ = 'account.payment.stripe.customer.identical'
+ source = fields.Many2One('account.payment.stripe.customer', "Source")
+ target = fields.Many2One('account.payment.stripe.customer', "Target")
+
+ @classmethod
+ def table_query(cls):
+ pool = Pool()
+ Fingerprint = pool.get('account.payment.stripe.customer.fingerprint')
+ source = Fingerprint.__table__()
+ target = Fingerprint.__table__()
+ return (
+ source
+ .join(target, condition=source.fingerprint == target.fingerprint)
+ .select(
+ Literal(0).as_('create_uid'),
+ source.create_date.as_('create_date'),
+ Literal(None).as_('write_uid'),
+ Literal(None).as_('write_date'),
+ sql_pairing(source.id, target.id).as_('id'),
+ source.customer.as_('source'),
+ target.customer.as_('target'),
+ where=source.customer != target.customer))
+
class Checkout(Wizard):
"Stripe Checkout"
diff -r 5443f5e69731 -r 990b40c7b2e0 payment.xml
--- a/payment.xml Mon May 02 17:23:53 2022 +0200
+++ b/payment.xml Sun Jul 17 19:07:38 2022 +0200
@@ -285,6 +285,11 @@
<field name="string">Detach Source</field>
<field name="model" search="[('model', '=',
'account.payment.stripe.customer')]"/>
</record>
+ <record model="ir.model.button" id="customer_find_identical_button">
+ <field name="name">find_identical</field>
+ <field name="string">Find Identical</field>
+ <field name="model" search="[('model', '=',
'account.payment.stripe.customer')]"/>
+ </record>
<record model="ir.action.wizard" id="wizard_checkout">
<field name="name">Stripe Checkout</field>
diff -r 5443f5e69731 -r 990b40c7b2e0
tests/scenario_account_payment_stripe_identical.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_account_payment_stripe_identical.rst Sun Jul 17
19:07:38 2022 +0200
@@ -0,0 +1,109 @@
+================================
+Account Payment Stripe Identical
+================================
+
+Imports::
+
+ >>> import datetime as dt
+ >>> import os
+ >>> import time
+ >>> from decimal import Decimal
+
+ >>> import stripe
+
+ >>> from proteus import Model, Wizard
+ >>> from trytond.tests.tools import activate_modules
+ >>> from trytond.modules.company.tests.tools import (
+ ... create_company, get_company)
+
+Activate modules::
+
+ >>> config = activate_modules('account_payment_stripe')
+
+ >>> Company = Model.get('company.company')
+ >>> Cron = Model.get('ir.cron')
+ >>> StripeAccount = Model.get('account.payment.stripe.account')
+ >>> StripeCustomer = Model.get('account.payment.stripe.customer')
+
+Create company::
+
+ >>> _ = create_company()
+ >>> company = get_company()
+
+Create Stripe account::
+
+ >>> stripe_account = StripeAccount(name="Stripe")
+ >>> stripe_account.secret_key = os.getenv('STRIPE_SECRET_KEY')
+ >>> stripe_account.publishable_key = os.getenv('STRIPE_PUBLISHABLE_KEY')
+ >>> stripe_account.save()
+ >>> stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
+
+Setup cron::
+
+ >>> cron_customer_create, = Cron.find([
+ ... ('method', '=', 'account.payment.stripe.customer|stripe_create'),
+ ... ])
+ >>> cron_customer_create.companies.append(Company(company.id))
+ >>> cron_customer_create.save()
+
+Create parties::
+
+ >>> Party = Model.get('party.party')
+ >>> customer1 = Party(name="Customer 1")
+ >>> customer1.save()
+
+ >>> customer2 = Party(name="Customer 2")
+ >>> customer2.save()
+
+Create a customer::
+
+ >>> stripe_customer1 = StripeCustomer()
+ >>> stripe_customer1.party = customer1
+ >>> stripe_customer1.stripe_account = stripe_account
+ >>> _ = stripe_customer1.click('stripe_checkout')
+ >>> token = stripe.Token.create(
+ ... card={
+ ... 'number': '4012888888881881',
+ ... 'exp_month': 12,
+ ... 'exp_year': dt.date.today().year + 1,
+ ... 'cvc': '123',
+ ... },
+ ... )
+ >>> StripeCustomer.write(
+ ... [stripe_customer1.id], {'stripe_token': token.id}, config.context)
+
+Run cron::
+
+ >>> cron_customer_create.click('run_once')
+
+ >>> stripe_customer1.reload()
+ >>> stripe_customer1.identical_customers
+ []
+
+Create a second customer with same card::
+
+ >>> stripe_customer2 = StripeCustomer()
+ >>> stripe_customer2.party = customer2
+ >>> stripe_customer2.stripe_account = stripe_account
+ >>> _ = stripe_customer2.click('stripe_checkout')
+ >>> token = stripe.Token.create(
+ ... card={
+ ... 'number': '4012888888881881',
+ ... 'exp_month': 12,
+ ... 'exp_year': dt.date.today().year + 1,
+ ... 'cvc': '123',
+ ... },
+ ... )
+ >>> StripeCustomer.write(
+ ... [stripe_customer2.id], {'stripe_token': token.id}, config.context)
+
+Run cron::
+
+ >>> cron_customer_create.click('run_once')
+
+ >>> stripe_customer2.reload()
+ >>> stripe_customer2.identical_customers == [stripe_customer1]
+ True
+ >>> stripe_customer1.reload()
+ >>> stripe_customer1.identical_customers == [stripe_customer2]
+ True
diff -r 5443f5e69731 -r 990b40c7b2e0 view/customer_form.xml
--- a/view/customer_form.xml Mon May 02 17:23:53 2022 +0200
+++ b/view/customer_form.xml Sun Jul 17 19:07:38 2022 +0200
@@ -14,6 +14,7 @@
<group id="buttons" col="-1" colspan="4">
<button name="stripe_checkout"/>
<button name="detach_source"/>
+ <button name="find_identical"/>
</group>
<label name="stripe_error_message"/>
<field name="stripe_error_message"/>
@@ -22,4 +23,6 @@
<field name="stripe_error_code"/>
<label name="stripe_error_param"/>
<field name="stripe_error_param"/>
+
+ <field name="identical_customers" colspan="4"/>
</form>