0001-lingo-factorize-eopayment-response-handling-49149.patch
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 |
- |