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 @@