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/.

PurposeLive 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.

MethodURLAction
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.

MethodURLAction
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.

MethodURLAction
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.

MethodURLAction
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.


Was this page helpful?