Projet

Général

Profil

0001-lingo-factorize-eopayment-response-handling-49149.patch

Benjamin Dauvergne, 06 mai 2021 15:23

Télécharger (18,5 ko)

Voir les différences:

Subject: [PATCH 1/3] lingo: factorize eopayment response handling (#49149)

 combo/apps/lingo/models.py | 174 +++++++++++++++++++++++++++++++++++--
 combo/apps/lingo/views.py  | 154 ++++----------------------------
 2 files changed, 184 insertions(+), 144 deletions(-)
combo/apps/lingo/models.py
60 60
    UserSAMLIdentifier = None
61 61

  
62 62

  
63
logger = logging.getLogger('combo.apps.lingo')
64

  
65

  
66
class PaymentException(Exception):
67
    pass
68

  
69

  
70
class UnsignedPaymentException(PaymentException):
71
    def __init__(self, transaction, *args, **kwargs):
72
        super(UnsignedPaymentException, self).__init__(*args, **kwargs)
73
        self.transaction = transaction
74

  
75

  
76
class UnknownPaymentException(PaymentException):
77
    pass
78

  
79

  
63 80
EXPIRED = 9999
64 81

  
65 82

  
......
79 96
]
80 97

  
81 98

  
99
def eopayment_response_to_extra_info(response, **kwargs):
100
    extra_info = dict(kwargs)
101
    extra_info.update(
102
        {
103
            'eopayment_order_id': response.order_id,
104
            'eopayment_response': repr(response),
105
        }
106
    )
107
    for k, v in response.bank_data.items():
108
        extra_info['eopayment_bank_data_' + k] = v
109
    return extra_info
110

  
111

  
82 112
class RegieException(Exception):
83 113
    pass
84 114

  
......
170 200
        backend = next(serializers.deserialize('json', json.dumps([json_backend]), ignorenonexistent=True))
171 201
        backend.save()
172 202

  
203
    def handle_backend_response(self, response, callback=True):
204
        try:
205
            transaction = Transaction.objects.get(order_id=response.order_id)
206
        except Transaction.DoesNotExist:
207
            logger.warning(
208
                'lingo: transaction not found for payment response with id %s',
209
                response.order_id,
210
                extra=eopayment_response_to_extra_info(response),
211
            )
212
            raise UnknownPaymentException('Received unknown payment response')
213
        else:
214
            logger.debug(
215
                'lingo: backend "%s" received payment response with id %%s' % self,
216
                response.order_id,
217
                extra=eopayment_response_to_extra_info(
218
                    response, lingo_transaction_id=transaction.pk, user=transaction.user
219
                ),
220
            )
221

  
222
        # check if transaction belong to the right payment backend
223
        if not transaction.regie.payment_backend == self:
224
            logger.warning(
225
                'lingo: backend "%s" received payment for backend "%s"',
226
                self.regie.payment_backend,
227
                self,
228
                extra=eopayment_response_to_extra_info(response),
229
            )
230
            raise PaymentException('Invalid payment backend')
231
        transaction.handle_backend_response(response, callback=callback)
232
        return transaction
233

  
173 234

  
174 235
@python_2_unicode_compatible
175 236
class Regie(models.Model):
......
346 407
            headers={'content-type': 'application/json'},
347 408
        )
348 409
        if response.status_code != 200 or response.json().get('err'):
349
            logger = logging.getLogger(__name__)
350 410
            logger.error('failed to compute extra fees (user: %r)', user)
351 411
            return
352 412
        basketitems.filter(extra_fee=True).delete()
......
645 705
                remote_item.payment_date = paid_items[remote_item.id]
646 706

  
647 707

  
708
def status_label(status):
709
    return {
710
        0: _('Running'),
711
        eopayment.WAITING: _('Running'),
712
        eopayment.PAID: _('Paid'),
713
        eopayment.ACCEPTED: _('Paid (accepted)'),
714
        eopayment.CANCELLED: _('Cancelled'),
715
        EXPIRED: _('Expired'),
716
    }.get(status) or _('Unknown')
717

  
718

  
648 719
class Transaction(models.Model):
649 720
    regie = models.ForeignKey(Regie, on_delete=models.CASCADE, null=True)
650 721
    items = models.ManyToManyField(BasketItem, blank=True)
......
671 742
    def is_paid(self):
672 743
        return self.status in (eopayment.PAID, eopayment.ACCEPTED)
