Payments Domain - Signals Documentation

Source: api/nextango/apps/payments/signals.py Last Updated: 2026-07-08

Overview

Six signal handlers across TransactionPayment and Refund. Beyond audit logging, two of the six carry real behavior: a pre-save integrity guard on cumulative refund amounts, and a post-save enqueue that reconciles late-arriving processor webhooks against the newly created payment.

Handlers

enforce_refund_cumulative_integrity

Signal: pre_save, sender Refund, dispatch_uid='refund_cumulative_integrity_check'

Runs on every Refund.save(), including paths that skip ModelForm.full_clean() (admin direct saves, scripts). Validates that the cumulative sum of pending and succeeded refunds against the parent payment does not exceed the payment amount, via Refund.validate_cumulative_refund_amount(), and raises ValidationError if it would. Refunds in other statuses are skipped, they cannot contribute to outstanding refund liability. This is a bypass-resistant layer on top of form-level validation, it does not catch bulk_create or raw SQL, those are covered by a database-level trigger and CHECK constraint documented separately.

payment_post_save

Signal: post_save, sender TransactionPayment

Logs payment creation: "New payment created: {id} for ${amount}". No further processing.

enqueue_awaiting_webhook_reconciliation

Signal: post_save, sender TransactionPayment, dispatch_uid='reconcile_awaiting_webhooks_on_payment_create'

Fires only on creation of a payment that has a processor_transaction_id. Schedules reconcile_awaiting_webhooks_for_payment.delay(payment_id) via transaction.on_commit, so the Celery task only runs once the payment row is durable. This drains any WebhookEvent rows that arrived before the payment record existed (the processor webhook can beat the POS's payment-creation request in a race). The enqueue itself is synchronous and cheap, the actual row lookup, locking, and processing happen in the worker, keeping this off the user-facing transaction path.

refund_post_save

Signal: post_save, sender Refund

Logs refund creation. On a status transition to succeeded (not creation), also logs the success and, unless the sync context indicates the save originated from an inbound webhook (should_skip_sync_to_sanity()), schedules async_transaction_sync_task.delay(model_name='SaleTransaction', instance_id=..., sync_type='refund_update') via transaction.on_commit, so the parent SaleTransaction re-syncs to Sanity reflecting the refund. This is real Sanity integration, not a documentation gap, the sync happens at the parent transaction level rather than on TransactionPayment/Refund themselves.

payment_post_delete / refund_post_delete

Signal: post_delete, senders TransactionPayment / Refund

Log deletion events only. Deleting payment or refund records should be rare, financial systems typically mark records as voided or cancelled rather than deleting them.

Sanity sync model

TransactionPayment and Refund extend SanityIntegratedModel but neither syncs to Sanity directly, there is no sync_payment_to_sanity or sync_refund_to_sanity handler in this file. The one Sanity-facing action in this module is the refund-triggered re-sync of the parent SaleTransaction, described above. Payments and refunds themselves are Django-owned operational records (see the authoring-surface rule: they are born from POS transactions, not authored in Sanity Studio).

Was this page helpful?