A back-dated payout's sat figure is unexplainable on its own — employee_2's
August backfill paid 18,353 sat and 14,794 sat for the same ten euro, and
only the rate says why. Both wallets now show it:
dev retainer — 2026-08-01 · 10 EUR @ 54,487 EUR/BTC
dev retainer — 2026-08-29 · 10 EUR @ 67,596.6 EUR/BTC
This required moving the `current` mode conversion into payroll as well.
Previously that mode handed create_invoice a fiat amount and let LNbits
convert, so the rate only became knowable by reading it back off the
resulting invoice — after the memo had already been fixed. Now every mode
resolves its rate before invoicing and the memo is accurate in all three,
rather than only for the back-dated ones.
It remains a single conversion, not a second opinion: fiat_amount_as_satoshis
is the same function create_invoice would have called, and the rate is
derived by the same formula calculate_fiat_amounts uses. Because passing
sats means LNbits no longer stamps the fiat metadata itself, payroll now
writes the identical fiat_currency / fiat_amount / fiat_rate / btc_rate keys
onto the payment, so a payroll payment still reads like any other
fiat-priced one in the payments list.
A rate that rounds an amount to zero sats now fails the period rather than
raising a zero-division while deriving the rate.
Memo omits the rate clause for sat-denominated contracts, where there is no
conversion to report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
49 lines
1.5 KiB
Python
49 lines
1.5 KiB
Python
"""Test fixtures for payroll.
|
|
|
|
The schedule math is pure, so most tests need nothing but a `Contract`
|
|
instance — no LNbits DB, no wallets, no event loop. That is the point of
|
|
keeping `services` split into a pure half and an effectful half.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from ..models import Contract, ContractStatus, Frequency, PricingMode
|
|
|
|
|
|
def make_contract(
|
|
*,
|
|
contract_id: str = "c1",
|
|
start_date: str = "2026-01-15",
|
|
frequency: Frequency = Frequency.monthly,
|
|
total_periods: int | None = 12,
|
|
periods_done: int = 0,
|
|
status: ContractStatus = ContractStatus.active,
|
|
amount: float = 1000,
|
|
currency: str = "sat",
|
|
backfill: bool = False,
|
|
created_at: str = "2026-01-01",
|
|
pricing_mode: PricingMode = PricingMode.payday,
|
|
manual_rate: float | None = None,
|
|
label: str = "",
|
|
) -> Contract:
|
|
return Contract(
|
|
id=contract_id,
|
|
employee_id="employee-account",
|
|
employee_username="alice",
|
|
employee_wallet="wallet-employee",
|
|
source_wallet="wallet-treasury",
|
|
amount=amount,
|
|
currency=currency,
|
|
frequency=frequency,
|
|
start_date=start_date,
|
|
total_periods=total_periods,
|
|
periods_done=periods_done,
|
|
status=status,
|
|
backfill=backfill,
|
|
pricing_mode=pricing_mode,
|
|
manual_rate=manual_rate,
|
|
label=label,
|
|
created_at=datetime.strptime(created_at, "%Y-%m-%d").replace(
|
|
tzinfo=timezone.utc
|
|
),
|
|
)
|