673 744

  
745
    def is_running(self):
746
        return self.status in [0, eopayment.WAITING, eopayment.RECEIVED]
747

  
674 748
    def get_status_label(self):
675
        return {
676
            0: _('Running'),
677
            eopayment.PAID: _('Paid'),
678
            eopayment.ACCEPTED: _('Paid (accepted)'),
679
            eopayment.CANCELLED: _('Cancelled'),
680
            EXPIRED: _('Expired'),
681
        }.get(self.status) or _('Unknown')
749
        return status_label(self.status)
682 750

  
683 751
    def first_notify_remote_items_of_payments(self):
684 752
        self.notify_remote_items_of_payments(self.remote_items)
......
687 755
        self.notify_remote_items_of_payments(self.to_be_paid_remote_items)
688 756

  
689 757
    def notify_remote_items_of_payments(self, items):
690
        logger = logging.getLogger(__name__)
691 758
        if not items:
692 759
            return
693 760
        if not self.is_paid():
......
734 801
        self.to_be_paid_remote_items = ','.join(to_be_paid_remote_items) or None
735 802
        self.save(update_fields=['to_be_paid_remote_items'])
736 803

  
804
    def handle_backend_response(self, response, callback=True):
805
        logger.debug('lingo: regie "%s" handling response for transaction "%%s"' % self.regie, self.order_id)
806
        if self.status == response.result:
807
            # return early if self status didn't change (it means the
808
            # payment service sent the response both as server to server and
809
            # via the user browser and we already handled one).
810
            return
811

  
812
        if not self.is_running():
813
            logger.info(
814
                'lingo: regie "%s" received payment notification on existing '
815
                'transaction, status changed, "%%s" (%%s) -> "%%s" (%%s)' % self.regie,
816
                status_label(self.status),
817
                self.status,
818
                status_label(response.result),
819
                response.result,
820
            )
821

  
822
        if not response.signed and not response.result == eopayment.CANCELLED:
823
            # we accept unsigned cancellation requests as some platforms do
824
            # that :/
825
            logger.warning(
826
                'lingo: regie "%s" received unsigned payment response with id %%s' % self.regie,
827
                response.order_id,
828
            )
829
            raise UnsignedPaymentException(self, 'Received unsigned payment response')
830
        self.status = response.result
831
        self.bank_transaction_id = response.transaction_id
832
        self.bank_data = response.bank_data
833
        self.end_date = timezone.now()
834
        # store transaction_date but prevent multiple updates
835
        if response.transaction_date is None:
836
            logger.warning('lingo: no transaction date')
837
        elif self.bank_transaction_date is None:
838
            self.bank_transaction_date = response.transaction_date
839
        elif response.transaction_date != self.bank_transaction_date:
840
            # XXX: don't know if it can happen, but we would like to know when it does
841
            # as for differed payments there can be multiple notifications.
842
            logger.error(
843
                'lingo: regie "%s" new transaction_date for transaction %%s(%%s) was %%s, received %%s'
844
                % self.regie,
845
                self.order_id,
846
                self.id,
847
                self.bank_transaction_date,
848
                response.transaction_date,
849
            )
850

  
851
        self.save()
852

  
853
        logger.info(
854
            'lingo: regie "%s" received %s payment notification for transaction %%s(%%s)'
855
            % (self.regie, 'synchronous' if callback else 'asynchronous'),
856
            self.order_id,
857
            self.id,
858
            extra=eopayment_response_to_extra_info(response),
859
        )
860

  
861
        if response.result == eopayment.WAITING:
862
            # mark basket items as waiting for payment confirmation
863
            self.items.all().update(waiting_date=timezone.now())
864
            return
865

  
866
        if response.result == eopayment.CANCELLED:
867
            # mark basket items as no longer waiting so the user can restart a
868
            # payment.
869
            self.items.all().update(waiting_date=None)
870
            return
871

  
872
        if response.result not in (eopayment.PAID, eopayment.ACCEPTED):
873
            return
874

  
875
        self.items.update(payment_date=self.end_date)
876

  
877
        for item in self.items.all():
878
            try:
879
                item.notify_payment()
880
            except Exception as e:
881
                # ignore errors, it will be retried later on if it fails
882
                logger.warning(
883
                    'lingo: regie "%s" error in sync notification for basket item %%s '
884
                    'and transaction %%s, %%s' % self.regie,
885
                    item.id,
886
                    self.order_id,
887
                    e,
888
                )
