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, … throughzwg).Currency— a value type that bundles aCurrenciescode with the ISO 4217 reference data:numericCode(e.g. 840 for USD) anddefaultFractionDigits(the minor-unit precision used to render fare totals on receipts). Modeled onjava.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:AmountdistanceCharge:AmounttimeCharge:AmountsurgeCharge:Amounttolls:AmountpromotionDiscount:AmountcancellationFee:Amountoptionalfees:Amounttotal: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 sharestotal.currency— enforcement: reject. Mixing currencies inside one breakdown is structurally impossible.
- Realizes: REQ-PAY-014.
FareParameters
Per-market fare engine inputs.
- Fields:
baseFare:AmountperKmRate:AmountperMinuteRate:AmountsurgeMultiplier: Decimal —min(1.0),max(5.0)minimumFare:Amountmarket: String
CommissionRate
- Fields:
percentage: Decimal —min(0),max(100)
- Realizes: REQ-PAY-042.
PaymentMethodInfo
- Fields:
paymentMethodId: UUIDtype:PaymentMethodTypelast4: StringisDefault: Booleanpriority: IntegervalidationStatus:PaymentMethodValidationStatusvalidatedAt: DateTime optional
- Realizes: REQ-PAY-001, REQ-PAY-002.
PayoutDetails
- Fields:
payoutId: UUIDtype:PayoutTypeamount:Amountfee:AmountoptionalnetAmount:AmountinitiatedAt: DateTimeexpectedArrivalAt: DateTimesettledAt: 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 uniquepaymentMethods:list<PaymentMethodInfo>outstandingDebt:Amount— default0createdAt: DateTime
- Invariants:
debtNonNegative:outstandingDebt.amount >= 0— enforcement: reject.
- Operations:
Add payment methodon commandAddPaymentMethod- Realizes: REQ-PAY-001
- State change:
paymentMethods = paymentMethods + addPaymentMethod.method. - Emits:
PaymentMethodAdded.
Validate stored method (zero-value auth)on commandValidatePaymentMethod- Realizes: REQ-PAY-001, REQ-PAY-002
- Emits:
PaymentMethodValidatedwith the processor outcome.
Settle outstanding debton commandSettleDebt- 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: UUIDrideId: UUID optionalriderId: UUIDstatus:PaymentStatus— defaultauthorizedcommittedFare:AmountfinalFare:AmountoptionalfareBreakdown:FareBreakdownoptionalpaymentMethodUsed:PaymentMethodInforefundedAmount:Amount— default0retryCount: Integer —min(0), default0debtRecorded: Boolean — defaultfalseauthorizedAt: DateTimecapturedAt: DateTime optionalfailedAt: DateTime optionalfailureReason: String optionalcreatedAt: DateTime
- Invariants:
refundDoesNotExceedCapture: whenfinalFareis defined,refundedAmount.amount <= finalFare.amount— enforcement: reject.
- Operations:
Authorize payment at requestonAuthorizePayment- Realizes: REQ-PAY-020
- State changes:
rideRequestId,riderId,committedFare,paymentMethodUsed,status = authorized,authorizedAt = now(),createdAt = now(). - Emits:
PaymentAuthorized.
Release pre-auth on free cancellationonReleasePaymentHold- Realizes: REQ-PAY-021, REQ-PAY-060
- Precondition
isAuthorized:Payment.status = authorized. - State change:
status = fullyRefunded. - Emits:
PaymentHoldReleased.
Capture on completiononCapturePayment- 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 failureonRetryPayment- Realizes: REQ-PAY-025
- Precondition
retriesLeft:retryCount < 3. - State change:
retryCount = retryCount + 1. - Emits:
PaymentRetryAttempted.
Fall back to next active methodonFallbackPaymentMethod- Realizes: REQ-PAY-026
- Precondition
retriesExhausted:retryCount >= 3. - State changes:
paymentMethodUsed = nextMethod,retryCount = 0. - Emits:
PaymentMethodFallback.
Record payment failure as debtonRecordPaymentFailure- Realizes: REQ-PAY-023
- State changes:
status = failed,failedAt = now(),failureReason,debtRecorded = true. - Emits:
PaymentFailed.
Process partial refundonProcessPartialRefund- Realizes: REQ-PAY-063, REQ-PAY-NFR-003
- Precondition
isCaptured:status = capturedorstatus = partiallyRefunded. - State changes:
refundedAmount += amount;status = fullyRefundedif total refund ≥ final fare, elsepartiallyRefunded. - Emits:
PartialRefundProcessed.
Process full refundonProcessFullRefund- 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 uniqueavailableBalance:Amount— default0pendingBalance:Amount— default0totalEarnings:Amount— default0totalPayouts:Amount— default0instantPayoutsTodayAmount:Amount— default0instantPayoutsTodayDate: DatelastWeeklyPayoutAt: DateTime optionalcreatedAt: DateTime
- Invariants:
balanceNonNegative:availableBalance.amount >= 0— enforcement: reject.earningsConsistency:totalEarnings = availableBalance + totalPayouts + pendingBalance— enforcement: alert.
- Operations:
Credit driver earnings on captureonCreditDriverEarnings- Realizes: REQ-PAY-040, REQ-PAY-041, REQ-PAY-042
- State changes:
availableBalance += netEarnings,totalEarnings += netEarnings. - Emits:
DriverEarningsCredited.
Initiate weekly payoutonInitiateWeeklyPayout- 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:
WeeklyPayoutInitiatedwithexpectedArrivalAt = now() + 2 business days.
Complete payout on bank settlementonCompletePayout- Realizes: REQ-PAY-045, REQ-PAY-NFR-006
- State changes:
pendingBalance = 0,totalPayouts += amount. - Emits:
PayoutCompleted.
Process instant payoutonProcessInstantPayout- Realizes: REQ-PAY-047, REQ-PAY-048, REQ-PAY-049
- Preconditions:
belowDailyCap:instantPayoutsTodayAmount + amount <= instantPayoutDailyCap(driverId).feeAndArrivalQuoted: bothfeeQuotedandexpectedArrivalare defined.
- State changes:
availableBalance -= amount,instantPayoutsTodayAmount += amount,totalPayouts += amount - feeQuoted. - Emits:
InstantPayoutProcessed.
Deduct driver-fault refund from balanceonDeductDriverBalance- Realizes: REQ-PAY-064
- State change:
availableBalance -= deductDriverBalance.amount. - Emits:
DriverBalanceDeductedwith 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: UUIDriderId: UUIDpaymentId: UUIDdriverId: UUIDrequestedAmount:AmountapprovedAmount:Amountoptionalstatus:RefundRequestStatus— defaultpendingfaultType:RefundFaultTypeoptionalreason: String —maxLength(2000)reviewerNotes: String optionalrequestedAt: DateTimereviewedAt: DateTime optionalexecutedAt: DateTime optional
- Operations:
File refund requestonRequestRefund- Realizes: REQ-PAY-061
- State changes: capture all request fields,
status = pending,requestedAt = now(). - Emits:
RefundRequested.
Approve refundonApproveRefund- 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 refundonRejectRefund- Realizes: REQ-PAY-062
- Precondition
pendingOrUnderReview:status = pending or status = underReview. - State changes:
status = rejected,reviewerNotes,reviewedAt = now(). - Emits:
RefundRejected.
Execute approved refundonExecuteRefund- 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: UUIDriderId: UUIDdriverId: UUIDpaymentId: UUIDfareBreakdown:FareBreakdownoriginalTotal:AmountadjustedTotal:AmountoptionalrefundAmount:Amount— default0status:ReceiptStatus— defaultgeneratedgeneratedAt: DateTimecorrectedAt: DateTime optional
- Operations:
Generate receipt within 60s of captureonGenerateReceipt- 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 refundonCorrectReceipt- 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:
authorized→capturedonCapturePaymentauthorized→fullyRefundedonReleasePaymentHoldauthorized→failedonRecordPaymentFailurecaptured→partiallyRefundedonProcessPartialRefundcaptured→fullyRefundedonProcessFullRefundpartiallyRefunded→fullyRefundedonProcessFullRefundpartiallyRefunded→partiallyRefundedonProcessPartialRefund
- Final states:
fullyRefunded,failed.
RefundRequestLifecycle on RefundRequest
- Start state:
pending. - States:
pending,underReview,approved,rejected,executed. - Transitions:
pending→underReviewonBeginRefundReviewpending→approvedonApproveRefundunderReview→approvedonApproveRefundunderReview→rejectedonRejectRefundapproved→executedonExecuteRefund
- Final states:
rejected,executed.
ReceiptLifecycle on Receipt
- Start state:
generated. - States:
generated,corrected. - Transitions:
generated→correctedonCorrectReceiptcorrected→correctedonCorrectReceipt
Domain Services
FareCalculationService
Compute estimates, final fares, cancellation fees, and driver earnings.
- Operations:
computeEstimate(pickup, dropoff, params: FareParameters) : FareBreakdowncomputeFinalFare(rideId: uuid, actualDistance: Distance, actualDuration: Duration, params: FareParameters) : FareBreakdowncomputeCancellationFee(rideId: uuid, minutesSinceAssignment: int, params: FareParameters) : AmountcomputeDriverEarnings(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) : voidreconcileBatch(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) : decimalscoreInstantPayout(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) : PaymentMethodValidationStatusplaceHold(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : voidcaptureCharge(method: PaymentMethodInfo, amount: Amount, idempotencyKey: uuid) : voidreleaseHold(method: PaymentMethodInfo, holdId: uuid) : voidrefund(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) : uuidgetTransferStatus(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) : uuidgetQuotedFee(card, amount: Amount) : AmountgetEstimatedArrival(card) : Duration
- Realizes: REQ-PAY-047, REQ-PAY-048.
PaymentNotificationService
Rider debt notifications, driver payout notifications, receipt delivery.
- Operations:
notifyRiderOfDebt(riderId: uuid, amount: Amount) : voidnotifyDriverOfPayout(driverId: uuid, amount: Amount, expectedArrival: datetime) : voiddeliverReceipt(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
}