diff --git a/docs/operations.md b/docs/operations.md index 2c816db..1bc1c8b 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -56,6 +56,17 @@ A period whose payday is **in the past** is where the modes diverge: | `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 | +`manual` is an instruction, not a lookup, so it applies to **every** period — +a contract pegged to an agreed rate honours it whether the payday is last +month or next week. + +`payday` and `current` only diverge for a payday already past. For one that +is today or ahead there is no history to consult, so `payday` collapses into +`current`: a future rate is not knowable, and the live one is the only +defensible answer. The pay-now dialog says so outright, naming the period's +date, rather than leaving a hint to be missed beside a control that appears +to do something. + 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. diff --git a/services.py b/services.py index 60e8950..e3cf8f2 100644 --- a/services.py +++ b/services.py @@ -290,12 +290,26 @@ async def resolve_price( mode = mode or contract.pricing_mode manual_rate = manual_rate or contract.manual_rate - # A payday that is today or ahead needs no history: every mode agrees on - # the live rate. Payroll still does the conversion rather than handing - # create_invoice a fiat amount, because knowing the rate *before* the - # invoice exists is what lets the memo state it. It is the same single - # conversion either way — fiat_amount_as_satoshis is the function - # create_invoice would have called — not a second opinion. + # An explicit rate wins outright, whatever the calendar says. It is an + # instruction rather than a lookup — "pay at the rate we agreed" is as + # meaningful for a payday next week as for one last month, and a + # contract pegged to a fixed rate should honour it on every period. + if mode == PricingMode.manual: + if not manual_rate: + raise PricingError("manual pricing selected but no rate given") + return Price( + sats_for(contract.amount, manual_rate), "sat", manual_rate, "manual" + ) + + # A payday that is today or ahead has no history to consult, so `payday` + # mode collapses into `current` — a future rate is not knowable, and the + # live one is the only defensible answer. + # + # Payroll still does the conversion rather than handing create_invoice a + # fiat amount, because knowing the rate *before* the invoice exists is + # what lets the memo state it. Same single conversion either way — + # fiat_amount_as_satoshis is the function create_invoice would have + # called — not a second opinion. if payday >= today or mode == PricingMode.current: amount_sat = await fiat_amount_as_satoshis(contract.amount, contract.currency) if amount_sat <= 0: @@ -306,13 +320,6 @@ async def resolve_price( rate = (contract.amount / amount_sat) * 100_000_000 return Price(amount_sat, "sat", rate, "current") - if mode == PricingMode.manual: - if not manual_rate: - raise PricingError("manual pricing selected but no rate given") - return Price( - sats_for(contract.amount, manual_rate), "sat", manual_rate, "manual" - ) - historical = await historical_btc_rate(payday, contract.currency) if historical is None: raise PricingError( diff --git a/static/js/index.js b/static/js/index.js index 4f88d7a..8f3b912 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -194,6 +194,19 @@ window.app = Vue.createApp({ ) }, + // Which period pay-now would settle, and whether it is actually in the + // past. "Rate on the payday" cannot mean anything for a date that has + // not happened, and the dialog should say so rather than offer a choice + // that quietly does nothing. + payNowPayday() { + const c = this.payNowDialog.contract + return c ? c.next_payday : null + }, + + payNowIsBackdated() { + return !!this.payNowPayday && this.payNowPayday < today() + }, + ledgerCsvHref() { const f = this.ledgerFilter const params = new URLSearchParams() diff --git a/templates/payroll/index.html b/templates/payroll/index.html index 0e5ca71..4a5ab2e 100644 --- a/templates/payroll/index.html +++ b/templates/payroll/index.html @@ -269,8 +269,8 @@ + label="Pricing" + hint="'Rate on the payday' only differs for a payday in the past">
+ label="Pricing">
${ payNowDialog.contract.label || payNowDialog.contract.employee_username } - — ${ payNowDialog.contract.amount } ${ payNowDialog.contract.currency }. + — ${ payNowDialog.contract.amount } ${ payNowDialog.contract.currency }, + payday ${ payNowPayday || '—' }. The period is consumed even if its payday has not arrived yet.
+ label="Price it at"> + +
+ Payday ${ payNowPayday } has not passed, so there is no historical + rate to look up — this will be priced at the current rate. +
+
+ Will convert at what BTC was worth on ${ payNowPayday }. +