Bounded Context: Payment (PAY)

Responsibility: Payment-method validation, fare calculation, pre-authorization and capture, driver earnings and payouts (weekly + instant), refund classification and execution, receipt generation and correction. The largest context in RideNow and the only one with a direct PCI-DSS Level 1 obligation.


Context Map

Relation Other Bounded Context Position Pattern Description
PAY ↔ RideManagement Ride Management (RIDE) downstream / upstream Customer/Supplier & Published Language PAY consumes RideRequested, RideRequestCancelledByRider, RideCompleted, RideCancelledByRider, RideCancelledRiderNoShow, NoDriverAvailable from RIDE. PAY publishes PaymentAuthorized, PaymentCaptured, PaymentFailed, FullRefundProcessed, PartialRefundProcessed so RIDE can advance the ride lifecycle and surface receipts.
PAY → DriverManagement Driver Management (DRV) upstream Published Language PAY publishes DriverEarningsCredited, WeeklyPayoutInitiated, PayoutCompleted, InstantPayoutProcessed, DriverBalanceDeducted to DRV for the driver app’s earnings view and for driver-state effects on deduction.
PAY → RiderManagement Rider Management (RDR) upstream Published Language PAY publishes PaymentMethodAdded, PaymentMethodValidated, DebtSettled so RDR knows whether the rider account has a usable method.
PAY → PaymentProcessor external infrastructure downstream Conformist (infrastructure) PCI-DSS Level 1 certified processor (primary + failover) wrapped behind PaymentProcessor. Card data never leaves the processor boundary.
PAY → BankTransferService external infrastructure downstream Conformist (infrastructure) ACH/SEPA bank transfers for weekly payouts wrapped behind BankTransferService; settlement callback drives BankTransferCompleted.
PAY → InstantPayoutNetwork external infrastructure downstream Conformist (infrastructure) Push-to-card debit-card instant transfer wrapped behind InstantPayoutNetwork.

Enums

Currencies, Currency (Shared Kernel)

The currency type is co-owned in the Shared Kernel — see the Shared Kernel page. It comes in two parts:

  • Currencies — the closed enum of ISO 4217 alphabetic codes (active list, ~178 entries: usd, eur, gbp, jpy, chf, … through zwg).
  • Currency — a value type that bundles a Currencies code with the ISO 4217 reference data: numericCode (e.g. 840 for USD) and defaultFractionDigits (the minor-unit precision used to render fare totals on receipts). Modeled on java.util.Currency.

Used inside every Amount.

PaymentStatus

  • Values: authorized, captured, partiallyRefunded, fullyRefunded, failed.
  • Realizes: REQ-PAY-020, REQ-PAY-022, REQ-PAY-023, REQ-PAY-060, REQ-PAY-063.

PaymentMethodType

  • Values: creditCard, debitCard, paypal, applePay, googlePay.

PaymentMethodValidationStatus

  • Values: pending, valid, invalid, expired.
  • Realizes: REQ-PAY-001, REQ-PAY-002.

RefundRequestStatus

  • Values: pending, underReview, approved, rejected, executed.
  • Realizes: REQ-PAY-061, REQ-PAY-062, REQ-PAY-063.

RefundFaultType

  • Values: driverFault, riderFault, platformFault, disputed.
  • Realizes: REQ-PAY-063, REQ-PAY-064.

PayoutType

  • Values: weekly, instant.
  • Realizes: REQ-PAY-043, REQ-PAY-047.

PayoutStatus

  • Values: initiated, inFlight, settled, failed.
  • Realizes: REQ-PAY-043, REQ-PAY-045.

ReceiptStatus

  • Values: generated, corrected.
  • Realizes: REQ-PAY-080, REQ-PAY-081.

Value Types

Amount (Shared Kernel)

Amount is co-owned in the Shared Kernel — see the Shared Kernel page for its fields (value: decimal min(0), currency: Currency) and the nonNegative invariant. It replaces the prior Money value type that lived in this context: every monetary field that used to be a raw decimal (with a sibling currency) is now an Amount.

FareBreakdown

The full fare decomposition shown to riders and bound into receipts.

  • Fields:
    • baseFare: Amount
    • distanceCharge: Amount
    • timeCharge: Amount
    • surgeCharge: Amount
    • tolls: Amount
    • promotionDiscount: Amount
    • cancellationFee: Amount optional
    • fees: Amount
    • total: Amount
  • Invariants:
    • componentsSumToTotal: total.value = baseFare.value + distanceCharge.value + timeCharge.value + surgeCharge.value + tolls.value + fees.value - promotionDiscount.value + (cancellationFee?.value ?: 0) — enforcement: reject.
    • currenciesAligned: every component shares total.currency — enforcement: reject. Mixing currencies inside one breakdown is structurally impossible.
  • Realizes: REQ-PAY-014.

FareParameters

Per-market fare engine inputs.

  • Fields:
    • baseFare: Amount
    • perKmRate: Amount
    • perMinuteRate: Amount
    • surgeMultiplier: Decimal — min(1.0), max(5.0)
    • minimumFare: Amount
    • market: String

CommissionRate

  • Fields:
    • percentage: Decimal — min(0), max(100)
  • Realizes: REQ-PAY-042.

PaymentMethodInfo

  • Fields:
    • paymentMethodId: UUID
    • type: PaymentMethodType
    • last4: String
    • isDefault: Boolean
    • priority: Integer
    • validationStatus: PaymentMethodValidationStatus
    • validatedAt: DateTime optional
  • Realizes: REQ-PAY-001, REQ-PAY-002.

PayoutDetails

  • Fields:
    • payoutId: UUID
    • type: PayoutType
    • amount: Amount
    • fee: Amount optional
    • netAmount: Amount
    • initiatedAt: DateTime
    • expectedArrivalAt: DateTime
    • settledAt: DateTime optional
  • Invariants:
    • netConsistency: netAmount.value = amount.value - (fee?.value ?: 0).
  • Realizes: REQ-PAY-043, REQ-PAY-045, REQ-PAY-047.

Entities

RiderAccount

Rider’s payment-method registry and outstanding debt.

  • Identifier: accountId : UUID
  • Fields:
    • riderId: UUID unique
    • paymentMethods: list<PaymentMethodInfo>
    • outstandingDebt: Amount — default 0
    • createdAt: DateTime
  • Invariants:
    • debtNonNegative: outstandingDebt.amount >= 0 — enforcement: reject.
  • Operations:
    • Add payment method on command AddPaymentMethod
      • Realizes: REQ-PAY-001
      • State change: paymentMethods = paymentMethods + addPaymentMethod.method.
      • Emits: PaymentMethodAdded.
    • Validate stored method (zero-value auth) on command ValidatePaymentMethod
      • Realizes: REQ-PAY-001, REQ-PAY-002
      • Emits: PaymentMethodValidated with the processor outcome.
    • Settle outstanding debt on command SettleDebt
      • Realizes: REQ-PAY-024
      • Precondition hasDebt: outstandingDebt.amount > 0
      • State change: outstandingDebt = Amount(0, currency).
      • Emits: DebtSettled.
  • Realizes: REQ-PAY-001, REQ-PAY-002, REQ-PAY-003, REQ-PAY-024, REQ-PAY-NFR-004.

Payment

Single ride’s payment lifecycle — pre-auth → capture or refund.

  • Identifier: paymentId : UUID
  • Fields:
    • rideRequestId: UUID
    • rideId: UUID optional
    • riderId: UUID
    • status: PaymentStatus — default authorized
    • committedFare: Amount
    • finalFare: Amount optional
    • fareBreakdown: FareBreakdown optional
    • paymentMethodUsed: PaymentMethodInfo
    • refundedAmount: Amount — default 0
    • retryCount: Integer — min(0), default 0
    • debtRecorded: Boolean — default false
    • authorizedAt: DateTime
    • capturedAt: DateTime optional
    • failedAt: DateTime optional
    • failureReason: String optional
    • createdAt: DateTime
  • Invariants:
    • refundDoesNotExceedCapture: when finalFare is defined, refundedAmount.amount <= finalFare.amount — enforcement: reject.
  • Operations:
    • Authorize payment at request on AuthorizePayment
      • Realizes: REQ-PAY-020
      • State changes: rideRequestId, riderId, committedFare, paymentMethodUsed, status = authorized, authorizedAt = now(), createdAt = now().
      • Emits: PaymentAuthorized.
    • Release pre-auth on free cancellation on ReleasePaymentHold
      • Realizes: REQ-PAY-021, REQ-PAY-060
      • Precondition isAuthorized: Payment.status = authorized.
      • State change: status = fullyRefunded.
      • Emits: PaymentHoldReleased.
    • Capture on completion on CapturePayment
      • Realizes: REQ-PAY-022, REQ-PAY-NFR-002, REQ-PAY-NFR-003
      • Precondition isAuthorized: Payment.status = authorized.
      • State changes: rideId, finalFare, fareBreakdown, status = captured, capturedAt = now().
      • Emits: PaymentCaptured.
    • Retry capture after transient failure on RetryPayment
      • Realizes: REQ-PAY-025
      • Precondition retriesLeft: retryCount < 3.
      • State change: retryCount = retryCount + 1.
      • Emits: PaymentRetryAttempted.
    • Fall back to next active method on FallbackPaymentMethod
      • Realizes: REQ-PAY-026
      • Precondition retriesExhausted: retryCount >= 3.
      • State changes: paymentMethodUsed = nextMethod, retryCount = 0.
      • Emits: PaymentMethodFallback.
    • Record payment failure as debt on RecordPaymentFailure
      • Realizes: REQ-PAY-023
      • State changes: status = failed, failedAt = now(), failureReason, debtRecorded = true.
      • Emits: PaymentFailed.
    • Process partial refund on ProcessPartialRefund
      • Realizes: REQ-PAY-063, REQ-PAY-NFR-003
      • Precondition isCaptured: status = captured or status = partiallyRefunded.
      • State changes: refundedAmount += amount; status = fullyRefunded if total refund ≥ final fare, else partiallyRefunded.
      • Emits: PartialRefundProcessed.
    • Process full refund on ProcessFullRefund
      • Realizes: REQ-PAY-060, REQ-PAY-063
      • State changes: refundedAmount = finalFare ?: committedFare, status = fullyRefunded.
      • Emits: FullRefundProcessed.
  • Realizes: REQ-PAY-020, REQ-PAY-021, REQ-PAY-022, REQ-PAY-023, REQ-PAY-025, REQ-PAY-026, REQ-PAY-NFR-002, REQ-PAY-NFR-003.

DriverBalance

Driver’s earnings balance and payout history.

  • Identifier: balanceId : UUID
  • Fields:
    • driverId: UUID unique
    • availableBalance: Amount — default 0
    • pendingBalance: Amount — default 0
    • totalEarnings: Amount — default 0
    • totalPayouts: Amount — default 0
    • instantPayoutsTodayAmount: Amount — default 0
    • instantPayoutsTodayDate: Date
    • lastWeeklyPayoutAt: DateTime optional
    • createdAt: DateTime
  • Invariants:
    • balanceNonNegative: availableBalance.amount >= 0 — enforcement: reject.
    • earningsConsistency: totalEarnings = availableBalance + totalPayouts + pendingBalance — enforcement: alert.
  • Operations:
    • Credit driver earnings on capture on CreditDriverEarnings
      • Realizes: REQ-PAY-040, REQ-PAY-041, REQ-PAY-042
      • State changes: availableBalance += netEarnings, totalEarnings += netEarnings.
      • Emits: DriverEarningsCredited.
    • Initiate weekly payout on InitiateWeeklyPayout
      • Realizes: REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046
      • Precondition aboveMinimum: availableBalance > weeklyPayoutThreshold(driverId).
      • State changes: pendingBalance = availableBalance, availableBalance = 0, lastWeeklyPayoutAt = now().
      • Emits: WeeklyPayoutInitiated with expectedArrivalAt = now() + 2 business days.
    • Complete payout on bank settlement on CompletePayout
      • Realizes: REQ-PAY-045, REQ-PAY-NFR-006
      • State changes: pendingBalance = 0, totalPayouts += amount.
      • Emits: PayoutCompleted.
    • Process instant payout on ProcessInstantPayout
      • Realizes: REQ-PAY-047, REQ-PAY-048, REQ-PAY-049
      • Preconditions:
        • belowDailyCap: instantPayoutsTodayAmount + amount <= instantPayoutDailyCap(driverId).
        • feeAndArrivalQuoted: both feeQuoted and expectedArrival are defined.
      • State changes: availableBalance -= amount, instantPayoutsTodayAmount += amount, totalPayouts += amount - feeQuoted.
      • Emits: InstantPayoutProcessed.
    • Deduct driver-fault refund from balance on DeductDriverBalance
      • Realizes: REQ-PAY-064
      • State change: availableBalance -= deductDriverBalance.amount.
      • Emits: DriverBalanceDeducted with reason "driver-fault refund".
  • Realizes: REQ-PAY-040, REQ-PAY-041, REQ-PAY-042, REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046, REQ-PAY-047, REQ-PAY-048, REQ-PAY-049, REQ-PAY-064, REQ-PAY-NFR-006.

RefundRequest

Rider-filed refund dispute reviewed by a trust-and-safety operator.

  • Identifier: refundRequestId : UUID
  • Fields:
    • rideId: UUID
    • riderId: UUID
    • paymentId: UUID
    • driverId: UUID
    • requestedAmount: Amount
    • approvedAmount: Amount optional
    • status: RefundRequestStatus — default pending
    • faultType: RefundFaultType optional
    • reason: String — maxLength(2000)
    • reviewerNotes: String optional
    • requestedAt: DateTime
    • reviewedAt: DateTime optional
    • executedAt: DateTime optional
  • Operations:
    • File refund request on RequestRefund
      • Realizes: REQ-PAY-061
      • State changes: capture all request fields, status = pending, requestedAt = now().
      • Emits: RefundRequested.
    • Approve refund on ApproveRefund
      • Realizes: REQ-PAY-062, REQ-PAY-063
      • Precondition pendingOrUnderReview: status = pending or status = underReview.
      • State changes: status = approved, approvedAmount, faultType, reviewerNotes, reviewedAt = now().
      • Emits: RefundApproved.
    • Reject refund on RejectRefund
      • Realizes: REQ-PAY-062
      • Precondition pendingOrUnderReview: status = pending or status = underReview.
      • State changes: status = rejected, reviewerNotes, reviewedAt = now().
      • Emits: RefundRejected.
    • Execute approved refund on ExecuteRefund
      • Realizes: REQ-PAY-063
      • Precondition approved: status = approved.
      • State changes: status = executed, executedAt = now().
      • Emits: RefundExecuted.
  • Realizes: REQ-PAY-061, REQ-PAY-062, REQ-PAY-063, REQ-PAY-064, REQ-PAY-065.

Receipt

Per-ride receipt; corrected on refund.

  • Identifier: receiptId : UUID
  • Fields:
    • rideId: UUID
    • riderId: UUID
    • driverId: UUID
    • paymentId: UUID
    • fareBreakdown: FareBreakdown
    • originalTotal: Amount
    • adjustedTotal: Amount optional
    • refundAmount: Amount — default 0
    • status: ReceiptStatus — default generated
    • generatedAt: DateTime
    • correctedAt: DateTime optional
  • Operations:
    • Generate receipt within 60s of capture on GenerateReceipt
      • Realizes: REQ-PAY-080
      • State changes: capture ride/rider/driver/payment ids, fare breakdown, originalTotal, status = generated, generatedAt = now().
      • Postcondition deliveredWithin60s: now() - generateReceipt.capturedAt <= 60s.
      • Emits: ReceiptGenerated.
    • Correct receipt after refund on CorrectReceipt
      • Realizes: REQ-PAY-081
      • State changes: refundAmount, adjustedTotal = originalTotal - refundAmount, status = corrected, correctedAt = now().
      • Emits: ReceiptCorrected.
  • Realizes: REQ-PAY-080, REQ-PAY-081.

Aggregates

Aggregate Root
PaymentAggregate Payment
DriverBalanceAggregate DriverBalance
RefundRequestAggregate RefundRequest
ReceiptAggregate Receipt
RiderAccountAggregate RiderAccount

State Machines

PaymentLifecycle on Payment

  • Start state: authorized.
  • States: authorized, captured, partiallyRefunded, fullyRefunded, failed.
  • Transitions:
    • authorizedcaptured on CapturePayment
    • authorizedfullyRefunded on ReleasePaymentHold
    • authorizedfailed on RecordPaymentFailure
    • capturedpartiallyRefunded on ProcessPartialRefund
    • capturedfullyRefunded on ProcessFullRefund
    • partiallyRefundedfullyRefunded on ProcessFullRefund
    • partiallyRefundedpartiallyRefunded on ProcessPartialRefund
  • Final states: fullyRefunded, failed.

RefundRequestLifecycle on RefundRequest

  • Start state: pending.
  • States: pending, underReview, approved, rejected, executed.
  • Transitions:
    • pendingunderReview on BeginRefundReview
    • pendingapproved on ApproveRefund
    • underReviewapproved on ApproveRefund
    • underReviewrejected on RejectRefund
    • approvedexecuted on ExecuteRefund
  • Final states: rejected, executed.

ReceiptLifecycle on Receipt

  • Start state: generated.
  • States: generated, corrected.
  • Transitions:
    • generatedcorrected on CorrectReceipt
    • correctedcorrected on CorrectReceipt

Domain Services

FareCalculationService

Compute estimates, final fares, cancellation fees, and driver earnings.

  • Operations:
    • computeEstimate(pickup, dropoff, params: FareParameters) : FareBreakdown
    • computeFinalFare(rideId: uuid, actualDistance: Distance, actualDuration: Duration, params: FareParameters) : FareBreakdown
    • computeCancellationFee(rideId: uuid, minutesSinceAssignment: int, params: FareParameters) : Amount
    • computeDriverEarnings(grossFare: Amount, commission: CommissionRate) : Amount
  • Realizes: REQ-PAY-010, REQ-PAY-011, REQ-PAY-012, REQ-PAY-013, REQ-PAY-014, REQ-PAY-015, REQ-PAY-040, REQ-PAY-042.

PayoutBatchOrchestrator

Mondays at 00:00 local market time — sweep balances, initiate payouts, reconcile against bank settlement.

  • Operations:
    • runWeeklyBatch(market: string, cutoffAt: datetime) : void
    • reconcileBatch(batchId: uuid) : void
  • Realizes: REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046, REQ-PAY-NFR-006.

FraudScoringService

Risk score for new authorizations and instant payouts; consumed by the PaymentAggregate and DriverBalanceAggregate.

  • Operations:
    • scoreAuthorization(payment: Payment) : decimal
    • scoreInstantPayout(driverId: uuid, amount: Amount) : decimal
  • Realizes: REQ-NFR-031.

Infrastructure Services

PaymentProcessor

