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
|
|
@ -278,6 +278,15 @@ window.app = Vue.createApp({
|
|||
const id = this.depositDialog.data.client_id
|
||||
return id ? this.clients.find(c => c.id === id) : null
|
||||
},
|
||||
depositMachineFiatCode() {
|
||||
// Currency the deposit will land in — bound to the machine the
|
||||
// selected LP is enrolled at. Resolved entirely client-side from
|
||||
// already-loaded data, but the server has the final say (#26).
|
||||
const c = this.selectedDepositClient
|
||||
if (!c) return null
|
||||
const m = this.machines.find(m => m.id === c.machine_id)
|
||||
return m ? m.fiat_code : null
|
||||
},
|
||||
worklistBuckets() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -966,7 +975,6 @@ window.app = Vue.createApp({
|
|||
id: deposit.id,
|
||||
client_id: deposit.client_id,
|
||||
amount: deposit.amount,
|
||||
currency: deposit.currency,
|
||||
notes: deposit.notes || ''
|
||||
}
|
||||
this.depositDialog.show = true
|
||||
|
|
@ -978,13 +986,14 @@ window.app = Vue.createApp({
|
|||
try {
|
||||
if (this.depositDialog.mode === 'add') {
|
||||
// machine_id is server-cross-checked but we send it explicitly.
|
||||
// currency is server-resolved from the machine's fiat_code
|
||||
// (#26); not in the request body.
|
||||
const client = this.clients.find(c => c.id === d.client_id)
|
||||
if (!client) throw new Error('client not found')
|
||||
const body = {
|
||||
client_id: d.client_id,
|
||||
machine_id: client.machine_id,
|
||||
amount: Number(d.amount),
|
||||
currency: (d.currency || 'GTQ').trim(),
|
||||
notes: (d.notes || '').trim() || null
|
||||
}
|
||||
const {data} = await LNbits.api.request('POST', DEPOSITS_PATH, null, body)
|
||||
|
|
@ -993,7 +1002,6 @@ window.app = Vue.createApp({
|
|||
} else {
|
||||
const body = {
|
||||
amount: Number(d.amount),
|
||||
currency: (d.currency || 'GTQ').trim(),
|
||||
notes: (d.notes || '').trim() || null
|
||||
}
|
||||
const {data} = await LNbits.api.request(
|
||||
|
|
@ -1279,10 +1287,12 @@ window.app = Vue.createApp({
|
|||
},
|
||||
|
||||
_emptyDepositForm() {
|
||||
// currency is server-resolved from the selected client's machine
|
||||
// fiat_code (see #26); not stored on the form, just displayed in
|
||||
// the dialog via depositMachineFiatCode() computed.
|
||||
return {
|
||||
client_id: null,
|
||||
amount: null,
|
||||
currency: 'GTQ',
|
||||
notes: ''
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue