Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
11 KiB
Markdown
182 lines
11 KiB
Markdown
---
|
|
title: Payment
|
|
tags: [data-model, mongoose]
|
|
aliases: [Payment Record, Escrow, IPayment]
|
|
---
|
|
|
|
# Payment
|
|
|
|
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
|
|
|
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. The current model is centered on Request Network pay-in, in-house checkout metadata, on-chain transaction verification, escrow state, and provider request IDs. The `provider` and `direction` discriminators let one collection hold incoming buyer payments, outgoing seller releases, refunds, and legacy/other provider records.
|
|
|
|
> [!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.
|
|
|
|
> [!warning] `provider` values (schema enum vs reality)
|
|
> The declared schema enum for `provider` is only `['request.network', 'other']`, yet production code writes additional values. The full set of providers that actually appear is: `request.network`, `shkeeper`, `decentralized`, `test`, `other`.
|
|
> - `paymentCoordinator.ts` and `RequestTemplateService.ts` create `Payment` docs with `provider: 'shkeeper'`.
|
|
> - The decentralized/on-chain flow uses `decentralized`.
|
|
> - ⚠️ **Frontend type bug:** the frontend `PaymentProvider` TypeScript type (`frontend/src/types/payment.ts`) is `'request.network' | 'test' | 'other'` — it is **missing `shkeeper` and `decentralized`**, so the client cannot represent payments created by those providers.
|
|
|
|
> [!warning] `confirmed` vs `completed` — stats undercount
|
|
> Payment stats (`paymentService.getPaymentStats`) only increment `successfulPayments` for status **`confirmed`**:
|
|
> ```ts
|
|
> case "confirmed": stats.successfulPayments += stat.count; break;
|
|
> ```
|
|
> The terminal SHKeeper / DePay state is **`completed`**, which has no case in the switch and is therefore **not** counted as a successful payment. ⚠️ This causes successful-payment stats to undercount any payment that reached `completed`.
|
|
|
|
> [!warning] `SIM_` payment-hash bypass — security concern
|
|
> In both `payment/paymentRoutes.ts` and `marketplace/routes.ts`, a `paymentHash` that starts with `SIM_` (or a short `0x...` hash under 64 chars) is treated as a simulated transaction and **skips on-chain verification entirely** (`isVerified = true`). There is **no environment guard** (e.g. no `NODE_ENV !== 'production'` check) around this branch, so the bypass is reachable in production. ⚠️ A caller can mark a payment verified without any real on-chain settlement.
|
|
|
|
## 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 | `request.network` | enum (declared): `request.network` / `other`. Values written in practice: `request.network`, `shkeeper`, `decentralized`, `request.network`, `test`, `other` | yes (compound, partial) | Payment processor. ⚠️ See provider note below — code writes `shkeeper` and `decentralized` even though they are not in the declared schema enum, and the frontend `PaymentProvider` type is missing both. |
|
|
| `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. ⚠️ `confirmed` vs `completed`: only `confirmed` is counted as a successful payment in stats. See status note below. |
|
|
| `escrowState` | String | no | — | enum: `funded` / `releasable` / `released` / `refunded` / `releasing` / `failed` / `cancelled` / `partial` | — | Escrow lifecycle. Note the intermediate states `releasable` (delivery confirmed, ready to pay out) and `releasing` (payout in flight) between `funded` and `released`. |
|
|
| `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.requestNetworkRequestId` | String | no | — | — | — | Request Network request id. |
|
|
| `metadata.requestNetworkPaymentReference` | String | no | — | — | — | Request Network payment reference. |
|
|
| `metadata.requestNetworkSecurePaymentUrl` | String | no | — | — | — | Request Network secure payment URL. |
|
|
| `metadata.requestNetworkData` | Mixed | no | — | — | — | Raw Request Network payload. |
|
|
| `metadata.transactionSafety` | Mixed | no | — | — | — | Last Transaction Safety Provider decision, checks, evidence, and blocker reason. |
|
|
| `metadata.derivedDestination` | Object | no | — | — | — | Snapshot of per-payment derived destination address/path/index/chain. |
|
|
| `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]].
|