feat(v2): lock deposit currency to machine.fiat_code (closes #26)
Each machine handles exactly one currency today (operator-set on
`dca_machines.fiat_code`). The deposit's currency is fully determined
by the machine it's recorded against, so it shouldn't be operator-
chooseable in the first place.
Surfaced during 2026-05-16 E2E testing: Jordan had a "15 USD" deposit
recorded against an EUR Sintra (operator typo in the freeform currency
input). The balance summary is currency-blind (`SUM(amount)` over
mixed currencies), so on the next cash-out the system distributed
15 EUR worth of sats on the strength of that 15 USD row. Worked out
by chance; could have over-paid by ~10% if the actual EUR/USD rate
had been further off.
Fix:
- `CreateDepositData` / `UpdateDepositData` no longer carry a
`currency` field. Any client-submitted value is silently dropped
at Pydantic validation, before reaching the handler.
- `api_create_deposit` resolves the machine's `fiat_code` and
passes it to `create_deposit(..., currency=...)` as a required
keyword arg. The deposit row's `currency` column always matches
the machine going forward.
- UI: the freeform `<q-input label="Currency">` becomes a read-only
`<q-chip>` slot on the amount field, sourced from the new
`depositMachineFiatCode` computed (resolves via the selected
client's machine).
- `m005_lock_deposit_currency_to_machine_fiat_code` migration
backfills existing rows: every `dca_deposits.currency` gets
rewritten to match its joined `dca_machines.fiat_code`. Greg's
stray `15 USD` row becomes `15 EUR` (the right answer at today's
invariant).
Multi-currency-per-machine support is explicitly out of scope here;
when hardware ships that reads multiple denominations across
currencies, the relevant changes are documented in issue #26's
"Future" section (dca_machines.fiat_codes set, currency-aware
balance summary, etc.). The current fix is "lock the input side";
that future work is "unlock it but constrained to the machine's
declared set".
3 new unit tests (`tests/test_deposit_currency.py`) lock in the
model-contract guarantees. Total suite 89 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da25d2e1f8
commit
d2e682712d
7 changed files with 151 additions and 15 deletions
56
tests/test_deposit_currency.py
Normal file
56
tests/test_deposit_currency.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
Locks in the contract from `aiolabs/satmachineadmin#26`: a deposit's
|
||||
currency is bound to its machine's `fiat_code`, never operator-chooseable.
|
||||
|
||||
The mechanism is two-layered:
|
||||
1. `CreateDepositData` / `UpdateDepositData` Pydantic models don't
|
||||
accept a `currency` field — any value a client submits is dropped
|
||||
at validation, before reaching the handler.
|
||||
2. The `api_create_deposit` endpoint resolves the machine's
|
||||
`fiat_code` server-side and passes it to `create_deposit(
|
||||
..., currency=...)`.
|
||||
|
||||
This test covers layer 1 (the model contract). Layer 2 is an
|
||||
endpoint-level behaviour better covered by an integration test against
|
||||
a running LNbits; tracked in #26 as a follow-up.
|
||||
"""
|
||||
|
||||
from ..models import CreateDepositData, UpdateDepositData
|
||||
|
||||
|
||||
def test_create_deposit_data_has_no_currency_field():
|
||||
"""A client posting `{currency: "USD"}` against an EUR machine must
|
||||
have that field silently dropped by validation — there's no public
|
||||
way to inject the wrong currency through this endpoint."""
|
||||
fields = CreateDepositData.__fields__
|
||||
assert "currency" not in fields, (
|
||||
f"CreateDepositData must not expose a `currency` field "
|
||||
f"(found {list(fields)})"
|
||||
)
|
||||
|
||||
|
||||
def test_update_deposit_data_has_no_currency_field():
|
||||
"""Same protection on the edit path: a pending deposit can have
|
||||
its amount / notes edited, but never its currency — that's bound
|
||||
to the machine."""
|
||||
fields = UpdateDepositData.__fields__
|
||||
assert "currency" not in fields, (
|
||||
f"UpdateDepositData must not expose a `currency` field "
|
||||
f"(found {list(fields)})"
|
||||
)
|
||||
|
||||
|
||||
def test_create_deposit_data_drops_unknown_currency_silently():
|
||||
"""Pydantic's default `Config` ignores unknown fields, so a stray
|
||||
`currency` on the request body parses cleanly without leaving
|
||||
a trace on the resulting model. Belt-and-braces — locks in the
|
||||
"input has no way to influence the currency" guarantee."""
|
||||
data = CreateDepositData(
|
||||
client_id="c1",
|
||||
machine_id="m1",
|
||||
amount=20.0,
|
||||
currency="USD", # ignored — field doesn't exist on the model
|
||||
)
|
||||
assert not hasattr(data, "currency")
|
||||
assert data.amount == 20.0
|
||||
assert data.machine_id == "m1"
|
||||
Loading…
Add table
Add a link
Reference in a new issue