Skip to content

Payment Gateway & Transaction System

Overview

The payment system handles subscriber invoice payments, transaction lifecycle management, webhook event processing, and reconciliation. It is built around a single payment gateway integration (Zwitch) with a mock mode for development and testing.

API versions: v2 (web app) and v3 (mobile app) Currency: INR only Gateway mode: Configurable — mock (dev/test) or zwitch (production)


Data Model

CycleInvoice (prisma/schema.prisma:1940)

Core invoice tied to a ProgramCycle and EnrolledSubscriber.

Field Type Notes
status InvoiceStatus DRAFT \| OPEN \| PAID \| FAILED \| REFUNDED \| CANCELLED \| PARTIALLY_PAID \| OVERDUE
invoiceNumber String? Unique, e.g. INV-202501-00001-A3F2
subTotalAmount Decimal? Amount before tax/discount/fees
totalAmount Decimal? Final: subTotal + tax - discount + delayFine
paidAmount Decimal? Amount actually paid
parentInvoiceId String? Supports reissuing invoices
Relations transactions[], collectionCharges[]

CycleTransaction (prisma/schema.prisma:2019)

Main transaction table — current state only.

Field Type Notes
idempotencyKey String Unique — prevents duplicate transactions
type TransactionType PAYMENT \| REFUND \| CHARGEBACK \| REVERSAL
status TransactionStatus PENDING \| PROCESSING \| COMPLETED \| FAILED \| CANCELLED \| REFUNDED
grossAmount Decimal Amount the user pays
gatewayFee Decimal Gateway fee (default 1% of gross)
gatewayTaxAmount Decimal 18% GST on gateway fee
platformFee Decimal Platform fee (default 0.32% of gross)
platformTaxAmount Decimal Platform tax (currently 0)
netAmount Decimal gross - gatewayFee - gatewayTax - platformFee - platformTax
paymentProvider String zwitch
gatewayOrderId String? Payment token from Zwitch
gatewayTxnId String? Transaction ID from gateway
attemptCount / maxAttempts Int Retry tracking (max 3)
settlementId String? Links to Settlement
Relations logs[], refunds[], webhookEvents[], settlement

CycleTransactionLog (prisma/schema.prisma:2131)

Immutable audit trail — every event is recorded here.

Field Notes
event TOKEN_CREATED \| GATEWAY_INITIATED \| GATEWAY_REDIRECT \| CUSTOMER_AUTHENTICATED \| PAYMENT_AUTHORIZED \| PAYMENT_CAPTURED \| PAYMENT_FAILED \| ...
statusBefore / statusAfter State transition snapshot
gatewayPayloadRequestId / gatewayPayloadResponseId Links to GatewayPayload
gatewayResponse Minimal gateway data at event time
triggeredBy SUBSCRIBER \| SYSTEM \| ADMIN \| WEBHOOK

GatewayPayload (prisma/schema.prisma:2196)

Stores full request/response/webhook payloads separately (large data).

  • payloadType: REQUEST \| RESPONSE \| WEBHOOK \| ERROR_RESPONSE
  • payload: Raw JSON
  • headers: HTTP headers

CycleRefund (prisma/schema.prisma:2253)

Refund tracking on transactions.

  • refundType: FULL \| PARTIAL
  • status: PENDING \| APPROVED \| PROCESSING \| COMPLETED \| FAILED \| REJECTED \| CANCELLED
  • reasonCategory: CUSTOMER_REQUEST \| DUPLICATE_CHARGE \| FRAUDULENT \| SERVICE_NOT_DELIVERED \| ...
  • Links to originalTransaction and settlement

WebhookEvent (prisma/schema.prisma:2320)

Tracks incoming webhooks.

  • Deduplication via payloadHash and (eventId, provider, attemptNumber) unique constraint
  • Status: RECEIVED \| VERIFIED \| PROCESSING \| PROCESSED \| FAILED \| IGNORED
  • Retry: retryCount, maxRetries (3), nextRetryAt

Settlement (prisma/schema.prisma:2381)

Batch settlements from payment gateway.

  • Tracks totalGrossAmount, totalFees, totalRefunds, netSettlement
  • Status: PENDING \| PROCESSING \| COMPLETED \| FAILED \| PARTIALLY_SETTLED
  • Reconciliation: reconciledAt, reconciledBy, discrepancy

CollectionCharge (prisma/schema.prisma:3424)

