Skip to content

[Phase 1] PaymentAttempt & PaymentTransaction State Machine #63

@syed-reza98

Description

@syed-reza98

Priority: P0 (Critical)

Phase: 1 - E-Commerce Core
Epic Link: Checkout / Payments (Stripe Integration #27 placeholder)
Estimate: 4 days
Type: Story

Context

Introduce PaymentAttempt and PaymentTransaction models to ensure atomic, auditable payment flows with retriable, idempotent state transitions. Prevent double-charging, enable reconciliation, and support future split payouts (Marketplace epic #37).

Problem

Current architecture lacks discrete tracking of payment lifecycle states (INITIATED → AUTHORIZED → CAPTURED → REFUNDED / FAILED) and does not persist provider references or failure metadata, increasing financial and integrity risk.

Scope

  • Model definitions (SQLite dev): PaymentAttempt, PaymentTransaction (later extend for Marketplace split payments)
  • State machine with allowed transitions & validation rules
  • Persistent providerReference (Stripe payment intent ID, etc.)
  • Error & retry metadata (lastErrorCode, attemptCount, nextRetryAt)
  • Idempotent create/capture endpoints
  • Reconciliation job (daily) to detect orphaned states

Acceptance Criteria

  • Prisma models created & migrated (incl. indexes: storeId+orderId, providerReference unique)
  • API endpoint: POST /api/payments/attempt (idempotent key header) creates INITIATED attempt
  • API endpoint: POST /api/payments/capture transitions AUTHORIZED → CAPTURED with validation
  • Double capture prevented (conflict 409 returned)
  • Failed attempt logs failure reason & increments attemptCount
  • Daily reconciliation job flags attempts stuck > 15m in AUTHORIZING state
  • Refund flow: POST /api/payments/refund creates REFUND transaction linked to original capture
  • Audit log entries for each state transition (action: payment_state_change)
  • All queries scoped by storeId

Data Model (Initial)

model PaymentAttempt {
  id              String   @id @default(cuid())
  storeId         String
  orderId         String
  provider        String   // stripe, bkash, cod
  providerReference String? @unique
  status          PaymentAttemptStatus @default(INITIATED)
  amount          Int      // minor units
  currency        String
  idempotencyKey  String?  @unique
  attemptCount    Int      @default(1)
  lastErrorCode   String?
  lastErrorMessage String?
  nextRetryAt     DateTime?
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  transactions    PaymentTransaction[]

  @@index([storeId, orderId])
  @@index([status])
}

enum PaymentAttemptStatus {
  INITIATED
  AUTHORIZING
  AUTHORIZED
  CAPTURED
  FAILED
  CANCELED
}

model PaymentTransaction {
  id           String   @id @default(cuid())
  attemptId    String
  storeId      String
  type         PaymentTransactionType
  amount       Int
  currency     String
  providerReference String?
  createdAt    DateTime @default(now())

  attempt      PaymentAttempt @relation(fields: [attemptId], references: [id])

  @@index([storeId, attemptId])
}

enum PaymentTransactionType {
  AUTH
  CAPTURE
  REFUND
  VOID
}

State Machine Rules

  • INITIATED → AUTHORIZING → AUTHORIZED → CAPTURED
  • AUTHORIZED → VOID allowed
  • CAPTURED → REFUND (multi refund allowed up to captured amount)
  • Any → FAILED (terminal)

Dependencies

Metrics

  • Mean auth latency < 3s
  • Double-charge incidents = 0
  • Reconciliation discrepancies < 0.5% of attempts

Testing Checklist

  • Idempotent attempt creation
  • Failed authorization triggers retry scheduling
  • Capture only after AUTHORIZED
  • Refund limited to remaining refundable amount
  • Audit log written for transitions

Risk

High financial & integrity risk if absent (score: 18). Addresses double-charging & orphaned payment states.

References

  • docs/GITHUB_ISSUES_COMPARISON_ANALYSIS.md (risk matrix section)
  • Stripe Payment Intent lifecycle

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions