forked from CGM_Public/pretix_original
API: Add transactions (#5292)
* API: Add transactions * Apply suggestions from code review Co-authored-by: Richard Schreiber <schreiber@rami.io> --------- Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -25,6 +25,7 @@ at :ref:`plugin-docs`.
|
|||||||
seats
|
seats
|
||||||
orders
|
orders
|
||||||
invoices
|
invoices
|
||||||
|
transactions
|
||||||
vouchers
|
vouchers
|
||||||
discounts
|
discounts
|
||||||
checkin
|
checkin
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _rest-invoices:
|
||||||
|
|
||||||
Invoices
|
Invoices
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|||||||
232
doc/api/resources/transactions.rst
Normal file
232
doc/api/resources/transactions.rst
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
.. _rest-transactions:
|
||||||
|
|
||||||
|
Transactions
|
||||||
|
============
|
||||||
|
|
||||||
|
Transactions are an additional way to think about orders. They are are an immutable, filterable view into an order's
|
||||||
|
history and are a good basis for financial reporting.
|
||||||
|
|
||||||
|
Our financial model
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
You can think of a pretix order similar to a debtor account in double-entry bookkeeping. For example, the flow of an
|
||||||
|
order could look like this:
|
||||||
|
|
||||||
|
===================================================== ==================== =====================
|
||||||
|
Transaction Debit Credit
|
||||||
|
===================================================== ==================== =====================
|
||||||
|
Order is placed with two tickets € 500
|
||||||
|
Order is paid partially with a gift card € 200
|
||||||
|
Remainder is paid with a credit card € 300
|
||||||
|
One of the tickets is canceled **-** € 250
|
||||||
|
Refund is made to the credit card **-** € 250
|
||||||
|
**Balance** **€ 250** **€ 250**
|
||||||
|
===================================================== ==================== =====================
|
||||||
|
|
||||||
|
If an order is fully settled, the sums of both columns match. However, as the movements in both columns do not always
|
||||||
|
happen at the same time, at some times during the lifecycle of an order the sums are not balanced, in which case we
|
||||||
|
consider an order to be "pending payment" or "overpaid".
|
||||||
|
|
||||||
|
In the API, the "Debit" column is represented by the "transaction" resource listed on this page.
|
||||||
|
In many cases, the left column *usually* also matches the data returned by the :ref:`rest-invoices` resource, but there
|
||||||
|
are two important differences:
|
||||||
|
|
||||||
|
- pretix may be configured such that an invoice is not always generated for an order. In this case, only the transactions
|
||||||
|
return the full data set.
|
||||||
|
|
||||||
|
- pretix does not enforce a new invoice to be created e.g. when a ticket is changed to a different subevent. However,
|
||||||
|
pretix always creates a new transaction whenever there is a change to a ticket that concerns the **price**, **tax rate**,
|
||||||
|
**product**, or **date** (in an event series).
|
||||||
|
|
||||||
|
The :ref:`rest-orders` themselves are not a good representation of the "Debit" side of the table for accounting
|
||||||
|
purposes since they are not immutable:
|
||||||
|
They will only tell you the current state of the order, not what it was a week ago.
|
||||||
|
|
||||||
|
The "Credit" column is represented by the :ref:`order-payment-resource` and :ref:`order-refund-resource`.
|
||||||
|
|
||||||
|
|
||||||
|
Resource description
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. rst-class:: rest-resource-table
|
||||||
|
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
Field Type Description
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
id integer Internal ID of the transaction
|
||||||
|
order string Order code the transaction was created from
|
||||||
|
event string Event slug, only present on organizer-level API calls
|
||||||
|
created datetime The creation time of the transaction in the database
|
||||||
|
datetime datetime The time at which the transaction is financially relevant.
|
||||||
|
This is usually the same as created, but may vary for
|
||||||
|
retroactively created transactions after software bugs or
|
||||||
|
for data that preceeds this data model.
|
||||||
|
positionid integer Number of the position within the order this refers to,
|
||||||
|
is ``null`` for transactions that refer to a fee
|
||||||
|
count integer Number of items purchased, is negative for cancellations
|
||||||
|
item integer The internal ID of the item purchased (or ``null`` for fees)
|
||||||
|
variation integer The internal ID of the variation purchased (or ``null``)
|
||||||
|
subevent integer The internal ID of the event series date (or ``null``)
|
||||||
|
price money (string) Gross price of the transaction
|
||||||
|
tax_rate decimal (string) Tax rate applied in transaction
|
||||||
|
tax_rule integer The internal ID of the tax rule used (or ``null``)
|
||||||
|
tax_code string The selected tax code (or ``null``)
|
||||||
|
tax_value money (string) The computed tax value
|
||||||
|
fee_type string The type of fee (or ``null`` for products)
|
||||||
|
internal_type string Additional type classification of the fee (or ``null`` for products)
|
||||||
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
.. versionchanged:: 2025.7.0
|
||||||
|
|
||||||
|
This resource was added to the API.
|
||||||
|
|
||||||
|
|
||||||
|
Endpoints
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/transactions/
|
||||||
|
|
||||||
|
Returns a list of all transactions of an event.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/events/sampleconf/transactions/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"order": "FOO",
|
||||||
|
"count": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
|
"item": null,
|
||||||
|
"variation": null,
|
||||||
|
"positionid": 1,
|
||||||
|
"price": "23.00",
|
||||||
|
"subevent": null,
|
||||||
|
"tax_code": "E",
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_rule": 23,
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"fee_type": null,
|
||||||
|
"internal_type": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string order: Only return transactions matching the given order code.
|
||||||
|
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
||||||
|
:query datetime_before: Only return transactions with a datetime before the given time.
|
||||||
|
:query created_since: Only return transactions with a creation time at or after the given time.
|
||||||
|
:query created_before: Only return transactions with a creation time before the given time.
|
||||||
|
:query item: Only return transactions that match the given item ID.
|
||||||
|
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
||||||
|
:query variation: Only return transactions that match the given variation ID.
|
||||||
|
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
||||||
|
:query subevent: Only return transactions that match the given subevent ID.
|
||||||
|
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
||||||
|
:query tax_rule: Only return transactions that match the given tax rule ID.
|
||||||
|
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
||||||
|
:query tax_code: Only return transactions that match the given tax code.
|
||||||
|
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
||||||
|
:query tax_rate: Only return transactions that match the given tax rate.
|
||||||
|
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
||||||
|
:query fee_type: Only return transactions that match the given fee type.
|
||||||
|
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
||||||
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:param event: The ``slug`` field of a valid event
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
|
.. http:get:: /api/v1/organizers/(organizer)/transactions/
|
||||||
|
|
||||||
|
Returns a list of all transactions of an organizer that you have access to.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/organizers/bigevents/transactions/ HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Accept: application/json, text/javascript
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"event": "sampleconf",
|
||||||
|
"order": "FOO",
|
||||||
|
"count": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
|
"item": null,
|
||||||
|
"variation": null,
|
||||||
|
"positionid": 1,
|
||||||
|
"price": "23.00",
|
||||||
|
"subevent": null,
|
||||||
|
"tax_code": "E",
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_rule": 23,
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"fee_type": null,
|
||||||
|
"internal_type": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||||
|
:query string event: Only return transactions matching the given event slug.
|
||||||
|
:query string order: Only return transactions matching the given order code.
|
||||||
|
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
||||||
|
:query datetime_before: Only return transactions with a datetime before the given time.
|
||||||
|
:query created_since: Only return transactions with a creation time at or after the given time.
|
||||||
|
:query created_before: Only return transactions with a creation time before the given time.
|
||||||
|
:query item: Only return transactions that match the given item ID.
|
||||||
|
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
||||||
|
:query variation: Only return transactions that match the given variation ID.
|
||||||
|
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
||||||
|
:query subevent: Only return transactions that match the given subevent ID.
|
||||||
|
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
||||||
|
:query tax_rule: Only return transactions that match the given tax rule ID.
|
||||||
|
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
||||||
|
:query tax_code: Only return transactions that match the given tax code.
|
||||||
|
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
||||||
|
:query tax_rate: Only return transactions that match the given tax rate.
|
||||||
|
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
||||||
|
:query fee_type: Only return transactions that match the given fee type.
|
||||||
|
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
||||||
|
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
||||||
|
:param organizer: The ``slug`` field of a valid organizer
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 401: Authentication failure
|
||||||
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
@@ -56,7 +56,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
PrintLog, RevokedTicketSecret,
|
PrintLog, RevokedTicketSecret, Transaction,
|
||||||
)
|
)
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
@@ -1783,3 +1783,23 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = BlockedTicketSecret
|
model = BlockedTicketSecret
|
||||||
fields = ('id', 'secret', 'updated', 'blocked')
|
fields = ('id', 'secret', 'updated', 'blocked')
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionSerializer(I18nAwareModelSerializer):
|
||||||
|
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Transaction
|
||||||
|
fields = (
|
||||||
|
"id", "order", "created", "datetime", "positionid", "count", "item", "variation",
|
||||||
|
"subevent", "price", "tax_rate", "tax_rule", "tax_code", "tax_value", "fee_type",
|
||||||
|
"internal_type"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizerTransactionSerializer(TransactionSerializer):
|
||||||
|
event = serializers.SlugRelatedField(source="order.event", slug_field="slug", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Transaction
|
||||||
|
fields = TransactionSerializer.Meta.fields + ("event",)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ orga_router.register(r'orders', order.OrganizerOrderViewSet)
|
|||||||
orga_router.register(r'invoices', order.InvoiceViewSet)
|
orga_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
||||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||||
|
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
|
||||||
|
|
||||||
team_router = routers.DefaultRouter()
|
team_router = routers.DefaultRouter()
|
||||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||||
@@ -83,6 +84,7 @@ event_router.register(r'quotas', item.QuotaViewSet)
|
|||||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||||
event_router.register(r'orders', order.EventOrderViewSet)
|
event_router.register(r'orders', order.EventOrderViewSet)
|
||||||
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||||
|
event_router.register(r'transactions', order.TransactionViewSet)
|
||||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ from pretix.api.serializers.order import (
|
|||||||
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
|
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
|
||||||
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
OrderPaymentCreateSerializer, OrderPaymentSerializer,
|
||||||
OrderPositionSerializer, OrderRefundCreateSerializer,
|
OrderPositionSerializer, OrderRefundCreateSerializer,
|
||||||
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
|
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
|
||||||
PrintLogSerializer, RevokedTicketSecretSerializer,
|
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
|
||||||
SimulatedOrderSerializer,
|
SimulatedOrderSerializer, TransactionSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.orderchange import (
|
from pretix.api.serializers.orderchange import (
|
||||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||||
@@ -80,6 +80,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret,
|
||||||
|
Transaction,
|
||||||
)
|
)
|
||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.pdf import get_images
|
from pretix.base.pdf import get_images
|
||||||
@@ -2030,3 +2031,61 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return BlockedTicketSecret.objects.filter(event=self.request.event)
|
return BlockedTicketSecret.objects.filter(event=self.request.event)
|
||||||
|
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
class TransactionFilter(FilterSet):
|
||||||
|
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact')
|
||||||
|
event = django_filters.CharFilter(field_name='order__event', lookup_expr='slug__iexact')
|
||||||
|
datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||||
|
datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||||
|
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
||||||
|
created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Transaction
|
||||||
|
fields = {
|
||||||
|
'item': ['exact', 'in'],
|
||||||
|
'variation': ['exact', 'in'],
|
||||||
|
'subevent': ['exact', 'in'],
|
||||||
|
'tax_rule': ['exact', 'in'],
|
||||||
|
'tax_code': ['exact', 'in'],
|
||||||
|
'tax_rate': ['exact', 'in'],
|
||||||
|
'fee_type': ['exact', 'in'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
serializer_class = TransactionSerializer
|
||||||
|
queryset = Transaction.objects.none()
|
||||||
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
|
ordering = ('datetime', 'pk')
|
||||||
|
ordering_fields = ('datetime', 'created', 'id',)
|
||||||
|
filterset_class = TransactionFilter
|
||||||
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Transaction.objects.filter(order__event=self.request.event).select_related("order")
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizerTransactionViewSet(TransactionViewSet):
|
||||||
|
serializer_class = OrganizerTransactionSerializer
|
||||||
|
permission = None
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = Transaction.objects.filter(
|
||||||
|
order__event__organizer=self.request.organizer
|
||||||
|
).select_related("order", "order__event")
|
||||||
|
|
||||||
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
qs = qs.filter(
|
||||||
|
order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
|
||||||
|
)
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
qs = qs.filter(
|
||||||
|
order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PermissionDenied("Unknown authentication scheme")
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ event_permission_sub_urls = [
|
|||||||
('get', 'can_view_orders', 'revokedsecrets/1/', 404),
|
('get', 'can_view_orders', 'revokedsecrets/1/', 404),
|
||||||
('get', 'can_view_orders', 'blockedsecrets/', 200),
|
('get', 'can_view_orders', 'blockedsecrets/', 200),
|
||||||
('get', 'can_view_orders', 'blockedsecrets/1/', 404),
|
('get', 'can_view_orders', 'blockedsecrets/1/', 404),
|
||||||
|
('get', 'can_view_orders', 'transactions/', 200),
|
||||||
|
('get', 'can_view_orders', 'transactions/1/', 404),
|
||||||
('get', 'can_view_orders', 'orders/', 200),
|
('get', 'can_view_orders', 'orders/', 200),
|
||||||
('get', 'can_view_orders', 'orderpositions/', 200),
|
('get', 'can_view_orders', 'orderpositions/', 200),
|
||||||
('delete', 'can_change_orders', 'orderpositions/1/', 404),
|
('delete', 'can_change_orders', 'orderpositions/1/', 404),
|
||||||
|
|||||||
254
src/tests/api/test_transactions.py
Normal file
254
src/tests/api/test_transactions.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import freezegun
|
||||||
|
import pytest
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from pretix.base.models import Order, OrderPosition
|
||||||
|
from pretix.base.models.orders import OrderFee
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def item(event):
|
||||||
|
return event.items.create(name="Budget Ticket", default_price=23)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def taxrule(event):
|
||||||
|
return event.tax_rules.create(rate=Decimal("19.00"), code="S/standard")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def order(event, item, device, taxrule):
|
||||||
|
with freezegun.freeze_time("2017-12-01T10:00:00"):
|
||||||
|
o = Order.objects.create(
|
||||||
|
code="FOO",
|
||||||
|
event=event,
|
||||||
|
email="dummy@dummy.test",
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
secret="k24fiuwvu8kxz3y1",
|
||||||
|
datetime=datetime.datetime(
|
||||||
|
2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc
|
||||||
|
),
|
||||||
|
expires=datetime.datetime(
|
||||||
|
2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc
|
||||||
|
),
|
||||||
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
|
total=23,
|
||||||
|
locale="en",
|
||||||
|
)
|
||||||
|
o.fees.create(
|
||||||
|
fee_type=OrderFee.FEE_TYPE_PAYMENT,
|
||||||
|
value=Decimal("0.25"),
|
||||||
|
tax_rate=Decimal("19.00"),
|
||||||
|
tax_value=Decimal("0.05"),
|
||||||
|
tax_rule=taxrule,
|
||||||
|
tax_code=taxrule.code,
|
||||||
|
)
|
||||||
|
OrderPosition.objects.create(
|
||||||
|
order=o,
|
||||||
|
item=item,
|
||||||
|
variation=None,
|
||||||
|
price=Decimal("23"),
|
||||||
|
attendee_name_parts={"full_name": "Peter", "_scheme": "full"},
|
||||||
|
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||||
|
pseudonymization_id="ABCDEFGHKL",
|
||||||
|
positionid=1,
|
||||||
|
)
|
||||||
|
o.create_transactions()
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
TEST_TRANSACTION_RES_OP = {
|
||||||
|
"count": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
|
"fee_type": None,
|
||||||
|
"internal_type": None,
|
||||||
|
"item": None,
|
||||||
|
"order": "FOO",
|
||||||
|
"positionid": 1,
|
||||||
|
"price": "23.00",
|
||||||
|
"subevent": None,
|
||||||
|
"tax_code": None,
|
||||||
|
"tax_rate": "0.00",
|
||||||
|
"tax_rule": None,
|
||||||
|
"tax_value": "0.00",
|
||||||
|
"variation": None,
|
||||||
|
}
|
||||||
|
TEST_TRANSACTION_RES_FEE = {
|
||||||
|
"count": 1,
|
||||||
|
"created": "2017-12-01T10:00:00Z",
|
||||||
|
"datetime": "2017-12-01T10:00:00Z",
|
||||||
|
"fee_type": "payment",
|
||||||
|
"internal_type": "",
|
||||||
|
"item": None,
|
||||||
|
"order": "FOO",
|
||||||
|
"positionid": None,
|
||||||
|
"price": "0.25",
|
||||||
|
"subevent": None,
|
||||||
|
"tax_code": "S/standard",
|
||||||
|
"tax_rate": "19.00",
|
||||||
|
"tax_rule": 1,
|
||||||
|
"tax_value": "0.05",
|
||||||
|
"variation": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_transaction_list(token_client, organizer, event, order, item, taxrule):
|
||||||
|
res_op = copy.deepcopy(TEST_TRANSACTION_RES_OP)
|
||||||
|
res_fee = copy.deepcopy(TEST_TRANSACTION_RES_FEE)
|
||||||
|
with scopes_disabled():
|
||||||
|
res_fee["id"] = order.transactions.get(fee_type="payment").pk
|
||||||
|
res_fee["tax_rule"] = taxrule.pk
|
||||||
|
res_op["id"] = order.transactions.get(item__isnull=False).pk
|
||||||
|
res_op["item"] = item.pk
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/".format(
|
||||||
|
organizer.slug,
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert res_op in resp.data["results"]
|
||||||
|
assert res_fee in resp.data["results"]
|
||||||
|
assert resp.data["count"] == 2
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?order=FOO".format(
|
||||||
|
organizer.slug,
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 2
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?order=BAR".format(
|
||||||
|
organizer.slug,
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 0
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?datetime_since=2017-12-01T09:00:00Z".format(
|
||||||
|
organizer.slug,
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 2
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?datetime_since=2017-12-02T09:00:00Z".format(
|
||||||
|
organizer.slug,
|
||||||
|
event.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 0
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?item={}".format(
|
||||||
|
organizer.slug, event.slug, item.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 1
|
||||||
|
assert res_op in resp.data["results"]
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/?fee_type={}".format(
|
||||||
|
organizer.slug, event.slug, "payment"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.data["count"] == 1
|
||||||
|
assert res_fee in resp.data["results"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_detail(token_client, organizer, event, order, item, taxrule):
|
||||||
|
res_fee = copy.deepcopy(TEST_TRANSACTION_RES_FEE)
|
||||||
|
with scopes_disabled():
|
||||||
|
tx = order.transactions.get(fee_type="payment")
|
||||||
|
res_fee["id"] = tx.pk
|
||||||
|
res_fee["tax_rule"] = taxrule.pk
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/events/{}/transactions/{}/".format(
|
||||||
|
organizer.slug, event.slug, tx.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert json.loads(json.dumps(res_fee)) == json.loads(json.dumps(resp.data))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_organizer_list(token_client, team, organizer, event, order, item, taxrule):
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/transactions/".format(
|
||||||
|
organizer.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data["count"] == 2
|
||||||
|
assert "event" in resp.data["results"][0]
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/transactions/?event=dummy".format(
|
||||||
|
organizer.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data["count"] == 2
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/transactions/?event=test".format(
|
||||||
|
organizer.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data["count"] == 0
|
||||||
|
|
||||||
|
team.all_events = False
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/transactions/".format(
|
||||||
|
organizer.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data["count"] == 0
|
||||||
|
|
||||||
|
team.all_events = True
|
||||||
|
team.can_view_orders = False
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
resp = token_client.get(
|
||||||
|
"/api/v1/organizers/{}/transactions/".format(
|
||||||
|
organizer.slug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data["count"] == 0
|
||||||
Reference in New Issue
Block a user