Collection charges collected from companies. Links Company, CycleInvoice, CycleTransaction.

CompanyInvoice (prisma/schema.prisma:3453)

Billing invoices for companies (separate from CycleInvoice — these are for service charges).


Architecture & Flow

Payment Initiation

Route (v2/v3)
  POST .../invoices/:invoiceId/payments/initiate
  ├─ PaymentsCommand.initiate()
  │    ├─ Validate subscriber (not APPLE_APP_REVIEW)
  │    ├─ Get invoice details
  │    │    ├─ Check invoice status (not PAID/CANCELLED/REFUNDED)
  │    │    ├─ Check cycle status (ACTIVE)
  │    │    ├─ Check subscriber profile (ACTIVE, email verified, has email/phone)
  │    │    └─ Check sub-account configured on program
  │    ├─ Validate paying amount equals installment amount
  │    ├─ Calculate payment splits via calculatePaymentSplits()
  │    ├─ Upsert CycleTransaction (with idempotencyKey + unique constraint)
  │    ├─ v2: Call pgPaymentService.createPaymentToken() → Zwitch payment token API
  │    ├─ v3: Call pgPaymentService.createUpiIntent() → Zwitch UPI intent API
  │    │    └─ Within unitOfWork:
  │    │         ├─ Store request/response in GatewayPayload
  │    │         ├─ Create CycleTransactionLog
  │    │         └─ Update CycleTransaction (gatewayOrderId, status)
  │    ├─ Enqueue deferred reconciliation job (RECONCILE_PAYMENT, 2 min delay)
  │    └─ Return paymentToken + accessKey + checkoutMode (+ UPI intent links in v3)
  └─ src: application/use-cases/cycle-invoices/commands/payments/payments-command.ts

Key files:

  • Route: src/interfaces/http/routes/v2/subscribers/invoices/[invoiceId]/payments/payments-routes.ts
  • Route (v3): src/interfaces/http/routes/v3/subscribers/invoices/[invoiceId]/payments/payments-routes.ts
  • Command: src/application/use-cases/cycle-invoices/commands/payments/payments-command.ts
  • Helpers: src/application/use-cases/cycle-invoices/commands/payments/payments-command-helpers.ts
  • Zwitch service: src/infrastructure/pg/zwitch/services/payment-service.ts
  • Mock service: src/infrastructure/pg/mock/services/payment-service.ts
  • Fee calculation: src/domain/helpers/cycle-transaction-helpers.ts
  • Constants: src/shared/constants/Pg.ts

Payment Status Check

Client GET .../payments/:paymentToken/status
  ├─ PaymentsCommand.statusCheck()
  │    ├─ Find transaction by paymentToken + subscriberId
  │    ├─ If PENDING/PROCESSING → call reconcilePayment() (gateway status check)
  │    └─ Return status + invoice details
  └─ src: application/use-cases/cycle-invoices/commands/payments/payments-command.ts:398

Webhook Processing

Zwitch → POST /webhooks/pg/zwitch
  ├─ [Middleware] zwitchIpWhitelistMiddleware → IP whitelist check
  │    └─ src: interfaces/http/middlewares/zwitch-ip.middleware.ts
  ├─ [Middleware] zwitchSignatureVerifyMiddleware → HMAC-SHA256
  │    └─ src: interfaces/http/middlewares/zwitch-signature.middleware.ts
  ├─ Route handler → Validates body (discriminated union)
  │    └─ src: interfaces/http/webhooks/pg/zwitch/zwitch-routes.ts
  ├─ processPGWebhookEvent() → within unitOfWork:
  │    ├─ Dedup: check (eventId, provider, attemptNumber) unique constraint
  │    ├─ Store webhook payload in GatewayPayload (type: WEBHOOK)
  │    ├─ Determine event type: PAYMENT or TOKEN
  │    ├─ Extract transactionId, invoiceId, subscriberId, paidAt
  │    ├─ Validate transaction (check terminal statuses)
  │    │    └─ Token events can recover from FAILED/CANCELLED
  │    │    └─ Payment events CANNOT recover from FAILED/CANCELLED
  │    ├─ Create CycleTransactionLog
  │    └─ Update CycleTransaction (status, gatewayStatus, paymentMethod, metadata)
  ├─ [Post-commit] Enqueue SAVE_WEBHOOK_EVENT job
  └─ [Post-commit] If payment successful → enqueue MARK_INVOICE_AS_PAID job
       └─ src: application/use-cases/webhook-events/commands/process-pg-webhook-event/

