feat(ui): choose how a late payday is priced

Surfaces the pricing modes in the console: a selector plus a rate field on
both the create and edit dialogs, and a "Rate used" column in the ledger
showing the figure alongside where it came from, so a payout can be
explained without opening the database.

Pay-now becomes a dialog rather than a confirm. That is the moment an
operator is most likely to want a rate other than the contract's own —
entering a payment that happened weeks ago at a figure they already know —
and the choice has to be made before it settles, not after. It defaults to
the contract's mode, disables the rate field unless "Rate I enter" is
picked, and refuses to submit a manual mode with no rate. The success toast
now names the rate applied, and a refused payout still shows its reason.

The mode selector is disabled for sat-denominated contracts, where there is
no conversion to have an opinion about, and its hint says the setting only
applies to a payday already in the past — otherwise it reads as though it
governs every payout, which it does not.

Quasar UMD rules honoured: no self-closing tags, `${ }` delimiters,
`:style` bindings rather than a <style> block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
Padreug 2026-08-31 21:43:21 +02:00
commit 3504bf2b02
3 changed files with 179 additions and 29 deletions

View file

@ -42,6 +42,42 @@ left on the employee's wallet. It was never settled and expires on its own;
that is the deliberate price of not pricing the period a second time just
to run a balance check.
## Pricing a back-dated period
A period whose payday is **today or ahead** is priced by LNbits itself —
`create_invoice(amount, currency)` does the conversion, and nothing here
touches the network. Every pricing mode agrees in that case.
A period whose payday is **in the past** is where the modes diverge:
| mode | converts at | use when |
|---|---|---|
| `payday` (default) | BTC's value on the payday | the expense belongs to that date |
| `current` | today's rate | the debt reads "we owe EUR 800, whenever it settles" |
| `manual` | a rate you state (`100000` EUR/BTC) | the figure was agreed, not looked up |
Set per contract; overridable per payout via `pricing_mode` / `manual_rate`
on `pay-now`, which is how you enter one back-dated payment without editing
the contract.
Historical rates come from `rates.py` — Kraken's daily OHLC series first
(one request covers every date in a backfill), CoinGecko by date as the
fallback for currencies Kraken does not quote. **Not** from LNbits' own
exchange providers: those are spot tickers with no date parameter, and the
rate history behind the admin chart is RAM-only, single-currency and wiped
on restart.
Where payroll converts, it hands `create_invoice` a **sat** amount, because
`create_invoice` always prices fiat itself and cannot be told a rate. The
rate used and its source are recorded on the payout row either way — in
`current` mode read back off the invoice LNbits priced, not recomputed.
**A rate that cannot be established fails the period.** There is deliberately
no fallback to today's rate: one that moved 30% since the payday would pay
30% off and record nothing about it. The failure goes through the same
retry-then-pause path as an underfunded wallet, and names the date and
currency it could not price.
## What a failure does
A failed period does **not** advance `periods_done`, so the next tick