Payments
One provider order per Order, how confirmation really happens, and the double-entry record it leaves
A payment is a short, strict sequence: one Ulsav Order, one provider order, one confirmation. The buyer's browser is never part of the decision, which is why a receipt cannot be faked by reloading a page.
Razorpay is the only payment provider wired up today. Ulsav never holds card details; the provider's own modal does.
Provider
Razorpay
The only live adapter
Provider orders
One per Ulsav Order
Retries reuse it
Merchant account
Chosen once
No fallback to another account
Payment window
15 minutes
Matches the seat hold
Confirms from
A signed callback
Or an explicit status re-check
Records
Ledger entries
Written in the same transaction
One Order, one provider order
When checkout starts, the Order is created and a single provider order is opened for its exact total. Every retry by the buyer reuses that same provider order, and the provider itself refuses a second successful charge on it.
The MerchantAccount — the credential set that will receive the money — is chosen once, at that moment. A method you have pinned wins; otherwise the account with the lowest priority number that supports the currency and method is used.
There is no walking down a list of accounts after a failure. If that account fails, the Order fails, and the buyer starts a fresh attempt. That keeps the settlement with one merchant instead of splitting it across two.
The provider is opened outside the database
Creating the provider order is a network call, so it happens after the database transaction that created the Order and its holds has committed. Only the resulting reference is written back.
If the provider refuses, Ulsav cancels the Order, unlinks the entries, releases the holds, and frees the coupon — in that order, so a buyer is never left holding seats for a checkout that cannot complete.
What confirms a payment
A confirmation arrives in one of two ways: a signed provider callback, or an explicit status re-check when a callback is late or lost. Both go through the same code path.
firstThe signature is verified
The raw body is authenticated before any business data is read. An unauthenticated body changes nothing and is answered with an error.
thenThe amount is matched
The captured amount, and the currency when the provider reports one, must equal the Order's stored total. A mismatch confirms nothing and opens a reconciliation case.
thenOne transaction settles everything
The Order is locked, marked confirmed, its entries confirmed, the coupon redemption confirmed, the holds consumed, guest links rotated, ledger entries written, and notifications queued.
after commitSlow work runs separately
Invoices issue, emails send, webhooks deliver, and analytics project. A failing email never blocks a sale.
Because it is one transaction, an Order is never half-confirmed: no state exists where the money is recorded but the entries are not.
The ledger
Every confirmed Order writes a balanced journal, debiting the money that arrived and crediting each party's share. Nothing is edited afterwards; a correction is a compensating entry.
For Order ORD-000123 at ₹3,894.12:
| Entry | Debit | Credit | Amount |
|---|---|---|---|
| Organizer payable | provider clearing | organizer payable | ₹3,717.00 |
| Organizer tax | provider clearing | organizer tax | ₹567.00 |
| Ulsav revenue | provider clearing | Ulsav revenue | ₹150.10 |
| Ulsav tax | provider clearing | Ulsav tax | ₹27.02 |
The four credits add to ₹3,894.12, which is what the debit side collects. Organizer payable is the remainder after the fee, the fee's tax, and the organizer's own tax, so the journal balances under either fee bearer.
When money arrives that should not
Two situations produce a captured payment with no sale: a payment that lands after the checkout expired or was cancelled, and a second successful payment against an Order that is already confirmed.
Neither is confirmed and neither is ignored. The capture is recorded, and an automatic refund of that exact payment is queued. The refund is unique per provider payment, so a repeated callback cannot refund twice.
A third case is quieter: the provider reports an amount that does not match the Order. Nothing is confirmed, and a reconciliation case opens for a person to resolve while the Order stays exactly as it was.
What the buyer experiences
The pay page shows the amount, the Event and Pass name, and a countdown. Reloading it reopens the same provider order rather than creating a new one.
If the 15 minutes run out, the Order expires and the buyer is asked to start again. A pending payment is never silently charged later: either the provider confirms inside the window, or nothing happens.
A payment that fails leaves the Order pending until expiry, which is deliberate. Provider failures are often retried by the buyer within seconds, and cancelling the Order on the first error would throw away seats unnecessarily.
Reconciliation and the worker
Three jobs keep payments honest:
- a reaper expires unpaid Orders about once a minute
- a reconciler polls refunds that are still processing
- an outbox drainer turns reconciliation events into durable cases and runs automatic refunds
An organizer does not have to trigger any of this. The pay page can also ask for a re-check on the spot, which is the fastest path when a callback is slow.
Failure paths
| What happened | Result |
|---|---|
| Provider refuses to open the order | Order cancelled, seats released, buyer can retry |
| Signature does not verify | Nothing changes; the callback is rejected |
| Amount differs from the Order | Reconciliation case; no confirmation |
| Payment succeeds after expiry | Automatic refund of the captured amount |
| Duplicate success on a confirmed Order | Automatic refund of the extra capture |
| The 15 minutes pass with no payment | Order expires, entries unlink |
Related
- Checkout — how the Order and provider order are created
- Refunds — money going back
- Invoices — what a confirmed Order issues
- Availability — what happens to the seats