Key files:

  • Webhook routes: src/interfaces/http/webhooks/pg/zwitch/zwitch-routes.ts
  • Validators: src/interfaces/http/webhooks/pg/zwitch/zwitch-validators.ts
  • Command: src/application/use-cases/webhook-events/commands/process-pg-webhook-event/process-pg-webhook-event-command.ts
  • Helpers: src/application/use-cases/webhook-events/commands/process-pg-webhook-event/process-pg-webhook-event-command-helpers.ts

Reconciliation

[Deferred Job] RECONCILE_PAYMENT (2 min delay, exponential backoff, max 5 attempts)
  ├─ reconcilePayment() → PaymentService
  │    ├─ Check if already terminal → return
  │    ├─ Call pgPaymentService.checkPaymentStatus() → Zwitch GET status API
  │    ├─ Within unitOfWork:
  │    │    ├─ Store response in GatewayPayload
  │    │    ├─ Create CycleTransactionLog
  │    │    └─ Update CycleTransaction
  │    ├─ If COMPLETED → mark invoice as paid
  │    └─ Return terminal status
  ├─ If non-terminal + attempts remain → throw RetryableJobError
  └─ If max attempts → failStuckTransaction()
       └─ src: application/services/payment/payment.service.ts

[Recurring Job] RECONCILE_STUCK_PAYMENTS (every 15 min)
  ├─ reconcileStuckPayments() → PaymentService
  │    └─ Find transactions older than 30 min with PENDING/PROCESSING status
  │         └─ For each: attempt gateway reconciliation
  │         └─ If no gatewayOrderId or reconciliation fails → mark as FAILED
  └─ src: application/services/payment/payment.service.ts:197

Key files:

  • Application service: src/application/services/payment/payment.service.ts
  • Queue service: src/application/queue/services/payment-queue.service.ts
  • Queue processor: src/application/queue/processors/payment.processor.ts
  • Repository: src/infrastructure/db/postgres/repositories/cycle-transactions-repository.ts

Fee Structure

Fees are computed in src/domain/helpers/cycle-transaction-helpers.ts using constants from src/shared/constants/Pg.ts:

Component Rate Applies To
Gateway fee 1% Gross amount
Gateway tax (GST) 18% Gateway fee
Platform fee 0.32% Gross amount
Platform tax 0% Platform fee (not applied yet)

Net amount = grossAmount - gatewayFee - gatewayTax - platformFee - platformTax


Queue Jobs

BullMQ queue: payment

Job Name Trigger Delay Max Retries Priority
SAVE_WEBHOOK_EVENT After webhook processed none 5 1
MARK_INVOICE_AS_PAID After successful payment webhook none 3 1
RECONCILE_PAYMENT After payment token created 2 min (exponential backoff) 5 1
RECONCILE_STUCK_PAYMENTS Recurring (every 15 min) - - 2

Key files:

  • Queue service: src/application/queue/services/payment-queue.service.ts
  • Processor: src/application/queue/processors/payment.processor.ts

Gateway: Zwitch

The system integrates with Zwitch, an Indian payment aggregator.

Config interface (src/application/interfaces/config/index.ts:29):

PgBasicConfig {
  apiKey: string;              // For non-payment APIs (IFSC)
  apiSecret: string;
  pgApiKey: string;            // For payment + sub-account APIs
  pgApiSecret: string;
  webhookSecret: string;       // Webhook signing
  paymentServiceUrl: string;
  upiIntentUrl: string;
  subAccountServiceUrl: string;
  ifscServiceUrl: string;
  webhookAllowedIps: Set<string>;
}

Mode: mock or zwitch (configured via pg.mode, resolved in di.ts:149)

Gateway operations:

  • createPaymentToken() — POST to Zwitch to get a payment token (SDK/MOCK checkout mode)
  • createUpiIntent() — POST to Zwitch to get UPI QR/intent links; the response data.transaction_id is stored as the payment token
  • checkPaymentStatus() — GET to Zwitch status URL by token

Sub-account management:

  • Programs have a subAccountId for splitting settlements
  • Bank accounts have subAccountId, subAccountStatus (CREATED → PENDING_APPROVAL → ACTIVATED → REJECTED → SUSPENDED → CLOSED)
  • pgSubAccountService registered in DI (di.ts:152)

Refunds

The CycleRefund model supports the full refund lifecycle (prisma/schema.prisma:2253):

  • Types: FULL, PARTIAL
  • Status: PENDING → APPROVED → PROCESSING → COMPLETED | FAILED | REJECTED | CANCELLED
  • Reason categories: CUSTOMER_REQUEST | DUPLICATE_CHARGE | FRAUDULENT | SERVICE_NOT_DELIVERED | QUALITY_ISSUE | ORDER_CANCELLED | SYSTEM_ERROR | OTHER

Status: The DB schema and model are fully defined, but refund initiation (approve/process) is not yet implemented in the application layer. This is future work.


Settlements

Batch settlements are tracked via the Settlement model (prisma/schema.prisma:2381):

  • Links to BankAccount for payout destination
  • Auto-reconciliation is not yet implemented (manual reconciledAt / reconciledBy fields exist)

DI Registration

Payment services registered in src/infrastructure/di.ts:

Token Scope Source
pgConfig value Zwitch config
pgPaymentService singleton Zwitch or Mock based on mode
pgSubAccountService singleton Zwitch sub-account service
paymentProcessor singleton BullMQ payment processor
paymentQueueService singleton BullMQ queue
paymentService singleton Application payment service
webhookEventsRepository scoped PostgreSQL
gatewayPayloadRepository scoped PostgreSQL
cycleTransactionsRepository scoped PostgreSQL
cycleTransactionLogsRepository scoped PostgreSQL
cycleInvoicesRepository scoped PostgreSQL

Security

  • Webhook IP whitelist: Zwitch webhook endpoints require IPs in webhookAllowedIps
  • HMAC-SHA256 signature: x-zwitch-signature (or x-webhook-signature) verified with pgApiSecret
  • Payload hash: SHA-256 of raw body stored in WebhookEvent.payloadHash for deduplication
  • Idempotency: CycleTransaction.idempotencyKey is unique — prevents duplicate payment token creation across retries

Tests

  • Unit tests: src/application/services/payment/payment.service.test.ts
  • Unit tests: src/application/queue/services/payment-queue.service.test.ts
  • Unit tests: src/application/use-cases/cycle-invoices/commands/payments/payments-command.test.ts
  • Functional tests: src/interfaces/http/routes/v3/subscribers/invoices/[invoiceId]/payments/__tests__/payments-routes.test.ts

Key File Reference

Layer File Purpose
Domain src/domain/entities/paymentStatement.ts Payment statement entity
Domain src/domain/helpers/cycle-transaction-helpers.ts Fee split calculation
Application src/application/use-cases/cycle-invoices/commands/payments/payments-command.ts Payment orchestration
Application src/application/use-cases/webhook-events/commands/process-pg-webhook-event/process-pg-webhook-event-command.ts Webhook processing
Application src/application/services/payment/payment.service.ts Reconciliation, mark invoice paid
Application src/application/queue/services/payment-queue.service.ts BullMQ queue jobs
Application src/application/queue/processors/payment.processor.ts Job processing
Infrastructure src/infrastructure/pg/zwitch/services/payment-service.ts Zwitch API client
Infrastructure src/infrastructure/pg/mock/services/payment-service.ts Mock gateway
Infrastructure src/infrastructure/pg/zwitch/config/zwitch-config.ts Zwitch config
Infrastructure src/infrastructure/db/postgres/repositories/cycle-transactions-repository.ts Transaction DB
Infrastructure src/infrastructure/di.ts DI wiring
Interfaces src/interfaces/http/routes/v2/subscribers/invoices/[invoiceId]/payments/payments-routes.ts v2 payment routes
Interfaces src/interfaces/http/routes/v3/subscribers/invoices/[invoiceId]/payments/payments-routes.ts v3 payment routes
Interfaces src/interfaces/http/webhooks/pg/zwitch/zwitch-routes.ts Webhook handler
Interfaces src/interfaces/http/middlewares/zwitch-ip.middleware.ts IP whitelist
Interfaces src/interfaces/http/middlewares/zwitch-signature.middleware.ts HMAC verification
Shared src/shared/constants/Pg.ts Fee percentages, attempt limits
Shared src/shared/types/pg.types.ts PG type definitions
Config src/application/interfaces/config/index.ts App config interfaces