Payments Domain - Views Documentation
Source: api/nextango/apps/payments/views.py, api/nextango/apps/payments/webhooks.py
Last Updated: 2026-07-08
Overview
Four ModelViewSets (PaymentProcessorViewSet, PaymentMethodViewSet, PaymentViewSet, RefundViewSet) plus terminal and webhook APIViews (TerminalConnectionTokenView, TerminalCreatePaymentView, StripeConfirmPaymentView, UnifiedWebhookView, StripeWebhookReceiver). All processor communication routes through the gateway abstraction (get_gateway_for_processor()), no view references a specific processor SDK directly except the legacy Stripe webhook receiver.
Root mount
Payments is mounted twice. api/nextango/apps/payments/urls.py is included at path('payments/', include(...)) in nextango/urls.py:149, giving /payments/processors/, /payments/methods/, /payments/payments/, /payments/refunds/, plus the non-router paths below. The root router in nextango/urls.py:41-45 separately registers payment-processors, payment-methods, payments, refunds using the same view classes.
The path('payments/', ...) include is checked before the root router include, so a request under /payments/... first tries the app's own url patterns. For the root router's payments registration (basename='payment') the outcome splits by route: the list/create route at /payments/ is shadowed, because the app router's own API root matches that path first, so listing and creating payments must go through /payments/payments/. The detail route at /payments/<pk>/ is NOT shadowed: no app-router pattern matches a bare pk segment under the prefix, Django falls through to the root router, and payment detail is reachable at both /payments/<pk>/ and /payments/payments/<pk>/. The root router's payment-processors, payment-methods, and refunds registrations do not collide at all, they are live hyphenated alternatives to /payments/processors/, /payments/methods/, /payments/refunds/.
| Purpose | Live route(s) |
|---|---|
| Processors | /payment-processors/, /payments/processors/ |
| Payment methods | /payment-methods/, /payments/methods/ |
| Payments (list/create) | /payments/payments/ only, the flat /payments/ list route is shadowed by the app mount |
| Payments (detail) | /payments/<pk>/ (root-router fall-through) and /payments/payments/<pk>/ |
| Refunds | /refunds/, /payments/refunds/ |
| Terminal, webhook, confirm | /payments/terminal/..., /payments/webhook/..., /payments/confirm/, app-only |
None of these carry an /api/ prefix.
PaymentProcessorViewSet
Queryset: PaymentProcessor.objects.all()
Permissions: [IsAuthenticated, IsEmployeeOrManager], a dashboard-admin surface.
| Method | URL | Action |
|---|---|---|
| GET | /payment-processors/ | list |
| GET | /payment-processors/{id}/ | retrieve |
| POST | /payment-processors/ | create |
| PUT/PATCH | /payment-processors/{id}/ | update / partial_update |
| DELETE | /payment-processors/{id}/ | destroy |
| GET | /payment-processors/{id}/payment_methods/ | active PaymentMethods for this processor |
| POST | /payment-processors/{id}/test_connection/ | validate credentials via gateway.test_connection() |
test_connection calls the real gateway implementation, not a stub. It returns 501 if the gateway has no registered implementation (GatewayConnectionError or NotImplementedError), 502 on an unexpected error, and the gateway's own {success, message} shape otherwise.
Filters: processor_type (exact), is_active (exact), is_test_mode (exact), name (icontains), search across name/processor_type, ordering by name/processor_type/created_at.
PaymentMethodViewSet
Queryset: PaymentMethod.objects.select_related('processor').prefetch_related('available_at_stores__store')
Permissions: empty permission_classes, throttling disabled. POS terminals call this unauthenticated and at high frequency; the empty permission list inherits DEFAULT_PERMISSION_CLASSES at the DRF settings level, not a hardcoded bypass.
| Method | URL | Action |
|---|---|---|
| GET | /payment-methods/ | list |
| GET | /payment-methods/{id}/ | retrieve |
| POST | /payment-methods/ | create |
| PUT/PATCH | /payment-methods/{id}/ | update / partial_update |
| DELETE | /payment-methods/{id}/ | destroy |
| GET | /payment-methods/{id}/available_stores/ | stores where this method is available |
| GET | /payment-methods/{id}/payments_summary/ | aggregate payment stats for this method |
| GET | /payment-methods/by_store/?store_id=<uuid> | methods available at a store, POS startup endpoint |
Business rule: an empty available_at_stores means the method is available at every store; a non-empty list restricts it to those stores.
by_store strips processor.configuration from the response (may carry non-sensitive but still internal settings) and returns only processor metadata needed for POS fee display: name, type, fees, currency.
Filters: method_type, is_active, is_online_available, is_pos_available, processor (exact); name, processor_name (icontains); search; ordering.
PaymentViewSet
Queryset: TransactionPayment.objects.select_related('transaction', 'payment_method', 'cash_drawer_event').prefetch_related('refunds')
Permissions: [IsAuthenticated] for the ViewSet; process_refund narrows further to employee/manager.
Idempotency: includes IdempotencyMixin, IDEMPOTENT_ACTIONS = ['create', 'process_refund']. See the idempotency mixin.
| Method | URL | Action |
|---|---|---|
| GET | /payments/payments/ | list |
| GET | /payments/payments/{id}/ | retrieve |
| POST | /payments/payments/ | create, idempotent |
| PUT/PATCH | /payments/payments/{id}/ | update / partial_update |
| DELETE | /payments/payments/{id}/ | destroy |
| POST | /payments/payments/{id}/process_refund/ | create and process a refund, idempotent |
| GET | /payments/payments/by_transaction/?transaction_id=<uuid> | all payments for a transaction |
| GET | /payments/payments/summary/ | aggregate totals, fees, status/method breakdown, respects active filters |
Filters: status, payment_method, transaction (exact); transaction_id, payment_method_name (icontains); amount_min/amount_max; date_from/date_to; search; ordering.
RefundViewSet
Queryset: Refund.objects.select_related('payment__transaction', 'payment__payment_method')
Idempotency: includes IdempotencyMixin.
| Method | URL | Action |
|---|---|---|
| GET | /payments/refunds/ | list |
| GET | /payments/refunds/{id}/ | retrieve |
| POST | /payments/refunds/ | create |
| PUT/PATCH | /payments/refunds/{id}/ | update / partial_update |
| DELETE | /payments/refunds/{id}/ | destroy |
| POST | /payments/refunds/{id}/process/ | process a pending refund |
| GET | /payments/refunds/by_payment/?payment_id=<uuid> | all refunds for a payment |
| GET | /payments/refunds/summary/ | aggregate refund totals, respects active filters |
Filters: status, payment (exact); payment_id; transaction_id (icontains, double join through payment.transaction); amount_min/amount_max; date_from/date_to; search; ordering.
Terminal and confirmation endpoints
These three views are processor-agnostic, they delegate to whichever gateway is registered for the requested processor_type.
TerminalConnectionTokenView
POST /payments/terminal/connection-token/
Request: {"processor_type": "stripe", "location_id": "<optional>"}
Response: {"secret": "<sdk_token>", "processor_type": "stripe"}
Errors: 400 no active processor for the requested type; 500 gateway call failed; 501 gateway registered but not implemented.
TerminalCreatePaymentView
POST /payments/terminal/create-payment/, includes IdempotencyMixin.
Request: {"processor_type": "stripe", "amount": 1999, "currency": "usd", "metadata": {}}
Response: {"id": "<session_id>", "client_secret": "<secret>", "amount": 1999, "currency": "usd", "status": "requires_payment_method"}
StripeConfirmPaymentView
POST /payments/confirm/, permission_classes = [AllowAny], includes IdempotencyMixin (IDEMPOTENT_ACTIONS = ['post']).
Post-3DS-challenge verification. After the frontend resolves stripe.handleNextAction(), it calls this endpoint. The backend re-fetches the PaymentIntent from Stripe rather than trusting the client's assertion.
Request: {"payment_intent_id": "pi_..."}
On success (intent.status == 'succeeded'): sets the payment's gateway charge status to confirmed, advances the SaleTransaction to COMPLETED, and commits reserved inventory.
On failure (any other status): marks the payment failed, voids the transaction directly (not through TransactionService.void_transaction(), which rejects the PAYMENT_REQUIRES_ACTION status), and releases reserved inventory.
Authorization: authorize_before_idempotency() resolves the caller's web session and verifies, entirely from local DB state, that exactly one TransactionPayment matches payment_intent_id, exactly one CheckoutSession references its transaction, and that checkout's web_session matches the resolved caller. Any cardinality mismatch fails closed with a generic 403. The payment_intent.succeeded webhook is the backstop for cases where the browser closes mid-challenge.
Response: 200 {"confirmed": true, "transaction_id": "<uuid>"}, 400 on payment failure or missing payment_intent_id, 404 if no matching payment record, 502 on a Stripe API error.
Webhook receivers
UnifiedWebhookView
Source: api/nextango/apps/payments/webhooks.py
URL: POST /payments/webhook/<processor_type>/
Single receiver for all processors. Looks up the active PaymentProcessor for processor_type, calls gateway.verify_webhook(raw_body, signature_header), and routes the verified event. processor_type is one of stripe, square, paypal, custom.
StripeWebhookReceiver
URL: POST /payments/webhook/stripe/, GET for endpoint-accessibility checks (Stripe dashboard setup).
Stripe-specific webhook path, HMAC-SHA256 signature verification with a 5-minute replay-attack tolerance window, idempotent via the WebhookEvent model. Payments are already succeeded by the time this webhook arrives (POS confirms with Stripe Terminal directly), the webhook is asynchronous validation and reconciliation, not payment processing. See Webhooks for the full signature-verification and idempotency detail.
Related Documentation
- Models - Payment data models and relationships
- Serializers - API serialization and validation
- Services - Business logic layer, the gateway registry
- Webhooks - Payment processor webhook handlers
- Transactions Views - Related transaction endpoints