diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index d1b12e030..043a51112 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order total money (string) Total value of this order comment string Internal comment on this order +api_meta object Meta data for that order. Only available through API, no guarantees + on the content structure. You can use this to save references to your system. custom_followup_at date Internal date for a custom follow-up action checkin_attention boolean If ``true``, the check-in app should show a warning that this ticket requires special attention if a ticket @@ -562,6 +564,7 @@ Fetching individual orders "fees": [], "total": "23.00", "comment": "", + "api_meta": {}, "custom_followup_at": null, "checkin_attention": false, "checkin_text": null, @@ -742,6 +745,8 @@ Updating order fields * ``comment`` + * ``api_meta`` + * ``custom_followup_at`` * ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address) diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index e8fd1fc31..f7b6ef800 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -39,7 +39,7 @@ Frontend .. automodule:: pretix.presale.signals - :members: order_info, order_info_top, order_meta_from_request + :members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request Request flow """""""""""" diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 4898b4e5b..b9869a7c7 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -726,7 +726,7 @@ class OrderSerializer(I18nAwareModelSerializer): 'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date', 'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', - 'url', 'customer', 'valid_if_pending' + 'url', 'customer', 'valid_if_pending', 'api_meta' ) read_only_fields = ( 'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date', @@ -786,7 +786,7 @@ class OrderSerializer(I18nAwareModelSerializer): # Even though all fields that shouldn't be edited are marked as read_only in the serializer # (hopefully), we'll be extra careful here and be explicit about the model fields we update. update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale', - 'phone', 'valid_if_pending'] + 'phone', 'valid_if_pending', 'api_meta'] if 'invoice_address' in validated_data: iadata = validated_data.pop('invoice_address') @@ -1059,7 +1059,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', 'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date', 'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', - 'require_approval', 'valid_if_pending', 'expires') + 'require_approval', 'valid_if_pending', 'expires', 'api_meta') def validate_payment_provider(self, pp): if pp is None: diff --git a/src/pretix/base/migrations/0269_order_api_meta.py b/src/pretix/base/migrations/0269_order_api_meta.py new file mode 100644 index 000000000..dc9fd5fed --- /dev/null +++ b/src/pretix/base/migrations/0269_order_api_meta.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-07-17 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0268_remove_subevent_items_remove_subevent_variations_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='api_meta', + field=models.JSONField(default=dict), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index ad2bbf27b..fadf1c972 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -299,6 +299,11 @@ class Order(LockModel, LoggedModel): verbose_name=_("Meta information"), null=True, blank=True ) + api_meta = models.JSONField( + verbose_name=_("API meta information"), + null=False, blank=True, + default=dict + ) last_modified = models.DateTimeField( auto_now=True, db_index=False ) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index fa8762d18..b48eec48d 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -960,7 +960,7 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime, payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None, address: InvoiceAddress=None, meta_info: dict=None, shown_total=None, - customer=None, valid_if_pending=False): + customer=None, valid_if_pending=False, api_meta: dict=None): payments = [] try: @@ -985,6 +985,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no total=total, testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False, meta_info=json.dumps(meta_info or {}), + api_meta=api_meta or {}, require_approval=require_approval, sales_channel=sales_channel, customer=customer, @@ -1096,7 +1097,7 @@ def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosi def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str], email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web', - shown_total=None, customer=None): + shown_total=None, customer=None, api_meta: dict=None): for p in payment_requests: p['pprov'] = event.get_payment_providers(cached=True)[p['provider']] if not p['pprov']: @@ -1200,7 +1201,8 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis sales_channel=sales_channel, shown_total=shown_total, customer=customer, - valid_if_pending=valid_if_pending + valid_if_pending=valid_if_pending, + api_meta=api_meta, ) try: @@ -2873,12 +2875,13 @@ class OrderChangeManager: @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) def perform_order(self, event: Event, payments: List[dict], positions: List[str], email: str=None, locale: str=None, address: int=None, meta_info: dict=None, - sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None): + sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None, + api_meta: dict=None): with language(locale), time_machine_now_assigned(override_now_dt): try: try: return _perform_order(event, payments, positions, email, locale, address, meta_info, - sales_channel, shown_total, customer) + sales_channel, shown_total, customer, api_meta) except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 12b238036..a0afcb8ca 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -85,7 +85,7 @@ from pretix.presale.forms.customer import AuthenticationForm, RegistrationForm from pretix.presale.signals import ( checkout_all_optional, checkout_confirm_messages, checkout_flow_steps, contact_form_fields, contact_form_fields_overrides, - order_meta_from_request, question_form_fields, + order_api_meta_from_request, order_meta_from_request, question_form_fields, question_form_fields_overrides, ) from pretix.presale.utils import customer_login @@ -1544,11 +1544,14 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): str(m) for m in self.confirm_messages.values() ] } + api_meta = {} unlock_hashes = request.session.get('pretix_unlock_hashes', []) if unlock_hashes: meta_info['unlock_hashes'] = unlock_hashes for receiver, response in order_meta_from_request.send(sender=request.event, request=request): meta_info.update(response) + for receiver, response in order_api_meta_from_request.send(sender=request.event, request=request): + api_meta.update(response) return self.do( self.request.event.id, @@ -1562,6 +1565,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): shown_total=self.cart_session.get('shown_total'), customer=self.cart_session.get('customer'), override_now_dt=time_machine_now(default=None), + api_meta=api_meta, ) def get_success_message(self, value): diff --git a/src/pretix/presale/signals.py b/src/pretix/presale/signals.py index c4387ae44..97bbd7e06 100644 --- a/src/pretix/presale/signals.py +++ b/src/pretix/presale/signals.py @@ -170,6 +170,17 @@ You will receive the request triggering the order creation as the ``request`` ke As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ +order_api_meta_from_request = EventPluginSignal() +""" +Arguments: ``request`` + +This signal is sent before an order is created through the pretixpresale frontend. It allows you +to return a dictionary that will be merged in the api_meta attribute of the order. +You will receive the request triggering the order creation as the ``request`` keyword argument. + +As with all event-plugin signals, the ``sender`` keyword argument will contain the event. +""" + checkout_confirm_page_content = EventPluginSignal() """ Arguments: ``request`` diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py index 964b18ef3..ca271e123 100644 --- a/src/tests/api/test_order_change.py +++ b/src/tests/api/test_order_change.py @@ -255,6 +255,9 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): organizer.slug, event.slug, order.code ), format='json', data={ 'comment': 'Here is a comment', + 'api_meta': { + 'test': 1 + }, 'valid_if_pending': True, 'custom_followup_at': '2021-06-12', 'checkin_attention': True, @@ -280,6 +283,9 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): assert resp.status_code == 200 order.refresh_from_db() assert order.comment == 'Here is a comment' + assert order.api_meta == { + 'test': 1 + } assert order.custom_followup_at.isoformat() == '2021-06-12' assert order.checkin_attention assert order.checkin_text == 'foobar' diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py index daab41cd5..b52fbad1c 100644 --- a/src/tests/api/test_order_create.py +++ b/src/tests/api/test_order_create.py @@ -232,6 +232,9 @@ def test_order_create(token_client, organizer, event, item, quota, question): with scopes_disabled(): customer = organizer.customers.create() res['customer'] = customer.identifier + res['api_meta'] = { + 'test': 1 + } resp = token_client.post( '/api/v1/organizers/{}/events/{}/orders/'.format( organizer.slug, event.slug @@ -251,6 +254,9 @@ def test_order_create(token_client, organizer, event, item, quota, question): assert o.valid_if_pending assert o.expires > now() assert not o.testmode + assert o.api_meta == { + 'test': 1 + } with scopes_disabled(): p = o.payments.first() @@ -421,6 +427,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques ], 'total': '21.75', 'comment': '', + 'api_meta': {}, "custom_followup_at": None, 'invoice_address': { 'is_business': False, diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index f2ebbbfa9..fe4a6c900 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -291,6 +291,7 @@ TEST_ORDER_RES = { "payment_provider": "banktransfer", "total": "23.00", "comment": "", + "api_meta": {}, "custom_followup_at": None, "checkin_attention": False, "checkin_text": None,