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>

Reply via email to