feat(payments): add Order & Escrow entities with CQRS commands, Prisma schema

- Add Order entity with lifecycle (pending → paid → completed/cancelled/refunded)
- Add Escrow entity with hold/release/dispute flow for secure transactions
- Add PlatformFee value object with tiered commission calculation
- Implement CQRS: CreateOrder, CancelOrder, HoldEscrow, ReleaseEscrow commands
- Add GetOrderStatus query handler
- Add OrdersController with REST endpoints and DTOs
- Add Prisma models for Order, Escrow, EscrowStatusHistory
- Add domain event classes for order and escrow state changes
- Add unit tests for Order, Escrow entities and PlatformFee VO
- Update PROJECT_TRACKER to Wave 14 status

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-12 23:40:00 +07:00
parent 836499c1cf
commit 2c97f99214
42 changed files with 1786 additions and 34 deletions

View File

@@ -65,6 +65,8 @@ model User {
refreshTokens RefreshToken[]
oauthAccounts OAuthAccount[]
buyerTransactions Transaction[] @relation("BuyerTransactions")
buyerOrders Order[] @relation("BuyerOrders")
sellerOrders Order[] @relation("SellerOrders")
mfaChallenges MfaChallenge[]
@@index([role])
@@ -275,6 +277,7 @@ model Listing {
transactions Transaction[]
inquiries Inquiry[]
orders Order[]
// --- Single-column indexes ---
@@index([status])
@@ -419,6 +422,7 @@ enum PaymentType {
LISTING_FEE
DEPOSIT
FEATURED_LISTING
AUCTION_PAYMENT
}
model Payment {
@@ -427,6 +431,8 @@ model Payment {
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
transactionId String?
transaction Transaction? @relation(fields: [transactionId], references: [id], onDelete: SetNull)
orderId String?
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
provider PaymentProvider
type PaymentType
amountVND BigInt
@@ -440,6 +446,7 @@ model Payment {
@@unique([userId, provider, idempotencyKey], name: "Payment_idempotency_unique")
@@index([userId])
@@index([transactionId])
@@index([orderId])
@@index([status])
@@index([providerTxId])
@@index([createdAt])
@@ -448,6 +455,77 @@ model Payment {
@@index([userId, type, createdAt(sort: Desc)])
}
// =============================================================================
// ORDERS & ESCROW (Auction Settlement)
// =============================================================================
enum OrderStatus {
CREATED
PAYMENT_PENDING
PAYMENT_CONFIRMED
ESCROW_HELD
SHIPPED
DELIVERED
DISPUTE
ESCROW_RELEASED
COMPLETED
CANCELLED
REFUNDED
}
enum EscrowStatus {
PENDING
HELD
RELEASED
REFUNDED
DISPUTED
}
model Order {
id String @id @default(cuid())
buyerId String
buyer User @relation("BuyerOrders", fields: [buyerId], references: [id], onDelete: Restrict)
sellerId String
seller User @relation("SellerOrders", fields: [sellerId], references: [id], onDelete: Restrict)
listingId String
listing Listing @relation(fields: [listingId], references: [id], onDelete: Restrict)
status OrderStatus @default(CREATED)
amountVND BigInt
platformFeeVND BigInt
sellerPayoutVND BigInt
idempotencyKey String? @unique
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
payments Payment[]
escrow Escrow?
@@index([buyerId])
@@index([sellerId])
@@index([listingId])
@@index([status])
@@index([createdAt(sort: Desc)])
}
model Escrow {
id String @id @default(cuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id], onDelete: Restrict)
amountVND BigInt
feeVND BigInt
status EscrowStatus @default(PENDING)
heldAt DateTime?
releasedAt DateTime?
disputeReason String? @db.Text
disputedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([orderId])
}
// =============================================================================
// SUBSCRIPTIONS
// =============================================================================