PCI-DSS Level 1 certified processor (primary + failover) — card data never crosses the boundary.

  • Operations:
    • zeroValueAuthorize(method: PaymentMethodInfo) : PaymentMethodValidationStatus
    • placeHold(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
    • captureCharge(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
    • releaseHold(method: PaymentMethodInfo, holdId: uuid) : void
    • refund(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
  • Realizes: REQ-PAY-NFR-001, REQ-PAY-NFR-005, REQ-NFR-030.

BankTransferService

Bank ACH/SEPA payouts to driver accounts; settlement callback drives BankTransferCompleted.

  • Operations:
    • transferToBank(bankAccount, amount: Amount, idempotencyKey: uuid) : uuid
    • getTransferStatus(transferId: uuid) : PayoutStatus
  • Realizes: REQ-PAY-043, REQ-PAY-045, REQ-PAY-NFR-006.

InstantPayoutNetwork

Push-to-card debit-card instant transfer.

  • Operations:
    • transferToDebitCard(card, amount: Amount, idempotencyKey: uuid) : uuid
    • getQuotedFee(card, amount: Amount) : Amount
    • getEstimatedArrival(card) : Duration
  • Realizes: REQ-PAY-047, REQ-PAY-048.

PaymentNotificationService

Rider debt notifications, driver payout notifications, receipt delivery.

  • Operations:
    • notifyRiderOfDebt(riderId: uuid, amount: Amount) : void
    • notifyDriverOfPayout(driverId: uuid, amount: Amount, expectedArrival: datetime) : void
    • deliverReceipt(riderId: uuid, receiptId: uuid) : void
  • Realizes: REQ-PAY-023, REQ-PAY-046, REQ-PAY-080.

Commands

Command Fields Realizes
AddPaymentMethod accountId, riderId, method, correlationId REQ-PAY-001
ValidatePaymentMethod accountId, paymentMethodId, outcome, correlationId REQ-PAY-001, REQ-PAY-002
SettleDebt accountId, amount, correlationId REQ-PAY-024
AuthorizePayment rideRequestId, riderId, committedFare, method, correlationId REQ-PAY-020
ReleasePaymentHold paymentId, correlationId REQ-PAY-021, REQ-PAY-060
CapturePayment paymentId, rideId, finalFare, fareBreakdown, correlationId REQ-PAY-022, REQ-PAY-NFR-002, REQ-PAY-NFR-003
RetryPayment paymentId, correlationId REQ-PAY-025
FallbackPaymentMethod paymentId, nextMethod, correlationId REQ-PAY-026
RecordPaymentFailure paymentId, reason, correlationId REQ-PAY-023
ProcessPartialRefund paymentId, amount, faultType, correlationId REQ-PAY-063
ProcessFullRefund paymentId, correlationId REQ-PAY-060, REQ-PAY-063
CreditDriverEarnings driverId, rideId, grossFare, commission, netEarnings, correlationId REQ-PAY-040, REQ-PAY-041, REQ-PAY-042
InitiateWeeklyPayout driverId, amount, correlationId REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046
CompletePayout driverId, payoutId, amount, correlationId REQ-PAY-045, REQ-PAY-NFR-006
ProcessInstantPayout driverId, amount, feeQuoted, expectedArrival, correlationId REQ-PAY-047, REQ-PAY-048, REQ-PAY-049
DeductDriverBalance driverId, rideId, amount, correlationId REQ-PAY-064
RequestRefund rideId, riderId, paymentId, driverId, amount, reason, correlationId REQ-PAY-061
BeginRefundReview refundRequestId, correlationId REQ-PAY-062
ApproveRefund refundRequestId, approvedAmount, faultType, notes, correlationId REQ-PAY-062, REQ-PAY-063
RejectRefund refundRequestId, notes, correlationId REQ-PAY-062
ExecuteRefund refundRequestId, correlationId REQ-PAY-063
GenerateReceipt rideId, riderId, driverId, paymentId, fareBreakdown, capturedAt, correlationId REQ-PAY-080
CorrectReceipt receiptId, refundAmount, correlationId REQ-PAY-081

Events

Internal events

Event Fields Realizes
PaymentMethodAdded accountId, riderId, method, addedAt REQ-PAY-001
PaymentMethodValidated accountId, paymentMethodId, status, validatedAt REQ-PAY-001, REQ-PAY-002
DebtSettled accountId, settledAmount, settledAt REQ-PAY-024
PaymentAuthorized paymentId, rideRequestId, riderId, amount, authorizedAt REQ-PAY-020
PaymentHoldReleased paymentId, rideRequestId, releasedAt REQ-PAY-021
PaymentCaptured paymentId, rideId, riderId, finalFare, fareBreakdown, paymentMethodUsed, capturedAt REQ-PAY-022, REQ-PAY-NFR-002
PaymentRetryAttempted paymentId, attempt, attemptedAt REQ-PAY-025
PaymentRetryExhausted paymentId, exhaustedAt REQ-PAY-025
PaymentMethodFallback paymentId, newMethod, fallbackAt REQ-PAY-026
PaymentFailed paymentId, riderId, debtAmount, reason, failedAt REQ-PAY-023
PartialRefundProcessed paymentId, riderId, amount, faultType, refundedAt REQ-PAY-063
FullRefundProcessed paymentId, riderId, amount, refundedAt REQ-PAY-060, REQ-PAY-063
DriverEarningsCredited driverId, rideId, grossFare, commission, netEarnings, creditedAt REQ-PAY-040, REQ-PAY-041
WeeklyPayoutInitiated driverId, amount, expectedArrivalAt, initiatedAt REQ-PAY-043, REQ-PAY-046
PayoutCompleted driverId, payoutId, amount, settledAt REQ-PAY-045
InstantPayoutProcessed driverId, amount, fee, expectedArrival, initiatedAt REQ-PAY-047, REQ-PAY-048, REQ-PAY-049
DriverBalanceDeducted driverId, rideId, amount, reason, deductedAt REQ-PAY-064
RefundRequested refundRequestId, rideId, riderId, requestedAt REQ-PAY-061
RefundApproved refundRequestId, rideId, riderId, driverId, approvedAmount, faultType, reviewedAt REQ-PAY-062, REQ-PAY-063
RefundRejected refundRequestId, rejectedAt REQ-PAY-062
RefundExecuted refundRequestId, paymentId, amount, faultType, executedAt REQ-PAY-063
ReceiptGenerated receiptId, rideId, riderId, total, generatedAt REQ-PAY-080
ReceiptCorrected receiptId, rideId, refundAmount, adjustedTotal, correctedAt REQ-PAY-081

Temporal events

Event Schedule Realizes
WeeklyPayoutCycleDue recurring 0 0 * * MON per-market REQ-PAY-043
PreAuthReleaseDeadline relative-to PaymentHoldReleased offset 1 business day; guard not (Payment.status = fullyRefunded) — alert if hold not actually released by processor by deadline REQ-PAY-021
InstantPayoutDailyReset recurring 0 0 * * * per-market REQ-PAY-049

External-bound events (Published Language)

PaymentAuthorized, PaymentCaptured, PaymentFailed, FullRefundProcessed, PartialRefundProcessed cross the boundary to Ride Management so the ride lifecycle can advance and the receipt can be surfaced. DriverEarningsCredited, WeeklyPayoutInitiated, PayoutCompleted, InstantPayoutProcessed, DriverBalanceDeducted cross to Driver Management for the driver-app earnings view. PaymentMethodAdded, PaymentMethodValidated, DebtSettled cross to Rider Management.

External-consumed events

Event From Triggers
RideRequested Ride Management reaction preAuthorizeOnRideRequested
RideRequestCancelledByRider Ride Management reaction releaseOnFreeCancellation
RideCompleted Ride Management reaction captureOnRideCompleted
RideCancelledByRider Ride Management downstream cancellation flows (cancellation fee + capture)
RideCancelledRiderNoShow Ride Management downstream no-show fee flow
NoDriverAvailable Ride Management reaction autoRefundOnSystemCancellation
BankTransferCompleted BankTransferService reaction completePayoutOnBankSettlement

Reactions

preAuthorizeOnRideRequested

“Place hold on ride request.”

  • Trigger: event RideRequested
  • Effect: AuthorizePayment(rideRequestId, riderId, committedFare = Amount(value = event.committedFare.amount.value, currency = event.committedFare.amount.currency), method = lookupDefaultMethod(event.riderId))
  • Realizes: REQ-PAY-020

releaseOnFreeCancellation

“Release hold within 1 business day of rider cancel before assignment.”

  • Trigger: event RideRequestCancelledByRider
  • Effect: ReleasePaymentHold(paymentId = lookupPaymentByRequest(event.requestId))
  • Realizes: REQ-PAY-021

captureOnRideCompleted

“Capture committed fare with adjustments.”

  • Trigger: event RideCompleted
  • Effect: CapturePayment(paymentId = lookupPaymentByRide(event.rideId), rideId, finalFare = FareCalculationService.computeFinalFare(...).total, fareBreakdown = FareCalculationService.computeFinalFare(...))
  • Realizes: REQ-PAY-022, REQ-PAY-NFR-002

creditDriverAfterCapture

“Credit driver net earnings after fare capture.”

  • Trigger: event PaymentCaptured
  • Effect: CreditDriverEarnings(driverId = lookupDriver(event.rideId), rideId, grossFare = event.finalFare, commission = computeCommission(event.finalFare, event.rideId), netEarnings = computeNet(event.finalFare, event.rideId))
  • Realizes: REQ-PAY-040, REQ-PAY-041, REQ-PAY-042

generateReceiptAfterCapture

“Receipt is delivered within 60s of capture.”

  • Trigger: event PaymentCaptured
  • Effect: GenerateReceipt(rideId, riderId, driverId = lookupDriver(event.rideId), paymentId, fareBreakdown = event.fareBreakdown, capturedAt = event.capturedAt)
  • Realizes: REQ-PAY-080

autoRefundOnSystemCancellation

“Full refund within 5 minutes of system-driven cancellation.”

  • Trigger: event NoDriverAvailable
  • Effect: ReleasePaymentHold(paymentId = lookupPaymentByRequest(event.requestId))
  • Realizes: REQ-PAY-060

retryOnTransientFailure

“Retry up to 3 times with exponential backoff.”

  • Trigger: event PaymentRetryAttempted
  • Guard: event.attempt < 3
  • Effect: RetryPayment(paymentId = event.paymentId)
  • Realizes: REQ-PAY-025

fallbackOnRetryExhaustion

“Fall back to next active method when retries exhausted.”

  • Trigger: event PaymentRetryExhausted
  • Effect: FallbackPaymentMethod(paymentId = event.paymentId, nextMethod = lookupNextMethod(event.paymentId))
  • Realizes: REQ-PAY-026

recordDebtOnFinalFailure

“Record debt and notify rider when all methods exhausted.”

  • Trigger: event PaymentFailed
  • Effect: PaymentNotificationService.notifyRiderOfDebt(riderId = event.riderId, amount = event.debtAmount)
  • Realizes: REQ-PAY-023

executeRefundOnApproval

“Execute refund and trigger downstream effects.”

  • Trigger: event RefundApproved
  • Effect: ExecuteRefund(refundRequestId = event.refundRequestId)
  • Realizes: REQ-PAY-063

deductDriverOnFaultRefund

“Driver-fault refund deducts from driver balance.”

  • Trigger: event RefundExecuted
  • Guard: event.faultType = driverFault
  • Effect: DeductDriverBalance(driverId = lookupDriver(event.refundRequestId), rideId = lookupRide(event.refundRequestId), amount = event.amount)
  • Realizes: REQ-PAY-064

correctReceiptAfterRefund

“Reissue corrected receipt to rider.”

  • Trigger: event RefundExecuted
  • Effect: CorrectReceipt(receiptId = lookupReceiptByPayment(event.paymentId), refundAmount = event.amount)
  • Realizes: REQ-PAY-081

completePayoutOnBankSettlement

“Mark payout complete when bank settlement confirmed.”

  • Trigger: event BankTransferCompleted
  • Effect: CompletePayout(driverId = event.driverId, payoutId = event.payoutId, amount = event.amount)
  • Realizes: REQ-PAY-045

initiateWeeklyPayouts

“Sweep eligible balances on weekly cutoff.”

  • Trigger: temporal event WeeklyPayoutCycleDue
  • Effect: PayoutBatchOrchestrator.runWeeklyBatch(market = event.market, cutoffAt = event.referenceTime)
  • Realizes: REQ-PAY-043, REQ-PAY-044

notifyDriverOfPayout

“Tell driver about scheduled payout amount and arrival.”

  • Trigger: event WeeklyPayoutInitiated
  • Effect: PaymentNotificationService.notifyDriverOfPayout(driverId = event.driverId, amount = event.amount, expectedArrival = event.expectedArrivalAt)
  • Realizes: REQ-PAY-046

resetInstantPayoutCounter

“Reset per-driver daily counters.”

  • Trigger: temporal event InstantPayoutDailyReset
  • Effect: resetInstantPayoutTotals(market = event.market)
  • Realizes: REQ-PAY-049

Traceability Matrix

Requirement Realized by
REQ-PAY-001 RiderAccount, PaymentMethodInfo, ops Add payment method and Validate stored method, events PaymentMethodAdded, PaymentMethodValidated, PaymentProcessor.zeroValueAuthorize
REQ-PAY-002 RiderAccount, op Validate stored method, event PaymentMethodValidated
REQ-PAY-003 RiderAccount (cross-context: RIDE rejects request when default method invalid)
REQ-PAY-010 FareCalculationService.computeEstimate, FareBreakdown, FareParameters
REQ-PAY-011 Payment.committedFare, op Authorize payment at request
REQ-PAY-012 FareCalculationService.computeFinalFare, Payment.finalFare, op Capture on completion
REQ-PAY-013 FareParameters.minimumFare, FareCalculationService
REQ-PAY-014 FareBreakdown (invariant componentsSumToTotal), FareCalculationService
REQ-PAY-015 FareCalculationService.computeCancellationFee
REQ-PAY-020 Payment, op Authorize payment at request, command AuthorizePayment, event PaymentAuthorized, reaction preAuthorizeOnRideRequested, PaymentProcessor.placeHold
REQ-PAY-021 op Release pre-auth on free cancellation, event PaymentHoldReleased, reaction releaseOnFreeCancellation, temporal PreAuthReleaseDeadline
REQ-PAY-022 op Capture on completion, event PaymentCaptured, reaction captureOnRideCompleted, PaymentProcessor.captureCharge
REQ-PAY-023 op Record payment failure as debt, event PaymentFailed, reaction recordDebtOnFinalFailure
REQ-PAY-024 RiderAccount.outstandingDebt, op Settle outstanding debt
REQ-PAY-025 op Retry capture after transient failure, event PaymentRetryAttempted, reaction retryOnTransientFailure
REQ-PAY-026 op Fall back to next active method, event PaymentMethodFallback, reaction fallbackOnRetryExhaustion
REQ-PAY-040 DriverBalance, op Credit driver earnings on capture, FareCalculationService.computeDriverEarnings, event DriverEarningsCredited, reaction creditDriverAfterCapture
REQ-PAY-041 op Credit driver earnings on capture (publishes within 5s), event DriverEarningsCredited
REQ-PAY-042 CommissionRate, FareCalculationService.computeDriverEarnings
REQ-PAY-043 temporal WeeklyPayoutCycleDue, op Initiate weekly payout, event WeeklyPayoutInitiated, PayoutBatchOrchestrator.runWeeklyBatch, reaction initiateWeeklyPayouts
REQ-PAY-044 op Initiate weekly payout (precondition aboveMinimum)
REQ-PAY-045 op Complete payout on bank settlement, event PayoutCompleted, reaction completePayoutOnBankSettlement, BankTransferService
REQ-PAY-046 event WeeklyPayoutInitiated, reaction notifyDriverOfPayout, PaymentNotificationService.notifyDriverOfPayout
REQ-PAY-047 op Process instant payout, event InstantPayoutProcessed, InstantPayoutNetwork.transferToDebitCard
REQ-PAY-048 op Process instant payout (expectedArrival ≤ 30 min), InstantPayoutNetwork.getEstimatedArrival
REQ-PAY-049 op Process instant payout (precondition belowDailyCap), temporal InstantPayoutDailyReset, reaction resetInstantPayoutCounter
REQ-PAY-060 reaction autoRefundOnSystemCancellation, op Process full refund, event FullRefundProcessed
REQ-PAY-061 RefundRequest, op File refund request, event RefundRequested
REQ-PAY-062 ops Approve refund / Reject refund, events RefundApproved, RefundRejected, state machine RefundRequestLifecycle
REQ-PAY-063 op Process partial refund, op Execute approved refund, events PartialRefundProcessed, RefundExecuted, reaction executeRefundOnApproval
REQ-PAY-064 op Deduct driver-fault refund from balance, event DriverBalanceDeducted, reaction deductDriverOnFaultRefund
REQ-PAY-065 (operational policy — refund issuance presents the processor’s standard arrival window)
REQ-PAY-080 Receipt, op Generate receipt within 60s of capture, event ReceiptGenerated, reaction generateReceiptAfterCapture, PaymentNotificationService.deliverReceipt
REQ-PAY-081 op Correct receipt after refund, event ReceiptCorrected, reaction correctReceiptAfterRefund
REQ-PAY-NFR-001 PaymentProcessor (PCI-DSS Level 1 boundary)
REQ-PAY-NFR-002 op Capture on completion (latency budget), event PaymentCaptured
REQ-PAY-NFR-003 PaymentProcessor idempotencyKey on placeHold/captureCharge/refund; ops Capture on completion, Process partial refund
REQ-PAY-NFR-004 append-only audit log on every authorization, capture, refund, deduction, payout (operational concern across all aggregates)
REQ-PAY-NFR-005 PaymentProcessor failover within 30s of primary outage detection
REQ-PAY-NFR-006 PayoutBatchOrchestrator.reconcileBatch, op Complete payout on bank settlement

Concrete syntax (.domain DSL)

The full .domain source that this page narrates. Entity declarations, value types, commands, events, invariants, preconditions, postconditions, services, state machines, temporal events, external events, and reactions appear in parse order.

context Payment :: "Method validation, fares, pre-auth/capture, payouts, refunds, receipts" {
    requirements-source "payment.sysreq"

    // Currency is co-owned in the Shared context (see shared.domain).

    enum PaymentStatus { authorized, captured, partiallyRefunded, fullyRefunded, failed }
    enum PaymentMethodType { creditCard, debitCard, paypal, applePay, googlePay }
    enum PaymentMethodValidationStatus { pending, valid, invalid, expired }
    enum RefundRequestStatus { pending, underReview, approved, rejected, executed }
    enum RefundFaultType { driverFault, riderFault, platformFault, disputed }
    enum PayoutType { weekly, instant }
    enum PayoutStatus { initiated, inFlight, settled, failed }
    enum ReceiptStatus { generated, corrected }

    // -----------------------------------------------------------------
    // Value types
    // -----------------------------------------------------------------
    // Amount, Distance, Duration are co-owned in the Shared context.

    value FareBreakdown {
        satisfies [REQ-PAY-014]
        fields {
            baseFare : Amount
            distanceCharge : Amount
            timeCharge : Amount
            surgeCharge : Amount
            tolls : Amount
            promotionDiscount : Amount
            cancellationFee : Amount optional
            fees : Amount
            total : Amount
        }
        invariants {
            componentsSumToTotal :: "Total equals sum of components minus discounts" enforcement reject {
                total.value = baseFare.value + distanceCharge.value + timeCharge.value + surgeCharge.value + tolls.value + fees.value - promotionDiscount.value + (cancellationFee?.value ?: 0)
            }
            currenciesAligned :: "All components share the same currency" enforcement reject {
                baseFare.currency = total.currency
                and distanceCharge.currency = total.currency
                and timeCharge.currency = total.currency
                and surgeCharge.currency = total.currency
                and tolls.currency = total.currency
                and promotionDiscount.currency = total.currency
                and fees.currency = total.currency
                and (if cancellationFee is defined { cancellationFee.currency = total.currency })
            }
        }
    }

    value FareParameters {
        fields {
            baseFare : Amount
            perKmRate : Amount
            perMinuteRate : Amount
            surgeMultiplier : decimal min(1.0) max(5.0)
            minimumFare : Amount
            market : string
        }
    }

    value CommissionRate {
        satisfies [REQ-PAY-042]
        fields { percentage : decimal min(0) max(100) }
    }

    value PaymentMethodInfo {
        satisfies [REQ-PAY-001, REQ-PAY-002]
        fields {
            paymentMethodId : uuid
            type : PaymentMethodType
            last4 : string
            isDefault : boolean
            priority : int
            validationStatus : PaymentMethodValidationStatus
            validatedAt : datetime optional
        }
    }

    value PayoutDetails {
        satisfies [REQ-PAY-043, REQ-PAY-045, REQ-PAY-047]
        fields {
            payoutId : uuid
            type : PayoutType
            amount : Amount
            fee : Amount optional
            netAmount : Amount
            initiatedAt : datetime
            expectedArrivalAt : datetime
            settledAt : datetime optional
        }
        invariants {
            netConsistency { netAmount.value = amount.value - (fee?.value ?: 0) }
        }
    }

    // -----------------------------------------------------------------
    // Entities
    // -----------------------------------------------------------------

    entity RiderAccount :: "Rider's outstanding debt and method registry" {
        satisfies [REQ-PAY-001, REQ-PAY-002, REQ-PAY-003, REQ-PAY-024, REQ-PAY-NFR-004]
        identifier accountId : UUID
        fields {
            riderId : UUID unique
            paymentMethods : list<PaymentMethodInfo>
            outstandingDebt : Amount
            createdAt : datetime
        }
        invariants {
            debtNonNegative :: "Debt is never negative" enforcement reject {
                outstandingDebt.value >= 0
            }
        }

        operations {
            "Add payment method" on AddPaymentMethod {
                satisfies [REQ-PAY-001]
                sets RiderAccount {
                    paymentMethods = RiderAccount.paymentMethods + addPaymentMethod.method
                }
                emits PaymentMethodAdded {
                    accountId = RiderAccount.accountId
                    riderId = RiderAccount.riderId
                    method = addPaymentMethod.method
                    addedAt = now()
                }
            }

            "Validate stored method (zero-value auth)" on ValidatePaymentMethod {
                satisfies [REQ-PAY-001, REQ-PAY-002]
                emits PaymentMethodValidated {
                    accountId = RiderAccount.accountId
                    paymentMethodId = validatePaymentMethod.paymentMethodId
                    status = validatePaymentMethod.outcome
                    validatedAt = now()
                }
            }

            "Settle outstanding debt" on SettleDebt {
                satisfies [REQ-PAY-024]
                precondition hasDebt { RiderAccount.outstandingDebt.value > 0 }
                sets RiderAccount {
                    outstandingDebt = Amount(value = 0, currency = RiderAccount.outstandingDebt.currency)
                }
                emits DebtSettled {
                    accountId = RiderAccount.accountId
                    settledAmount = settleDebt.amount
                    settledAt = now()
                }
            }
        }
    }

    entity Payment :: "Single ride's payment lifecycle — pre-auth → capture or refund" {
        satisfies [REQ-PAY-020, REQ-PAY-021, REQ-PAY-022, REQ-PAY-023, REQ-PAY-025, REQ-PAY-026,
                   REQ-PAY-NFR-002, REQ-PAY-NFR-003]

        identifier paymentId : UUID
        fields {
            rideRequestId : UUID
            rideId : UUID optional
            riderId : UUID
            status : PaymentStatus default("authorized")
            committedFare : Amount
            finalFare : Amount optional
            fareBreakdown : FareBreakdown optional
            paymentMethodUsed : PaymentMethodInfo
            refundedAmount : Amount
            retryCount : int min(0) default(0)
            debtRecorded : boolean default(false)
            authorizedAt : datetime
            capturedAt : datetime optional
            failedAt : datetime optional
            failureReason : string optional
            createdAt : datetime
        }

        invariants {
            refundDoesNotExceedCapture :: "Refunded amount cannot exceed final fare" enforcement reject {
                if finalFare is defined { refundedAmount.value <= finalFare.value }
            }
        }

        operations {
            "Authorize payment at request" on AuthorizePayment {
                satisfies [REQ-PAY-020]
                sets Payment {
                    rideRequestId = authorizePayment.rideRequestId
                    riderId = authorizePayment.riderId
                    committedFare = authorizePayment.committedFare
                    paymentMethodUsed = authorizePayment.method
                    refundedAmount = Amount(value = 0, currency = authorizePayment.committedFare.currency)
                    status = authorized
                    authorizedAt = now()
                    createdAt = now()
                }
                emits PaymentAuthorized {
                    paymentId = Payment.paymentId
                    rideRequestId = Payment.rideRequestId
                    riderId = Payment.riderId
                    amount = Payment.committedFare
                    authorizedAt = Payment.authorizedAt
                }
            }

            "Release pre-auth on free cancellation" on ReleasePaymentHold {
                satisfies [REQ-PAY-021, REQ-PAY-060]
                precondition isAuthorized { Payment.status = authorized }
                sets Payment { status = fullyRefunded }
                emits PaymentHoldReleased {
                    paymentId = Payment.paymentId
                    rideRequestId = Payment.rideRequestId
                    releasedAt = now()
                }
            }

            "Capture on completion" on CapturePayment {
                satisfies [REQ-PAY-022, REQ-PAY-NFR-002, REQ-PAY-NFR-003]
                precondition isAuthorized { Payment.status = authorized }
                sets Payment {
                    rideId = capturePayment.rideId
                    finalFare = capturePayment.finalFare
                    fareBreakdown = capturePayment.fareBreakdown
                    status = captured
                    capturedAt = now()
                }
                emits PaymentCaptured {
                    paymentId = Payment.paymentId
                    rideId = Payment.rideId
                    riderId = Payment.riderId
                    finalFare = Payment.finalFare
                    fareBreakdown = Payment.fareBreakdown
                    paymentMethodUsed = Payment.paymentMethodUsed
                    capturedAt = Payment.capturedAt
                }
            }

            "Retry capture after transient failure" on RetryPayment {
                satisfies [REQ-PAY-025]
                precondition retriesLeft { Payment.retryCount < 3 }
                sets Payment { retryCount = Payment.retryCount + 1 }
                emits PaymentRetryAttempted {
                    paymentId = Payment.paymentId
                    attempt = Payment.retryCount
                    attemptedAt = now()
                }
            }

            "Fall back to next active method" on FallbackPaymentMethod {
                satisfies [REQ-PAY-026]
                precondition retriesExhausted { Payment.retryCount >= 3 }
                sets Payment {
                    paymentMethodUsed = fallbackPaymentMethod.nextMethod
                    retryCount = 0
                }
                emits PaymentMethodFallback {
                    paymentId = Payment.paymentId
                    newMethod = Payment.paymentMethodUsed
                    fallbackAt = now()
                }
            }

            "Record payment failure as debt" on RecordPaymentFailure {
                satisfies [REQ-PAY-023]
                sets Payment {
                    status = failed
                    failedAt = now()
                    failureReason = recordPaymentFailure.reason
                    debtRecorded = true
                }
                emits PaymentFailed {
                    paymentId = Payment.paymentId
                    riderId = Payment.riderId
                    debtAmount = Payment.committedFare
                    reason = Payment.failureReason
                    failedAt = Payment.failedAt
                }
            }

            "Process partial refund" on ProcessPartialRefund {
                satisfies [REQ-PAY-063, REQ-PAY-NFR-003]
                precondition isCaptured { Payment.status = captured or Payment.status = partiallyRefunded }
                sets Payment {
                    refundedAmount = Amount(
                        value = Payment.refundedAmount.value + processPartialRefund.amount.value,
                        currency = Payment.refundedAmount.currency
                    )
                    status = if Payment.refundedAmount.value + processPartialRefund.amount.value >= Payment.finalFare.value then fullyRefunded else partiallyRefunded
                }
                emits PartialRefundProcessed {
                    paymentId = Payment.paymentId
                    riderId = Payment.riderId
                    amount = processPartialRefund.amount
                    faultType = processPartialRefund.faultType
                    refundedAt = now()
                }
            }

            "Process full refund" on ProcessFullRefund {
                satisfies [REQ-PAY-060, REQ-PAY-063]
                sets Payment {
                    refundedAmount = Payment.finalFare ?: Payment.committedFare
                    status = fullyRefunded
                }
                emits FullRefundProcessed {
                    paymentId = Payment.paymentId
                    riderId = Payment.riderId
                    amount = Payment.refundedAmount
                    refundedAt = now()
                }
            }
        }
    }

    entity DriverBalance :: "Driver's earnings balance and payout history" {
        satisfies [REQ-PAY-040, REQ-PAY-041, REQ-PAY-042, REQ-PAY-043, REQ-PAY-044,
                   REQ-PAY-045, REQ-PAY-046, REQ-PAY-047, REQ-PAY-048, REQ-PAY-049,
                   REQ-PAY-064, REQ-PAY-NFR-006]

        identifier balanceId : UUID
        fields {
            driverId : UUID unique
            availableBalance : Amount
            pendingBalance : Amount
            totalEarnings : Amount
            totalPayouts : Amount
            instantPayoutsTodayAmount : Amount
            instantPayoutsTodayDate : date
            lastWeeklyPayoutAt : datetime optional
            createdAt : datetime
        }

        invariants {
            balanceNonNegative :: "Available balance ≥ 0" enforcement reject {
                availableBalance.value >= 0
            }
            earningsConsistency :: "Total earnings equal balance + payouts + pending" enforcement alert {
                totalEarnings.value = availableBalance.value + totalPayouts.value + pendingBalance.value
            }
        }

        operations {
            "Credit driver earnings on capture" on CreditDriverEarnings {
                satisfies [REQ-PAY-040, REQ-PAY-041, REQ-PAY-042]
                sets DriverBalance {
                    availableBalance = Amount(
                        value = DriverBalance.availableBalance.value + creditDriverEarnings.netEarnings.value,
                        currency = DriverBalance.availableBalance.currency
                    )
                    totalEarnings = Amount(
                        value = DriverBalance.totalEarnings.value + creditDriverEarnings.netEarnings.value,
                        currency = DriverBalance.totalEarnings.currency
                    )
                }
                emits DriverEarningsCredited {
                    driverId = DriverBalance.driverId
                    rideId = creditDriverEarnings.rideId
                    grossFare = creditDriverEarnings.grossFare
                    commission = creditDriverEarnings.commission
                    netEarnings = creditDriverEarnings.netEarnings
                    creditedAt = now()
                }
            }

            "Initiate weekly payout" on InitiateWeeklyPayout {
                satisfies [REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046]
                precondition aboveMinimum {
                    DriverBalance.availableBalance.value > weeklyPayoutThreshold(DriverBalance.driverId)
                }
                sets DriverBalance {
                    pendingBalance = DriverBalance.availableBalance
                    availableBalance = Amount(value = 0, currency = DriverBalance.availableBalance.currency)
                    lastWeeklyPayoutAt = now()
                }
                emits WeeklyPayoutInitiated {
                    driverId = DriverBalance.driverId
                    amount = initiateWeeklyPayout.amount
                    expectedArrivalAt = now() + 2businessDays
                    initiatedAt = now()
                }
            }

            "Complete payout on bank settlement" on CompletePayout {
                satisfies [REQ-PAY-045, REQ-PAY-NFR-006]
                sets DriverBalance {
                    pendingBalance = Amount(value = 0, currency = DriverBalance.pendingBalance.currency)
                    totalPayouts = Amount(
                        value = DriverBalance.totalPayouts.value + completePayout.amount.value,
                        currency = DriverBalance.totalPayouts.currency
                    )
                }
                emits PayoutCompleted {
                    driverId = DriverBalance.driverId
                    payoutId = completePayout.payoutId
                    amount = completePayout.amount
                    settledAt = now()
                }
            }

            "Process instant payout" on ProcessInstantPayout {
                satisfies [REQ-PAY-047, REQ-PAY-048, REQ-PAY-049]
                precondition belowDailyCap {
                    DriverBalance.instantPayoutsTodayAmount.value + processInstantPayout.amount.value <= instantPayoutDailyCap(DriverBalance.driverId)
                }
                precondition feeAndArrivalQuoted {
                    processInstantPayout.feeQuoted is defined
                    and processInstantPayout.expectedArrival is defined
                }
                sets DriverBalance {
                    availableBalance = Amount(
                        value = DriverBalance.availableBalance.value - processInstantPayout.amount.value,
                        currency = DriverBalance.availableBalance.currency
                    )
                    instantPayoutsTodayAmount = Amount(
                        value = DriverBalance.instantPayoutsTodayAmount.value + processInstantPayout.amount.value,
                        currency = DriverBalance.instantPayoutsTodayAmount.currency
                    )
                    totalPayouts = Amount(
                        value = DriverBalance.totalPayouts.value + processInstantPayout.amount.value - processInstantPayout.feeQuoted.value,
                        currency = DriverBalance.totalPayouts.currency
                    )
                }
                emits InstantPayoutProcessed {
                    driverId = DriverBalance.driverId
                    amount = processInstantPayout.amount
                    fee = processInstantPayout.feeQuoted
                    expectedArrival = processInstantPayout.expectedArrival
                    initiatedAt = now()
                }
            }

            "Deduct driver-fault refund from balance" on DeductDriverBalance {
                satisfies [REQ-PAY-064]
                sets DriverBalance {
                    availableBalance = Amount(
                        value = DriverBalance.availableBalance.value - deductDriverBalance.amount.value,
                        currency = DriverBalance.availableBalance.currency
                    )
                }
                emits DriverBalanceDeducted {
                    driverId = DriverBalance.driverId
                    rideId = deductDriverBalance.rideId
                    amount = deductDriverBalance.amount
                    reason = "driver-fault refund"
                    deductedAt = now()
                }
            }
        }
    }

    entity RefundRequest :: "Rider-filed refund dispute" {
        satisfies [REQ-PAY-061, REQ-PAY-062, REQ-PAY-063, REQ-PAY-064, REQ-PAY-065]

        identifier refundRequestId : UUID
        fields {
            rideId : UUID
            riderId : UUID
            paymentId : UUID
            driverId : UUID
            requestedAmount : Amount
            approvedAmount : Amount optional
            status : RefundRequestStatus default("pending")
            faultType : RefundFaultType optional
            reason : string maxLength(2000)
            reviewerNotes : string optional
            requestedAt : datetime
            reviewedAt : datetime optional
            executedAt : datetime optional
        }

        operations {
            "File refund request" on RequestRefund {
                satisfies [REQ-PAY-061]
                sets RefundRequest {
                    rideId = requestRefund.rideId
                    riderId = requestRefund.riderId
                    paymentId = requestRefund.paymentId
                    driverId = requestRefund.driverId
                    requestedAmount = requestRefund.amount
                    reason = requestRefund.reason
                    status = pending
                    requestedAt = now()
                }
                emits RefundRequested {
                    refundRequestId = RefundRequest.refundRequestId
                    rideId = RefundRequest.rideId
                    riderId = RefundRequest.riderId
                    requestedAt = RefundRequest.requestedAt
                }
            }

            "Approve refund" on ApproveRefund {
                satisfies [REQ-PAY-062, REQ-PAY-063]
                precondition pendingOrUnderReview {
                    RefundRequest.status = pending or RefundRequest.status = underReview
                }
                sets RefundRequest {
                    status = approved
                    approvedAmount = approveRefund.approvedAmount
                    faultType = approveRefund.faultType
                    reviewerNotes = approveRefund.notes
                    reviewedAt = now()
                }
                emits RefundApproved {
                    refundRequestId = RefundRequest.refundRequestId
                    rideId = RefundRequest.rideId
                    riderId = RefundRequest.riderId
                    driverId = RefundRequest.driverId
                    approvedAmount = RefundRequest.approvedAmount
                    faultType = RefundRequest.faultType
                    reviewedAt = RefundRequest.reviewedAt
                }
            }

            "Reject refund" on RejectRefund {
                satisfies [REQ-PAY-062]
                precondition pendingOrUnderReview {
                    RefundRequest.status = pending or RefundRequest.status = underReview
                }
                sets RefundRequest {
                    status = rejected
                    reviewerNotes = rejectRefund.notes
                    reviewedAt = now()
                }
                emits RefundRejected {
                    refundRequestId = RefundRequest.refundRequestId
                    rejectedAt = RefundRequest.reviewedAt
                }
            }

            "Execute approved refund" on ExecuteRefund {
                satisfies [REQ-PAY-063]
                precondition approved { RefundRequest.status = approved }
                sets RefundRequest { status = executed; executedAt = now() }
                emits RefundExecuted {
                    refundRequestId = RefundRequest.refundRequestId
                    paymentId = RefundRequest.paymentId
                    amount = RefundRequest.approvedAmount
                    faultType = RefundRequest.faultType
                    executedAt = RefundRequest.executedAt
                }
            }
        }
    }

    entity Receipt :: "Per-ride receipt; corrected on refund" {
        satisfies [REQ-PAY-080, REQ-PAY-081]
        identifier receiptId : UUID
        fields {
            rideId : UUID
            riderId : UUID
            driverId : UUID
            paymentId : UUID
            fareBreakdown : FareBreakdown
            originalTotal : Amount
            adjustedTotal : Amount optional
            refundAmount : Amount
            status : ReceiptStatus default("generated")
            generatedAt : datetime
            correctedAt : datetime optional
        }

        operations {
            "Generate receipt within 60s of capture" on GenerateReceipt {
                satisfies [REQ-PAY-080]
                sets Receipt {
                    rideId = generateReceipt.rideId
                    riderId = generateReceipt.riderId
                    driverId = generateReceipt.driverId
                    paymentId = generateReceipt.paymentId
                    fareBreakdown = generateReceipt.fareBreakdown
                    originalTotal = generateReceipt.fareBreakdown.total
                    refundAmount = Amount(value = 0, currency = generateReceipt.fareBreakdown.total.currency)
                    status = generated
                    generatedAt = now()
                }
                postcondition deliveredWithin60s :: "Receipt is rendered to the rider app within 60s of capture" {
                    now() - generateReceipt.capturedAt <= 60s
                }
                emits ReceiptGenerated {
                    receiptId = Receipt.receiptId
                    rideId = Receipt.rideId
                    riderId = Receipt.riderId
                    total = Receipt.originalTotal
                    generatedAt = Receipt.generatedAt
                }
            }

            "Correct receipt after refund" on CorrectReceipt {
                satisfies [REQ-PAY-081]
                sets Receipt {
                    refundAmount = correctReceipt.refundAmount
                    adjustedTotal = Amount(
                        value = Receipt.originalTotal.value - correctReceipt.refundAmount.value,
                        currency = Receipt.originalTotal.currency
                    )
                    status = corrected
                    correctedAt = now()
                }
                emits ReceiptCorrected {
                    receiptId = Receipt.receiptId
                    rideId = Receipt.rideId
                    refundAmount = Receipt.refundAmount
                    adjustedTotal = Receipt.adjustedTotal
                    correctedAt = Receipt.correctedAt
                }
            }
        }
    }

    aggregate PaymentAggregate root Payment
    aggregate DriverBalanceAggregate root DriverBalance
    aggregate RefundRequestAggregate root RefundRequest
    aggregate ReceiptAggregate root Receipt
    aggregate RiderAccountAggregate root RiderAccount

    statemachine PaymentLifecycle on Payment {
        start authorized
        state authorized {}
        state captured {}
        state partiallyRefunded {}
        state fullyRefunded {}
        state failed {}

        transition authorized -> captured on CapturePayment
        transition authorized -> fullyRefunded on ReleasePaymentHold
        transition authorized -> failed on RecordPaymentFailure
        transition captured -> partiallyRefunded on ProcessPartialRefund
        transition captured -> fullyRefunded on ProcessFullRefund
        transition partiallyRefunded -> fullyRefunded on ProcessFullRefund
        transition partiallyRefunded -> partiallyRefunded on ProcessPartialRefund

        final fullyRefunded
        final failed
    }

    statemachine RefundRequestLifecycle on RefundRequest {
        start pending
        state pending {}
        state underReview {}
        state approved {}
        state rejected {}
        state executed {}
        transition pending -> underReview on BeginRefundReview
        transition pending -> approved on ApproveRefund
        transition underReview -> approved on ApproveRefund
        transition underReview -> rejected on RejectRefund
        transition approved -> executed on ExecuteRefund
        final rejected
        final executed
    }

    statemachine ReceiptLifecycle on Receipt {
        start generated
        state generated {}
        state corrected {}
        transition generated -> corrected on CorrectReceipt
        transition corrected -> corrected on CorrectReceipt
    }

    // -----------------------------------------------------------------
    // Domain services
    // -----------------------------------------------------------------

    service FareCalculationService :: "Compute estimates, final fares, cancellation fees, driver earnings" {
        satisfies [REQ-PAY-010, REQ-PAY-011, REQ-PAY-012, REQ-PAY-013, REQ-PAY-014, REQ-PAY-015,
                   REQ-PAY-040, REQ-PAY-042]
        operations {
            computeEstimate(pickup: any, dropoff: any, params: FareParameters) : FareBreakdown
            computeFinalFare(rideId: uuid, actualDistance: Distance, actualDuration: Duration, params: FareParameters) : FareBreakdown
            computeCancellationFee(rideId: uuid, minutesSinceAssignment: int, params: FareParameters) : Amount
            computeDriverEarnings(grossFare: Amount, commission: CommissionRate) : Amount
        }
    }

    service PayoutBatchOrchestrator :: "Mondays at 00:00 local market time — sweep balances, initiate payouts" {
        satisfies [REQ-PAY-043, REQ-PAY-044, REQ-PAY-045, REQ-PAY-046, REQ-PAY-NFR-006]
        operations {
            runWeeklyBatch(market: string, cutoffAt: datetime) : void
            reconcileBatch(batchId: uuid) : void
        }
    }

    service FraudScoringService {
        satisfies [REQ-NFR-031]
        operations {
            scoreAuthorization(payment: Payment) : decimal
            scoreInstantPayout(driverId: uuid, amount: Amount) : decimal
        }
    }

    // -----------------------------------------------------------------
    // Infrastructure services
    // -----------------------------------------------------------------

    infrastructure-service PaymentProcessor :: "PCI-DSS Level 1 certified processor (primary + failover)" {
        satisfies [REQ-PAY-NFR-001, REQ-PAY-NFR-005, REQ-NFR-030]
        operations {
            zeroValueAuthorize(method: PaymentMethodInfo) : PaymentMethodValidationStatus
            placeHold(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
            captureCharge(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
            releaseHold(method: PaymentMethodInfo, holdId: uuid) : void
            refund(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : void
        }
    }

    infrastructure-service BankTransferService :: "Bank ACH/SEPA payouts to driver accounts" {
        satisfies [REQ-PAY-043, REQ-PAY-045, REQ-PAY-NFR-006]
        operations {
            transferToBank(bankAccount: any, amount: Amount, idempotencyKey: uuid) : uuid
            getTransferStatus(transferId: uuid) : PayoutStatus
        }
    }

    infrastructure-service InstantPayoutNetwork :: "Push-to-card debit-card instant transfer" {
        satisfies [REQ-PAY-047, REQ-PAY-048]
        operations {
            transferToDebitCard(card: any, amount: Amount, idempotencyKey: uuid) : uuid
            getQuotedFee(card: any, amount: Amount) : Amount
            getEstimatedArrival(card: any) : Duration
        }
    }

    infrastructure-service PaymentNotificationService {
        satisfies [REQ-PAY-023, REQ-PAY-046, REQ-PAY-080]
        operations {
            notifyRiderOfDebt(riderId: uuid, amount: Amount) : void
            notifyDriverOfPayout(driverId: uuid, amount: Amount, expectedArrival: datetime) : void
            deliverReceipt(riderId: uuid, receiptId: uuid) : void
        }
    }

    // -----------------------------------------------------------------
    // Commands
    // -----------------------------------------------------------------

    command AddPaymentMethod { fields { accountId: uuid; riderId: uuid; method: PaymentMethodInfo; correlationId: uuid } }
    command ValidatePaymentMethod { fields { accountId: uuid; paymentMethodId: uuid; outcome: PaymentMethodValidationStatus; correlationId: uuid } }
    command SettleDebt { fields { accountId: uuid; amount: Amount; correlationId: uuid } }
    command AuthorizePayment { fields { rideRequestId: uuid; riderId: uuid; committedFare: Amount; method: PaymentMethodInfo; correlationId: uuid } }
    command ReleasePaymentHold { fields { paymentId: uuid; correlationId: uuid } }
    command CapturePayment { fields { paymentId: uuid; rideId: uuid; finalFare: Amount; fareBreakdown: FareBreakdown; correlationId: uuid } }
    command RetryPayment { fields { paymentId: uuid; correlationId: uuid } }
    command FallbackPaymentMethod { fields { paymentId: uuid; nextMethod: PaymentMethodInfo; correlationId: uuid } }
    command RecordPaymentFailure { fields { paymentId: uuid; reason: string; correlationId: uuid } }
    command ProcessPartialRefund { fields { paymentId: uuid; amount: Amount; faultType: RefundFaultType; correlationId: uuid } }
    command ProcessFullRefund { fields { paymentId: uuid; correlationId: uuid } }
    command CreditDriverEarnings { fields { driverId: uuid; rideId: uuid; grossFare: Amount; commission: Amount; netEarnings: Amount; correlationId: uuid } }
    command InitiateWeeklyPayout { fields { driverId: uuid; amount: Amount; correlationId: uuid } }
    command CompletePayout { fields { driverId: uuid; payoutId: uuid; amount: Amount; correlationId: uuid } }
    command ProcessInstantPayout { fields { driverId: uuid; amount: Amount; feeQuoted: Amount; expectedArrival: datetime; correlationId: uuid } }
    command DeductDriverBalance { fields { driverId: uuid; rideId: uuid; amount: Amount; correlationId: uuid } }
    command RequestRefund { fields { rideId: uuid; riderId: uuid; paymentId: uuid; driverId: uuid; amount: Amount; reason: string; correlationId: uuid } }
    command BeginRefundReview { fields { refundRequestId: uuid; correlationId: uuid } }
    command ApproveRefund { fields { refundRequestId: uuid; approvedAmount: Amount; faultType: RefundFaultType; notes: string; correlationId: uuid } }
    command RejectRefund { fields { refundRequestId: uuid; notes: string; correlationId: uuid } }
    command ExecuteRefund { fields { refundRequestId: uuid; correlationId: uuid } }
    command GenerateReceipt { fields { rideId: uuid; riderId: uuid; driverId: uuid; paymentId: uuid; fareBreakdown: FareBreakdown; capturedAt: datetime; correlationId: uuid } }
    command CorrectReceipt { fields { receiptId: uuid; refundAmount: Amount; correlationId: uuid } }

    // -----------------------------------------------------------------
    // Events — internal
    // -----------------------------------------------------------------

    event PaymentMethodAdded { satisfies [REQ-PAY-001] fields { accountId: uuid; riderId: uuid; method: PaymentMethodInfo; addedAt: datetime } }
    event PaymentMethodValidated { satisfies [REQ-PAY-001, REQ-PAY-002] fields { accountId: uuid; paymentMethodId: uuid; status: PaymentMethodValidationStatus; validatedAt: datetime } }
    event DebtSettled { satisfies [REQ-PAY-024] fields { accountId: uuid; settledAmount: Amount; settledAt: datetime } }
    event PaymentAuthorized { satisfies [REQ-PAY-020] fields { paymentId: uuid; rideRequestId: uuid; riderId: uuid; amount: Amount; authorizedAt: datetime } }
    event PaymentHoldReleased { satisfies [REQ-PAY-021] fields { paymentId: uuid; rideRequestId: uuid; releasedAt: datetime } }
    event PaymentCaptured { satisfies [REQ-PAY-022, REQ-PAY-NFR-002] fields { paymentId: uuid; rideId: uuid; riderId: uuid; finalFare: Amount; fareBreakdown: FareBreakdown; paymentMethodUsed: PaymentMethodInfo; capturedAt: datetime } }
    event PaymentRetryAttempted { satisfies [REQ-PAY-025] fields { paymentId: uuid; attempt: int; attemptedAt: datetime } }
    event PaymentRetryExhausted { satisfies [REQ-PAY-025] fields { paymentId: uuid; exhaustedAt: datetime } }
    event PaymentMethodFallback { satisfies [REQ-PAY-026] fields { paymentId: uuid; newMethod: PaymentMethodInfo; fallbackAt: datetime } }
    event PaymentFailed { satisfies [REQ-PAY-023] fields { paymentId: uuid; riderId: uuid; debtAmount: Amount; reason: string; failedAt: datetime } }
    event PartialRefundProcessed { satisfies [REQ-PAY-063] fields { paymentId: uuid; riderId: uuid; amount: Amount; faultType: RefundFaultType; refundedAt: datetime } }
    event FullRefundProcessed { satisfies [REQ-PAY-060, REQ-PAY-063] fields { paymentId: uuid; riderId: uuid; amount: Amount; refundedAt: datetime } }
    event DriverEarningsCredited { satisfies [REQ-PAY-040, REQ-PAY-041] fields { driverId: uuid; rideId: uuid; grossFare: Amount; commission: Amount; netEarnings: Amount; creditedAt: datetime } }
    event WeeklyPayoutInitiated { satisfies [REQ-PAY-043, REQ-PAY-046] fields { driverId: uuid; amount: Amount; expectedArrivalAt: datetime; initiatedAt: datetime } }
    event PayoutCompleted { satisfies [REQ-PAY-045] fields { driverId: uuid; payoutId: uuid; amount: Amount; settledAt: datetime } }
    event InstantPayoutProcessed { satisfies [REQ-PAY-047, REQ-PAY-048, REQ-PAY-049] fields { driverId: uuid; amount: Amount; fee: Amount; expectedArrival: datetime; initiatedAt: datetime } }
    event DriverBalanceDeducted { satisfies [REQ-PAY-064] fields { driverId: uuid; rideId: uuid; amount: Amount; reason: string; deductedAt: datetime } }
    event RefundRequested { satisfies [REQ-PAY-061] fields { refundRequestId: uuid; rideId: uuid; riderId: uuid; requestedAt: datetime } }
    event RefundApproved { satisfies [REQ-PAY-062, REQ-PAY-063] fields { refundRequestId: uuid; rideId: uuid; riderId: uuid; driverId: uuid; approvedAmount: Amount; faultType: RefundFaultType; reviewedAt: datetime } }
    event RefundRejected { satisfies [REQ-PAY-062] fields { refundRequestId: uuid; rejectedAt: datetime } }
    event RefundExecuted { satisfies [REQ-PAY-063] fields { refundRequestId: uuid; paymentId: uuid; amount: Amount; faultType: RefundFaultType; executedAt: datetime } }
    event ReceiptGenerated { satisfies [REQ-PAY-080] fields { receiptId: uuid; rideId: uuid; riderId: uuid; total: Amount; generatedAt: datetime } }
    event ReceiptCorrected { satisfies [REQ-PAY-081] fields { receiptId: uuid; rideId: uuid; refundAmount: Amount; adjustedTotal: Amount; correctedAt: datetime } }

    // -----------------------------------------------------------------
    // Temporal events
    // -----------------------------------------------------------------

    temporal-event WeeklyPayoutCycleDue :: "Recurring weekly payout cutoff (Mondays 00:00 local market time)" {
        satisfies [REQ-PAY-043]
        recurring "0 0 * * MON" per-market
    }

    temporal-event PreAuthReleaseDeadline :: "Released within 1 business day of free cancellation" {
        satisfies [REQ-PAY-021]
        relative-to PaymentHoldReleased offset 1businessDay
        guard not (Payment.status = fullyRefunded)
        // alert if hold not actually released by processor by deadline
    }

    temporal-event InstantPayoutDailyReset :: "Reset per-driver instant-payout running total at local midnight" {
        satisfies [REQ-PAY-049]
        recurring "0 0 * * *" per-market
    }

    // -----------------------------------------------------------------
    // External events (consumed)
    // -----------------------------------------------------------------

    external-event RideRequested from RideManagement
    external-event RideRequestCancelledByRider from RideManagement
    external-event RideCompleted from RideManagement
    external-event RideCancelledByRider from RideManagement
    external-event RideCancelledRiderNoShow from RideManagement
    external-event NoDriverAvailable from RideManagement

    // -----------------------------------------------------------------
    // Reactions
    // -----------------------------------------------------------------

    reaction preAuthorizeOnRideRequested :: "Place hold on ride request" {
        satisfies [REQ-PAY-020]
        trigger RideRequested
        effect AuthorizePayment(
            rideRequestId = event.requestId,
            riderId = event.riderId,
            committedFare = Amount(value = event.committedFare.amount.value, currency = event.committedFare.amount.currency),
            method = lookupDefaultMethod(event.riderId)
        )
    }

    reaction releaseOnFreeCancellation :: "Release hold within 1 business day of rider cancel before assignment" {
        satisfies [REQ-PAY-021]
        trigger RideRequestCancelledByRider
        effect ReleasePaymentHold(paymentId = lookupPaymentByRequest(event.requestId))
    }

    reaction captureOnRideCompleted :: "Capture committed fare with adjustments" {
        satisfies [REQ-PAY-022, REQ-PAY-NFR-002]
        trigger RideCompleted
        effect CapturePayment(
            paymentId = lookupPaymentByRide(event.rideId),
            rideId = event.rideId,
            finalFare = FareCalculationService.computeFinalFare(event.rideId, event.actualDistance, event.actualDuration, marketParams(event.rideId)).total,
            fareBreakdown = FareCalculationService.computeFinalFare(event.rideId, event.actualDistance, event.actualDuration, marketParams(event.rideId))
        )
    }

    reaction creditDriverAfterCapture :: "Credit driver net earnings after fare capture" {
        satisfies [REQ-PAY-040, REQ-PAY-041, REQ-PAY-042]
        trigger PaymentCaptured
        effect CreditDriverEarnings(
            driverId = lookupDriver(event.rideId),
            rideId = event.rideId,
            grossFare = event.finalFare,
            commission = computeCommission(event.finalFare, event.rideId),
            netEarnings = computeNet(event.finalFare, event.rideId)
        )
    }

    reaction generateReceiptAfterCapture :: "Receipt is delivered within 60s of capture" {
        satisfies [REQ-PAY-080]
        trigger PaymentCaptured
        effect GenerateReceipt(
            rideId = event.rideId,
            riderId = event.riderId,
            driverId = lookupDriver(event.rideId),
            paymentId = event.paymentId,
            fareBreakdown = event.fareBreakdown,
            capturedAt = event.capturedAt
        )
    }

    reaction autoRefundOnSystemCancellation :: "Full refund within 5 minutes of system-driven cancellation" {
        satisfies [REQ-PAY-060]
        trigger NoDriverAvailable
        effect ReleasePaymentHold(paymentId = lookupPaymentByRequest(event.requestId))
    }

    reaction retryOnTransientFailure :: "Retry up to 3 times with exponential backoff" {
        satisfies [REQ-PAY-025]
        trigger PaymentRetryAttempted
        guard event.attempt < 3
        effect RetryPayment(paymentId = event.paymentId)
    }

    reaction fallbackOnRetryExhaustion :: "Fall back to next active method when retries exhausted" {
        satisfies [REQ-PAY-026]
        trigger PaymentRetryExhausted
        effect FallbackPaymentMethod(paymentId = event.paymentId, nextMethod = lookupNextMethod(event.paymentId))
    }

    reaction recordDebtOnFinalFailure :: "Record debt and notify rider when all methods exhausted" {
        satisfies [REQ-PAY-023]
        trigger PaymentFailed
        effect PaymentNotificationService.notifyRiderOfDebt(
            riderId = event.riderId,
            amount = event.debtAmount
        )
    }

    reaction executeRefundOnApproval :: "Execute refund and trigger downstream effects" {
        satisfies [REQ-PAY-063]
        trigger RefundApproved
        effect ExecuteRefund(refundRequestId = event.refundRequestId)
    }

    reaction deductDriverOnFaultRefund :: "Driver-fault refund deducts from driver balance" {
        satisfies [REQ-PAY-064]
        trigger RefundExecuted
        guard event.faultType = driverFault
        effect DeductDriverBalance(
            driverId = lookupDriver(event.refundRequestId),
            rideId = lookupRide(event.refundRequestId),
            amount = event.amount
        )
    }

    reaction correctReceiptAfterRefund :: "Reissue corrected receipt to rider" {
        satisfies [REQ-PAY-081]
        trigger RefundExecuted
        effect CorrectReceipt(
            receiptId = lookupReceiptByPayment(event.paymentId),
            refundAmount = event.amount
        )
    }

    reaction completePayoutOnBankSettlement :: "Mark payout complete when bank settlement confirmed" {
        satisfies [REQ-PAY-045]
        trigger BankTransferCompleted
        effect CompletePayout(
            driverId = event.driverId,
            payoutId = event.payoutId,
            amount = event.amount
        )
    }

    reaction initiateWeeklyPayouts :: "Sweep eligible balances on weekly cutoff" {
        satisfies [REQ-PAY-043, REQ-PAY-044]
        trigger WeeklyPayoutCycleDue
        effect PayoutBatchOrchestrator.runWeeklyBatch(market = event.market, cutoffAt = event.referenceTime)
    }

    reaction notifyDriverOfPayout :: "Tell driver about scheduled payout amount and arrival" {
        satisfies [REQ-PAY-046]
        trigger WeeklyPayoutInitiated
        effect PaymentNotificationService.notifyDriverOfPayout(
            driverId = event.driverId,
            amount = event.amount,
            expectedArrival = event.expectedArrivalAt
        )
    }

    reaction resetInstantPayoutCounter :: "Reset per-driver daily counters" {
        satisfies [REQ-PAY-049]
        trigger InstantPayoutDailyReset
        effect resetInstantPayoutTotals(market = event.market)
    }

    // -----------------------------------------------------------------
    // External events (driven by infrastructure callbacks)
    // -----------------------------------------------------------------

    external-event BankTransferCompleted from BankTransferService
}