fix: resuming an auto-paused contract no longer discards the backlog
Found by tracing what happens when a back-dated backfill contract cannot fetch a historical rate. The failure handling itself was fine — period 0 fails, the backlog halts so nothing settles out of order, five ledger rows record the reason, no money moves, and the contract auto-pauses once the retry budget is spent. The recovery was not. The operator fixes the cause (switches to a stated rate, or to current), clicks Resume, and periods_done jumps 0 -> 6: every unpaid payday silently written off, contract back to looking healthy, employee never paid. The confirm dialog even asserted the missed paydays "are written off" — true of one kind of pause and a lie about the other. Two features colliding. "Do not backfill a deliberate pause" is right when the operator paused: the pause *was* the decision not to pay. It is wrong when payroll paused, because nobody decided anything — the money is still owed and the operator has just removed whatever blocked it. Contracts now carry `paused_reason`, set only when payroll pauses them and cleared by a deliberate pause. Resume infers from it, and an explicit `catch_up` still overrides either way. The console asks a different question for each, quoting the reason, and flags a payroll-paused contract in the table so the distinction is visible before anyone clicks. Verified end to end: five failing ticks leave periods_done at 0 and pause with "period 0 (2026-08-01) failed 5 times: no historical EUR rate available for 2026-08-01"; resuming after switching to a manual rate keeps the position at 0, and the next tick settles all seven owed periods. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
parent
1584eb337f
commit
e77f431d47
8 changed files with 168 additions and 22 deletions
|
|
@ -138,9 +138,25 @@ Retries are bounded. After `MAX_PERIOD_ATTEMPTS` (5) failures on the *same*
|
|||
period, the contract is **paused** and the operator has to act. Pausing
|
||||
rather than abandoning the period is the point: a payday that cannot be
|
||||
funded is a fact somebody needs to see, and silently dropping it is the one
|
||||
outcome payroll must never produce. Because pausing does not advance the
|
||||
position, resuming after topping up the source wallet retries that same
|
||||
payday.
|
||||
outcome payroll must never produce.
|
||||
|
||||
### Two kinds of pause, and why resume must tell them apart
|
||||
|
||||
| paused by | `paused_reason` | resume does |
|
||||
|---|---|---|
|
||||
| the operator | empty | **writes off** the missed paydays |
|
||||
| payroll | names the period and cause | **keeps** them; they settle next tick |
|
||||
|
||||
These mean opposite things. An operator pause *is* the decision not to pay
|
||||
those periods. An automatic pause means payroll could not pay them and
|
||||
nobody decided anything — the money is still owed, and the operator has just
|
||||
fixed whatever blocked it.
|
||||
|
||||
Sharing one path between the two destroys money silently: fix the cause,
|
||||
click resume, and the backlog vanishes while the contract looks healthy.
|
||||
`resume` therefore infers from `paused_reason`, and `catch_up` overrides it.
|
||||
The console asks a different question for each and flags a payroll-paused
|
||||
contract in the table with its reason.
|
||||
|
||||
## The payout ledger
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue