--- title: Payment tags: [data-model, mongoose] aliases: [Payment Record, Escrow, IPayment] --- # Payment Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. Designed around the SHKeeper crypto payment gateway with explicit fields for blockchain network, transaction hash, escrow state, and provider invoice ids. The `provider` and `direction` discriminators let one collection hold all four flow types (incoming buyer payment, outgoing seller payout, refund, and "other" provider integrations). > [!note] Source > `backend/src/models/Payment.ts:3` — schema definition > `backend/src/models/Payment.ts:257` — model export (default export) > [!warning] Mixed types > `purchaseRequestId`, `sellerOfferId`, and `sellerId` use `Schema.Types.Mixed`. They are usually `ObjectId`s, but the template-checkout flow passes string ids that do not yet exist in the database, so the schema accepts both. ## Schema | Field | Type | Required | Default | Validation | Index | Description | | --- | --- | --- | --- | --- | --- | --- | | `purchaseRequestId` | Mixed (ObjectId or String) | yes | — | — | yes (compound, partial) | Linked [[PurchaseRequest]] id (or template id). | | `sellerOfferId` | Mixed (ObjectId or String) | yes | — | — | — | Linked [[SellerOffer]] id (or template offer ref). | | `buyerId` | ObjectId → [[User]] | yes | — | — | yes (compound) | Buyer paying. | | `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). | | `amount.amount` | Number | yes | — | — | — | Numeric amount. | | `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. | | `provider` | String | no | `shkeeper` | enum: `shkeeper` / `other` | yes (compound, partial) | Payment processor. | | `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. | | `blockchain.network` | String | no | — | — | — | Network identifier. | | `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. | | `blockchain.blockchain` | String | no | — | enum: `ethereum` / `polygon` / `bsc` / `avalanche` / `solana` / `optimism` / `arbitrum` / `base` / `gnosis` | — | Chain. | | `blockchain.token` | String | no | — | — | — | Token symbol. | | `blockchain.sender` | String | no | — | — | — | Source address. | | `blockchain.receiver` | String | no | — | — | — | Destination address. | | `blockchain.confirmedAt` | Date | no | — | — | — | When tx confirmed. | | `blockchain.confirmations` | Number | no | `0` | — | — | Confirmation count. | | `status` | String | no | `pending` | enum: `pending` / `processing` / `confirmed` / `completed` / `failed` / `cancelled` / `refunded` | yes (compound) | Lifecycle status. | | `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` | — | Escrow lifecycle. | | `providerPaymentId` | String | no | — | — | yes (sparse) | External provider id for idempotency. | | `metadata.userAgent` | String | no | — | — | — | Browser UA. | | `metadata.ipAddress` | String | no | — | — | — | Client IP. | | `metadata.walletType` | String | no | — | — | — | Wallet category. | | `metadata.paymentMethod` | String | no | — | — | — | Payment method label. | | `metadata.shkeeperUrl` | String | no | — | — | — | Invoice URL. | | `metadata.shkeeperInvoiceId` | String | no | — | — | — | Invoice id. | | `metadata.shkeeperData` | Mixed | no | — | — | — | Raw provider payload. | | `metadata.shkeeperStatus` | String | no | — | — | — | Provider status string. | | `metadata.balanceFiat` | String | no | — | — | — | Fiat-equivalent balance. | | `metadata.balanceCrypto` | String | no | — | — | — | Crypto balance. | | `metadata.cryptoName` | String | no | — | — | — | Crypto label. | | `metadata.walletAddress` | String | no | — | — | — | Wallet address. | | `metadata.shkeeperTaskId` | String | no | — | — | — | Payout task id. | | `metadata.lastWebhookAt` | Date | no | — | — | — | Last webhook timestamp. | | `metadata.webhookPayload` | Mixed | no | — | — | — | Last webhook body. | | `metadata.createdVia` | String | no | — | — | — | Origin marker. | | `metadata.payoutType` | String | no | — | — | — | Payout sub-type. | | `metadata.error` | String | no | — | — | — | Last error message. | | `metadata.failedAt` | Date | no | — | — | — | When it failed. | | `createdAt` | Date | auto | `Date.now` | — | yes (compound) | Mongoose timestamp. | | `processedAt` | Date | no | — | — | — | When processing started. | | `completedAt` | Date | no | — | — | — | When fully settled. | | `notes` | String | no | — | — | — | Free-form notes. | | `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. | ## Virtuals | Virtual | Returns | Definition | | --- | --- | --- | | `paymentRef` | `PAY-` | `backend/src/models/Payment.ts:191` | The schema enables `toJSON: { virtuals: true }` and `toObject: { virtuals: true }` so the ref appears in API responses. ## Indexes Defined at `backend/src/models/Payment.ts:174-188`: - `{ status: 1, createdAt: -1 }` — admin queues. - `{ buyerId: 1, status: 1 }` — buyer dashboard. - `{ sellerId: 1, status: 1 }` — seller dashboard. - `{ 'blockchain.transactionHash': 1 }` (sparse) — webhook lookup by hash. - `{ providerPaymentId: 1 }` (sparse) — provider idempotency. - `{ buyerId: 1, purchaseRequestId: 1, provider: 1, direction: 1 }` (unique, partial: `provider === 'shkeeper' && direction === 'in' && status === 'pending'`, name `uniq_pending_shkeeper_by_buyer_session`) — guards against duplicate pending invoices. ## Pre/Post Hooks None declared. ## Instance Methods None defined. ## Static Methods None defined. ## Relationships - **References**: [[User]] (`buyerId`, sometimes `sellerId`), [[PurchaseRequest]] (`purchaseRequestId`), [[SellerOffer]] (`sellerOfferId`). - **Referenced by**: Indirectly through [[PurchaseRequest]] status transitions and [[Dispute]] resolution amounts; no model holds a direct foreign key back to `Payment`. ## State Transitions Payment status: ```mermaid stateDiagram-v2 [*] --> pending pending --> processing : webhook received processing --> confirmed : tx confirmed confirmed --> completed : escrow released / payout done pending --> cancelled : buyer aborts processing --> failed : provider error completed --> refunded : dispute resolved failed --> [*] cancelled --> [*] completed --> [*] refunded --> [*] ``` Escrow state (for `direction: 'in'`): ```mermaid stateDiagram-v2 [*] --> funded : buyer pays funded --> releasable : delivery confirmed releasable --> releasing : payout initiated releasing --> released : payout completed funded --> refunded : dispute refund releasing --> failed : payout error released --> [*] refunded --> [*] failed --> [*] ``` ## Common Queries ```ts // Buyer history Payment.find({ buyerId, direction: 'in' }).sort({ createdAt: -1 }); // Seller payouts Payment.find({ sellerId, direction: 'out', status: 'completed' }); // Webhook lookup Payment.findOne({ providerPaymentId }); // Pending escrows ready for release Payment.find({ direction: 'in', escrowState: 'releasable' }); // Idempotent invoice creation (will fail by unique index if a pending one exists) Payment.create({ buyerId, purchaseRequestId, provider: 'shkeeper', direction: 'in', status: 'pending', ... }); ``` Related: [[PurchaseRequest]], [[SellerOffer]], [[User]], [[Dispute]].