889

  
890
        if self.remote_items:
891
            self.first_notify_remote_items_of_payments()
892

  
737 893

  
738 894
class TransactionOperation(models.Model):
739 895
    OPERATIONS = [
combo/apps/lingo/views.py
55 55
    BasketItem,
56 56
    LingoBasketCell,
57 57
    PaymentBackend,
58
    PaymentException,
58 59
    Regie,
59 60
    RemoteInvoiceException,
60 61
    SelfDeclaredInvoicePayment,
61 62
    Transaction,
62 63
    TransactionOperation,
64
    UnknownPaymentException,
65
    UnsignedPaymentException,
63 66
)
64 67
from .utils import signing_dumps, signing_loads
65 68

  
......
607 610
        )
608 611

  
609 612

  
610
class PaymentException(Exception):
611
    pass
612

  
613

  
614
class UnsignedPaymentException(PaymentException):
615
    def __init__(self, transaction, *args, **kwargs):
616
        super(UnsignedPaymentException, self).__init__(*args, **kwargs)
617
        self.transaction = transaction
618

  
619

  
620
class UnknownPaymentException(PaymentException):
621
    pass
622

  
623

  
624 613
class PaymentView(View):
625
    def handle_response(self, request, backend_response, **kwargs):
614
    def handle_response(self, request, backend_response, *, callback=True, transaction=None, **kwargs):
626 615
        if 'regie_pk' in kwargs:
627 616
            payment_backend = get_object_or_404(Regie, pk=kwargs['regie_pk']).payment_backend
628 617
        elif 'payment_backend_pk' in kwargs:
......
632 621

  
633 622
        payment = get_eopayment_object(request, payment_backend)
634 623
        logger.info(u'received payment response: %r', backend_response)
635
        extra_info = kwargs.pop('payment_extra_info', {})
636 624
        try:
637
            payment_response = payment.response(backend_response, **extra_info)
625
            eopayment_response_kwargs = {'redirect': not callback}
626
            if transaction is not None:
627
                eopayment_response_kwargs.update(
628
                    {
629
                        'order_id_hint': transaction.order_id,
630
                        'order_status_hint': transaction.status,
631
                    }
632
                )
633
            payment_response = payment.response(backend_response, **eopayment_response_kwargs)
638 634
        except eopayment.PaymentException as e:
639 635
            logger.error(
640 636
                u'failed to process payment response: %s',
......
643 639
            )
644 640
            raise PaymentException('Failed to process payment response')
645 641

  
646
        extra_info = {
647
            'eopayment_order_id': smart_text(payment_response.order_id),
648
            'eopayment_response': repr(payment_response),
649
        }
650
        for k, v in payment_response.bank_data.items():
651
            extra_info['eopayment_bank_data_' + k] = smart_text(v)
652
        try:
653
            transaction = Transaction.objects.get(order_id=payment_response.order_id)
654
        except Transaction.DoesNotExist:
655
            logger.warning(
656
                u'received unknown payment response with id %s',
657
                smart_text(payment_response.order_id),
658
                extra=extra_info,
659
            )
660
            raise UnknownPaymentException('Received unknown payment response')
661
        else:
662
            extra_info['lingo_transaction_id'] = transaction.pk
663
            if transaction.user:
664
                # let hobo logger filter handle the extraction of user's infos
665
                extra_info['user'] = transaction.user
666
            logger.info(
667
                u'received known payment response with id %s',
668
                smart_text(payment_response.order_id),
669
                extra=extra_info,
670
            )
671

  
672
        if transaction.status == payment_response.result:
673
            # return early if transaction status didn't change (it means the
674
            # payment service sent the response both as server to server and
675
            # via the user browser and we already handled one).
676
            return transaction
677

  
678
        if transaction.status and transaction.status != payment_response.result:
679
            logger.info(
680
                u'received payment notification on existing transaction '
681
                '(status: %s, new status: %s)' % (transaction.status, payment_response.result)
682
            )
683

  
684
        # check if transaction belongs to right regie
685
        if not transaction.regie.payment_backend == payment_backend:
686
            logger.warning(
687
                u'received payment for inappropriate payment backend '
688
                '(expecteds: %s, received: %s)' % (transaction.regie.payment_backend, payment_backend)
689
            )
690
            raise PaymentException('Invalid payment regie')
