feat: schedule preview and off-cycle payouts
Two things an operator needs that the scheduler alone does not give them. Preview draws a contract's calendar from loose terms — start date, frequency, period count — with no employee or wallet required, because the schedule is what an operator wants to sanity-check first and a mistyped start date is cheapest to fix before anything is saved. The same shape is available for a live contract, from its current position. Both return `ends_on`: "12 monthly payments from 15 Jan" is far easier to verify against "ends 15 Dec" than against a list of twelve dates. pay-now settles the next period immediately and consumes it. Deliberately one endpoint rather than two: "run it now, don't wait for the tick" and "pay it early" are the same operation and differ only in whether today happens to be the payday. It bypasses the back-dated skip — that guard exists to stop a new contract firing surprise back-pay, and an operator explicitly asking to pay is not a surprise — and it takes the same per-contract lock as the scheduler, which is what that lock was added for. The ledger row is returned even on failure, since a manual payout that did not land is exactly when you want the reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
parent
8b054d27ea
commit
ee10b2e04d
6 changed files with 313 additions and 9 deletions
38
models.py
38
models.py
|
|
@ -244,3 +244,41 @@ class Payout(BaseModel):
|
|||
@property
|
||||
def amount_sat(self) -> int | None:
|
||||
return None if self.amount_msat is None else self.amount_msat // 1000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schedule preview
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SchedulePreviewRequest(BaseModel):
|
||||
"""Enough of a contract to draw its calendar, with none of its identity.
|
||||
|
||||
Deliberately not a `CreateContract`: previewing must work before an
|
||||
employee or a wallet has been chosen, since the schedule is usually the
|
||||
thing the operator wants to sanity-check first.
|
||||
"""
|
||||
|
||||
start_date: str # YYYY-MM-DD
|
||||
frequency: Frequency = Frequency.monthly
|
||||
total_periods: int | None = None
|
||||
count: int = 12 # how many paydays to draw
|
||||
|
||||
@validator("start_date")
|
||||
def _valid_date(cls, v: str) -> str:
|
||||
datetime.strptime(v, "%Y-%m-%d")
|
||||
return v
|
||||
|
||||
@validator("count")
|
||||
def _sane_count(cls, v: int) -> int:
|
||||
return max(1, min(v, 120))
|
||||
|
||||
|
||||
class SchedulePreview(BaseModel):
|
||||
paydays: list[str] # YYYY-MM-DD, in order
|
||||
total_periods: int | None = None
|
||||
# The contract's last payday, or None for an open-ended one. Worth
|
||||
# surfacing on its own: "12 monthly payments from 15 Jan" is much easier
|
||||
# to check against "ends 15 Dec" than against a list.
|
||||
ends_on: str | None = None
|
||||
truncated: bool = False # more paydays exist than were drawn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue