PaymentRunStatus
Owner context: partners | Entity: PaymentRun | Field: status | Values: 4
The lifecycle of a payout cycle: grouping many affiliate Dues for one partner, rating them against current market prices, rebalancing funds via CEX trades, and dispatching a blockchain transfer to the partner. Forward-only, no failure states. Drives the child state machines DueStatus (every Due in the run) and PaymentRunTradeStatus (every trade it triggers).
The enum carries a known refactor signal: withdrawing has a // todo: remove this comment in source (PaymentRunStatus.ts:8). The intent is to collapse the rated → withdrawing → paid segment, likely because withdrawing is effectively "after rated, before paid observed" and nothing distinguishes it from the implicit work already happening during rating. Document what the code does today; expect this state to disappear in a future simplification.
States
| State | Terminal | Description | Entry condition |
|---|---|---|---|
created | No | Run exists; no rates quoted yet | PaymentRunRateQuoter.findOrCreatePaymentRun() |
rated | No | Rates quoted for every coin in the payout; ready to initiate the withdraw | PaymentRunRateQuoter on successful quote |
withdrawing | No (marked for removal) | The CEX trades and the blockchain transfer are in flight | PaymentRunWithdrawInitiator |
paid | Yes (sink) | The outbound transfer has confirmed for the full payout amount | PaymentRunTransferCompletionChecker |
Helpers: isCreated(), isRated(), isWithdrawing(), isPaid(). No isFinal(), no canTransition(), no TRANSITIONS matrix, no all().
Transitions
Note the self-loop on rated: PaymentRunRateQuoter explicitly accepts created or rated as entry states and re-quotes. This matters operationally — rates can be refreshed as long as the run has not entered withdrawing.
Transition Table
| From | To | Trigger (service) | Invariant / guard | Side effects |
|---|---|---|---|---|
(new) → created | PaymentRunRateQuoter.findOrCreatePaymentRun() | Partner has open dues that fit the grouping criteria; no other run is withdrawing for the same partner | Due.paymentRunId is set on grouped dues (they stay open until the run advances) | |
created → rated | PaymentRunRateQuoter.run() | Successful quote for every coin/CEX combination in the payout | Sets paymentRun.ratedAt; populates rates[] | |
rated → rated (re-quote) | PaymentRunRateQuoter.run() | Entry allowed from rated too (PaymentRunRateQuoter.ts:170) | Refreshes rates[]; updates ratedAt | |
rated → withdrawing | PaymentRunWithdrawInitiator | paymentRun.status.isRated(); vault has sufficient funds for the pre-payout trades | Emits PaymentRunWithdrawInitiatedDomainEvent; cascades every Due in the run to settling; creates CexAccountTrades (paymentrun motive) via PaymentRunTradesCreator; eventually creates a Transfer in shifts via PaymentRunTransferCreator | |
withdrawing → paid | PaymentRunTransferCompletionChecker | Cumulative confirmed transfer amount ≥ payout amount and no failed transfers on the run | Cascades every Due in the run to paid |
If the run's transfer fails (or a partial amount lands and the remainder stalls), the run stays in withdrawing. There is no failed or cancelled state — recovery is out-of-band.
Invariants
- Initial state is always
created—PaymentRunRateQuoter.findOrCreatePaymentRun()hardcodes it (PaymentRunRateQuoter.ts:151). ratedtolerates re-entry — re-quoting refreshesrates[]andratedAtwithout leaving the state. Any other entry state is rejected by the quoter's guard at PaymentRunRateQuoter.ts:170.withdrawingis a compound step — in this state the run is simultaneously running CEX trades (CexAccountTradewithmotive=paymentrun, seePaymentRunTradeStatus) and dispatching a blockchainTransferinshifts. A run can be inspected mid-withdrawal by looking atpaymentRun.trades[]andpaymentRun.transferId.withdrawingis scheduled to be removed — the source carries// todo: remove this. Consumers should not build new guards that depend on distinguishingratedfromwithdrawing; useisPaid()orisCreated()and treat the middle states as "in progress".- Only one active withdraw per partner —
findOrCreatePaymentRunrefuses to create a new run while another is in a non-terminal state (PaymentRunRateQuoter.ts:162). paidis the only sink — nothing moves a run out ofpaid. There is no unpaid, canceled, or failed terminal.- Status changes cascade to
DueStatus— everyopen → settlingandsettling → paidtransition on the underlying dues is triggered by aPaymentRunStatuschange (seeDueStatus). TheDuenever transitions on its own after creation. - No per-run success event for
paid—PaymentRunWithdrawInitiatedDomainEventfires onrated → withdrawing, but there is noPaymentRunPaidDomainEvent. Downstream readers learn about payment completion viaDue.status = paidor by pollingPaymentRun.status.
Relationship with child state machines
| Child | Writer | Transition driven by PaymentRunStatus |
|---|---|---|
DueStatus | Repository bulk update keyed by paymentRunId | open → settling on rated → withdrawing; settling → paid on withdrawing → paid |
PaymentRunTradeStatus | Projection reloaded by PaymentRunTradesSynchronizer | Trades are created during withdrawing; the projection reflects whatever the CEX (CexAccountTradeStatus) reports |
Code Pointers
- Value object: packages/partners/domain/valueObjects/PaymentRunStatus.ts
- Entity: packages/partners/domain/entities/PaymentRun.ts
- Creation and rating: packages/partners/application/services/PaymentRunRateQuoter.ts
- Withdrawal initiation (
rated → withdrawing): packages/partners/application/services/PaymentRunWithdrawInitiator.ts - Trade creation during withdrawal: packages/partners/application/services/PaymentRunTradesCreator.ts
- Transfer creation in
shifts: packages/partners/application/services/PaymentRunTransferCreator.ts - Completion check (
withdrawing → paid): packages/partners/application/services/PaymentRunTransferCompletionChecker.ts - Events: packages/partners/domain/events/PaymentRunWithdrawInitiatedDomainEvent.ts,
PaymentRunInsufficientFundsForTradesDomainEvent.ts
Related
- Owner context:
partners - Child state machines:
DueStatus,PaymentRunTradeStatus - Upstream source of truth for the trades it triggers:
CexAccountTradeStatus(incex-wallets) - Cross-context bridge for the outbound blockchain
Transfer:TransferStatus - Flow that consumes this state machine:
payment-run(to be documented) - Upstream flow that feeds
Dues into this one:shift-lifecycle(step 9 creates theDues grouped here)
