Initial commit: nick docs
This commit is contained in:
157
02 - Data Models/Payment.md
Normal file
157
02 - Data Models/Payment.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
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-<LAST_8_OF_ID_UPPERCASE>` | `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]].
|
||||
Reference in New Issue
Block a user