PayU payment provider¶
Status: integration candidate. Razorpay remains the default and rollback provider.
Safety boundary¶
PayU uses Hosted Checkout. VayzoMart never collects card data and never sends the merchant salt to
a browser or mobile client. The API creates the transaction ID and request hash. A browser return
is not proof of payment: the API validates PayU's reverse hash and calls verify_payment before the
shared checkout finalizer can create an order or mark it paid.
Both providers converge on the existing atomic inventory-reservation finalizer. Its database
conditional update elects exactly one winner when a browser return, webhook, or duplicate delivery
races another request. PaymentTransaction.Provider identifies the provider while settlement,
invoice, tax, cancellation, and internal refund accounting remain provider-neutral.
Configuration¶
Razorpay is the safe default:
Staging PayU activation requires all of the following runtime secrets/settings:
PaymentGateway__Provider=PayU
PayU__Enabled=true
PayU__Environment=Test
PayU__MerchantKey=<PayU TEST merchant key>
PayU__MerchantSalt=<PayU TEST salt>
PayU__WebhookEnabled=true
PayU__RefundExecutionEnabled=false
PayU__ReconciliationEnabled=false
PayU__RefundCallbackUrl=https://<api-host>/api/webhooks/payu/refund
PayU__FrontendReturnUrl=https://<frontend-host>/checkout/payment-callback
Staging startup rejects non-TEST PayU endpoints. Production startup rejects TEST endpoints and
requires PayU:Environment=Live. Callback URLs must be real HTTPS URLs, and
PayU:FrontendReturnUrl must point at /checkout/payment-callback (the page that asks
/api/payment/status for the opaque txnid); startup rejects any other path. Razorpay credentials stay
configured while PayU is selected so rollback does not require rebuilding the service.
Never commit merchant keys, salts, Razorpay secrets, webhook secrets, or database credentials.
Webhooks and reconciliation¶
POST /api/webhooks/payu is anonymous to application JWT authentication but accepts traffic only
when PayU and its webhook switch are enabled. It validates the provider response hash before
recording both an exact payload fingerprint and a provider-qualified semantic event key. Equivalent
deliveries with reordered or additional form fields return success without re-running business
logic. Logs include transaction identifiers and machine-readable outcomes but not raw payloads,
hashes, credentials, or salts.
Provider identity is persisted on the inventory reservation and checkout-idempotency record, so a
provider switch cannot replay or finalize another provider's same-looking identifier. PayU
verification outcomes are durable (Pending, Unknown, Failed, or Verified). Pending and
unknown reservations are excluded from ordinary expiry release and are re-verified by the payment
reconciliation worker after restarts. A verified capture that arrives after inventory was released
creates one durable, provider-qualified refund intent instead of silently losing the payment.
PayU refund requests use the internal refund transaction's deterministic merchant reference
(at most 23 characters; longer keys become a deterministic digest). Dispatch continues to use the
existing database claim/lease rules. A PayU refund is never re-sent once its outcome is ambiguous:
a timeout, HTTP 429/5xx, transport failure, unusable response, a "token already used / request
pending" answer, or error_code 102 without a request id moves the attempt straight to
UnknownOutcome (Razorpay keeps its transient retry). error_code 102 or a duplicate-token answer
that does carry a request id is recorded as Submitted. Only an explicit status=0 rejection is a
definitive failure. Submitted and UnknownOutcome PayU attempts are resolved only by
check_action_status (by request id when known, otherwise by PayU payment id plus merchant token);
the provider's own request_id is recorded, never a locally invented one.
A verify_payment response without an explicit per-transaction "Not Found" record (invalid hash,
wrong key, top-level error, unexpected shape) is a lookup failure, never "not initiated", so it
can never authorize releasing inventory. A browser return whose form fails key or hash validation
is sent back (303) to the callback page without any reference or state; the server-to-server
webhook keeps rejecting it outright.
Refund execution and reconciliation are separate kill switches and default to false. Enable refund
execution only after TEST refund and check_action_status certification; startup rejects refund
execution without reconciliation. Checkout and webhook activation do not implicitly activate money
movement.
Rollback¶
Set PaymentGateway__Provider=Razorpay and restart through the normal controlled deployment
process. Do not delete PayU payment, webhook, or refund records. Transactions already initiated on
PayU must continue to accept valid callbacks/webhooks and must be reconciled with PayU even after
new checkout creation returns to Razorpay.
No production activation is authorized by this document or by the implementation tranche.