691

  
692
        if not payment_response.signed and not payment_response.result == eopayment.CANCELLED:
693
            # we accept unsigned cancellation requests as some platforms do
694
            # that :/
695
            logger.warning(
696
                u'received unsigned payment response with id %s',
697
                smart_text(payment_response.order_id),
698
                extra=extra_info,
699
            )
700
            raise UnsignedPaymentException(transaction, 'Received unsigned payment response')
701

  
702
        transaction.status = payment_response.result
703
        transaction.bank_transaction_id = payment_response.transaction_id
704
        transaction.bank_data = payment_response.bank_data
705
        transaction.end_date = timezone.now()
706
        # store transaction_date but prevent multiple updates
707
        if payment_response.transaction_date is None:
708
            logger.warning('no transaction date')
709
        elif transaction.bank_transaction_date is None:
710
            transaction.bank_transaction_date = payment_response.transaction_date
711
        elif payment_response.transaction_date != transaction.bank_transaction_date:
712
            # XXX: don't know if it can happen, but I would like to know when it does
713
            # as for differed payments there can be multiple notifications.
714
            logger.error(
715
                'new transaction_date for transaction %s was %s, received %s',
716
                transaction.id,
717
                transaction.bank_transaction_date,
718
                payment_response.transaction_date,
719
            )
720
        transaction.save()
721

  
722
        if payment_response.result == eopayment.WAITING:
723
            # mark basket items as waiting for payment confirmation
724
            transaction.items.all().update(waiting_date=timezone.now())
725
            return transaction
726

  
727
        if payment_response.result == eopayment.CANCELLED:
728
            # mark basket items as no longer waiting so the user can restart a
729
            # payment.
730
            transaction.items.all().update(waiting_date=None)
731
            return transaction
732

  
733
        if payment_response.result not in (eopayment.PAID, eopayment.ACCEPTED):
734
            return transaction
735

  
736
        transaction.items.update(payment_date=transaction.end_date)
737

  
738
        for item in transaction.items.all():
739
            try:
740
                item.notify_payment()
741
            except:
742
                # ignore errors, it will be retried later on if it fails
743
                logger.exception('error in sync notification for basket item %s', item.id)
744

  
745
        if transaction.remote_items:
746
            transaction.first_notify_remote_items_of_payments()
747

  
748
        return transaction
642
        return payment_backend.handle_backend_response(payment_response)
749 643

  
750 644

  
751 645
class CallbackView(PaymentView):
......
783 677
        )
784 678

  
785 679
    def handle_return(self, request, backend_response, **kwargs):
786
        payment_extra_info = {'redirect': True}
787

  
788 680
        transaction = None
789 681
        transaction_id = kwargs.get('transaction_signature')
790 682
        if transaction_id:
......
792 684
                transaction_id = signing_loads(transaction_id)
793 685
            except signing.BadSignature:
794 686
                transaction_id = None
795

  
796
        if transaction_id:
797
            # retrieve info about previously known state
798
            try:
799
                current_transaction = Transaction.objects.get(pk=transaction_id)
800
            except Transaction.DoesNotExist:
801
                pass
802 687
            else:
803
                payment_extra_info['order_id_hint'] = current_transaction.order_id
804
                payment_extra_info['order_status_hint'] = current_transaction.status
688
                transaction = Transaction.objects.filter(id=transaction_id).first()
805 689

  
806 690
        try:
807 691
            transaction = self.handle_response(
808
                request, backend_response, payment_extra_info=payment_extra_info, **kwargs
692
                request, backend_response, callback=False, transaction=transaction, **kwargs
809 693
            )
810
        except UnsignedPaymentException as e:
694
        except UnsignedPaymentException:
811 695
            # some payment backends do not sign return URLs, don't mark this as
812 696
            # an error, they will provide a notification to the callback
813 697
            # endpoint.
814 698
            if transaction_id:
815 699
                return HttpResponseRedirect(get_payment_status_view(transaction_id))
816 700
            return HttpResponseRedirect(get_basket_url())
817
        except PaymentException as e:
701
        except PaymentException:
818 702
            messages.error(
819
                request, _('We are sorry but the payment service ' 'failed to provide a correct answer.')
703
                request, _('We are sorry but the payment service failed to provide a correct answer.')
820 704
            )
821 705
            if transaction_id:
822 706
                return HttpResponseRedirect(get_payment_status_view(transaction_id))
823
-