diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index a8014a451..1beb5ed81 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -231,6 +231,15 @@ created datetime Date and time o payment_date datetime Date and time of completion of this payment (or ``null``) provider string Identification string of the payment provider payment_url string The URL where an user can continue with the payment (or ``null``) +details object Payment-specific information. This is a dictionary + with various fields that can be different between + payment providers, versions, payment states, etc. If + you read this field, you always need to be able to + deal with situations where values that you expect are + missing. Mostly, the field contains various IDs that + can be used for matching with other systems. If a + payment provider does not implement this feature, + the object is empty. ===================================== ========================== ======================================================= .. versionchanged:: 2.0 @@ -239,7 +248,7 @@ payment_url string The URL where a .. versionchanged:: 3.1 - The attribute ``payment_url`` has been added. + The attributes ``payment_url`` and ``details`` have been added. .. _order-refund-resource: diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst index 4279268f0..cc83ce418 100644 --- a/doc/development/api/payment.rst +++ b/doc/development/api/payment.rst @@ -108,6 +108,8 @@ The provider class .. automethod:: execute_refund + .. automethod:: api_payment_details + .. automethod:: shred_payment_info .. autoattribute:: is_implicit diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 04b6f5fb5..e664356b5 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -297,12 +297,22 @@ class PaymentURLField(serializers.URLField): }) +class PaymentDetailsField(serializers.Field): + def to_representation(self, value: OrderPayment): + pp = value.payment_provider + if not pp: + return {} + return pp.api_payment_details(value) + + class OrderPaymentSerializer(I18nAwareModelSerializer): payment_url = PaymentURLField(source='*', allow_null=True, read_only=True) + details = PaymentDetailsField(source='*', allow_null=True, read_only=True) class Meta: model = OrderPayment - fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url') + fields = ('local_id', 'state', 'amount', 'created', 'payment_date', 'provider', 'payment_url', + 'details') class OrderRefundSerializer(I18nAwareModelSerializer): diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index b4d103e1f..bfbc52837 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -608,22 +608,24 @@ class Event(EventMixin, LoggedModel): question_map=question_map ) - def get_payment_providers(self) -> dict: + def get_payment_providers(self, cached=False) -> dict: """ Returns a dictionary of initialized payment providers mapped by their identifiers. """ from ..signals import register_payment_providers - responses = register_payment_providers.send(self) - providers = {} - for receiver, response in responses: - if not isinstance(response, list): - response = [response] - for p in response: - pp = p(self) - providers[pp.identifier] = pp + if not cached or not hasattr(self, '_cached_payment_providers'): + responses = register_payment_providers.send(self) + providers = {} + for receiver, response in responses: + if not isinstance(response, list): + response = [response] + for p in response: + pp = p(self) + providers[pp.identifier] = pp - return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name))) + self._cached_payment_providers = OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name))) + return self._cached_payment_providers def get_html_mail_renderer(self): """ diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 91e0561ef..9dc46747b 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1196,7 +1196,7 @@ class OrderPayment(models.Model): """ Cached access to an instance of the payment provider in use. """ - return self.order.event.get_payment_providers().get(self.provider) + return self.order.event.get_payment_providers(cached=True).get(self.provider) def _mark_paid(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False): from pretix.base.signals import order_paid diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 7c4094870..1a61d8844 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -657,6 +657,15 @@ class BasePaymentProvider: obj.info = '{}' obj.save(update_fields=['info']) + def api_payment_details(self, payment: OrderPayment): + """ + Will be called to populate the ``details`` parameter of the payment in the REST API. + + :param payment: The payment in question. + :return: A serializable dictionary + """ + return {} + class PaymentException(Exception): pass @@ -720,6 +729,12 @@ class BoxOfficeProvider(BasePaymentProvider): def order_change_allowed(self, order: Order) -> bool: return False + def api_payment_details(self, payment: OrderPayment): + return { + "pos_id": payment.info_data.get('pos_id', None), + "receipt_id": payment.info_data.get('receipt_id', None), + } + def payment_control_render(self, request, payment) -> str: if not payment.info: return @@ -864,6 +879,11 @@ class OffsettingProvider(BasePaymentProvider): def order_change_allowed(self, order: Order) -> bool: return False + def api_payment_details(self, payment: OrderPayment): + return { + "orders": payment.info_data.get('orders', []), + } + def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str: return _('Balanced against orders: %s' % ', '.join(payment.info_data['orders'])) diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py index 2d18e4cbd..dd0f6f5df 100644 --- a/src/pretix/plugins/paypal/payment.py +++ b/src/pretix/plugins/paypal/payment.py @@ -395,6 +395,20 @@ class Paypal(BasePaymentProvider): 'retry': retry, 'order': payment.order} return template.render(ctx) + def api_payment_details(self, payment: OrderPayment): + sale_id = None + for trans in payment.info_data.get('transactions', []): + for res in trans.get('related_resources', []): + if 'sale' in res and 'id' in res['sale']: + sale_id = res['sale']['id'] + return { + "payer_email": payment.info_data.get('payer', {}).get('payer_info', {}).get('email'), + "payer_id": payment.info_data.get('payer', {}).get('payer_info', {}).get('payer_id'), + "cart_id": payment.info_data.get('cart', None), + "payment_id": payment.info_data.get('id', None), + "sale_id": sale_id, + } + def payment_control_render(self, request: HttpRequest, payment: OrderPayment): template = get_template('pretixplugins/paypal/control.html') ctx = {'request': request, 'event': self.event, 'settings': self.settings, diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 26d2aef21..1342e17f6 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -411,6 +411,12 @@ class StripeMethod(BasePaymentProvider): } return template.render(ctx) + def api_payment_details(self, payment: OrderPayment): + return { + "id": payment.info_data.get("id", None), + "payment_method": payment.info_data.get("payment_method", None) + } + def payment_control_render(self, request, payment) -> str: if payment.info: payment_info = json.loads(payment.info) diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 3e3151a6d..a91d4eca6 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -160,6 +160,10 @@ TEST_PAYMENTS_RES = [ "payment_date": "2017-12-01T10:00:00Z", "provider": "stripe", "payment_url": None, + "details": { + "id": None, + "payment_method": None + }, "state": "refunded", "amount": "23.00" }, @@ -169,6 +173,7 @@ TEST_PAYMENTS_RES = [ "payment_date": None, "provider": "banktransfer", "payment_url": None, + "details": {}, "state": "pending", "amount": "23.00" }