payroll/tests/conftest.py
Padreug 3b14db3dff feat: name the applied rate in the payout memo
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
2026-08-31 21:55:49 +02:00

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
),
)