Skip to content

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

StateTerminalDescriptionEntry condition
createdNoRun exists; no rates quoted yetPaymentRunRateQuoter.findOrCreatePaymentRun()
ratedNoRates quoted for every coin in the payout; ready to initiate the withdrawPaymentRunRateQuoter on successful quote
withdrawingNo (marked for removal)The CEX trades and the blockchain transfer are in flightPaymentRunWithdrawInitiator
paidYes (sink)The outbound transfer has confirmed for the full payout amountPaymentRunTransferCompletionChecker

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

FromToTrigger (service)Invariant / guardSide effects
(new) → createdPaymentRunRateQuoter.findOrCreatePaymentRun()Partner has open dues that fit the grouping criteria; no other run is withdrawing for the same partnerDue.paymentRunId is set on grouped dues (they stay open until the run advances)
created → ratedPaymentRunRateQuoter.run()Successful quote for every coin/CEX combination in the payoutSets paymentRun.ratedAt; populates rates[]
rated → rated (re-quote)PaymentRunRateQuoter.run()Entry allowed from rated too (PaymentRunRateQuoter.ts:170)Refreshes rates[]; updates ratedAt
rated → withdrawingPaymentRunWithdrawInitiatorpaymentRun.status.isRated(); vault has sufficient funds for the pre-payout tradesEmits PaymentRunWithdrawInitiatedDomainEvent; cascades every Due in the run to settling; creates CexAccountTrades (paymentrun motive) via PaymentRunTradesCreator; eventually creates a Transfer in shifts via PaymentRunTransferCreator
withdrawing → paidPaymentRunTransferCompletionCheckerCumulative confirmed transfer amount ≥ payout amount and no failed transfers on the runCascades 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

  1. Initial state is always createdPaymentRunRateQuoter.findOrCreatePaymentRun() hardcodes it (PaymentRunRateQuoter.ts:151).
  2. rated tolerates re-entry — re-quoting refreshes rates[] and ratedAt without leaving the state. Any other entry state is rejected by the quoter's guard at PaymentRunRateQuoter.ts:170.
  3. withdrawing is a compound step — in this state the run is simultaneously running CEX trades (CexAccountTrade with motive=paymentrun, see PaymentRunTradeStatus) and dispatching a blockchain Transfer in shifts. A run can be inspected mid-withdrawal by looking at paymentRun.trades[] and paymentRun.transferId.
  4. withdrawing is scheduled to be removed — the source carries // todo: remove this. Consumers should not build new guards that depend on distinguishing rated from withdrawing; use isPaid() or isCreated() and treat the middle states as "in progress".
  5. Only one active withdraw per partnerfindOrCreatePaymentRun refuses to create a new run while another is in a non-terminal state (PaymentRunRateQuoter.ts:162).
  6. paid is the only sink — nothing moves a run out of paid. There is no unpaid, canceled, or failed terminal.
  7. Status changes cascade to DueStatus — every open → settling and settling → paid transition on the underlying dues is triggered by a PaymentRunStatus change (see DueStatus). The Due never transitions on its own after creation.
  8. No per-run success event for paidPaymentRunWithdrawInitiatedDomainEvent fires on rated → withdrawing, but there is no PaymentRunPaidDomainEvent. Downstream readers learn about payment completion via Due.status = paid or by polling PaymentRun.status.

Relationship with child state machines

ChildWriterTransition driven by PaymentRunStatus
DueStatusRepository bulk update keyed by paymentRunIdopen → settling on rated → withdrawing; settling → paid on withdrawing → paid
PaymentRunTradeStatusProjection reloaded by PaymentRunTradesSynchronizerTrades are created during withdrawing; the projection reflects whatever the CEX (CexAccountTradeStatus) reports

Code Pointers