In-house Request Network checkout went fully end-to-end on dev today. A real 0.01 USDC payment flowed through wallet connect -> approve -> ERC20FeeProxy.transferFromWithReferenceAndFee -> RN webhook -> TransactionSafetyProvider -> Payment.status=completed -> page success state. Tx 0x494c77a29161b5100d8e0b1ac675f1822955d0bb3633ecdbfafb886f84f2f320. Docs: - New PRD: Wallet, Multichain, Confirmations, AML, Trezor (5 follow-ups, each sized for an independent contributor) - Updated PRD: Request Network In-House Checkout (phases 0..3 done, phase 4 partial, phases 5-6 not started) - Updated handoff: deployed versions, what is working end-to-end, follow-up tasks index Taskmaster: 5 new top-level tasks (#7..#11) covering ephemeral destination wallets, multichain proxy registry + USDC/USDT, runtime confirmation thresholds, optional seller-paid AML screening, and Trezor signing for admin actions. Tasks are scoped fine-grained so each is independent enough for kimi to pick up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
8.2 KiB
title, tags, aliases
| title | tags | aliases | |||||
|---|---|---|---|---|---|---|---|
| Payment |
|
|
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 definitionbackend/src/models/Payment.ts:257— model export (default export)
[!warning] Mixed types
purchaseRequestId,sellerOfferId, andsellerIduseSchema.Types.Mixed. They are usuallyObjectIds, 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 / request.network / request-network / 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 / cancelled / partial |
— | 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.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.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', nameuniq_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, sometimessellerId), 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:
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'):
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
// 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.