diff --git a/crud.py b/crud.py
index 5a70fec..94c42e2 100644
--- a/crud.py
+++ b/crud.py
@@ -408,7 +408,16 @@ async def delete_dca_client(client_id: str) -> None:
# =============================================================================
-async def create_deposit(creator_user_id: str, data: CreateDepositData) -> DcaDeposit:
+async def create_deposit(
+ creator_user_id: str, data: CreateDepositData, *, currency: str
+) -> DcaDeposit:
+ """Insert a deposit row.
+
+ `currency` is passed explicitly by the caller (the API endpoint
+ resolves it from the target machine's `fiat_code`) rather than
+ coming off the request body — the operator doesn't get to choose
+ it (`aiolabs/satmachineadmin#26`).
+ """
deposit_id = urlsafe_short_hash()
await db.execute(
"""
@@ -424,7 +433,7 @@ async def create_deposit(creator_user_id: str, data: CreateDepositData) -> DcaDe
"machine_id": data.machine_id,
"creator_user_id": creator_user_id,
"amount": data.amount,
- "currency": data.currency,
+ "currency": currency,
"status": "pending",
"notes": data.notes,
"created_at": datetime.now(),
diff --git a/migrations.py b/migrations.py
index 0d9bc7d..508aa51 100644
--- a/migrations.py
+++ b/migrations.py
@@ -437,3 +437,36 @@ async def m004_introduce_dca_lp_table(db):
"autoforward_enabled",
):
await db.execute(f"ALTER TABLE satoshimachine.dca_clients DROP COLUMN {col}")
+
+
+async def m005_lock_deposit_currency_to_machine_fiat_code(db):
+ """Rewrite every `dca_deposits.currency` row to match its joined
+ `dca_machines.fiat_code`.
+
+ Today each machine handles exactly one currency (operator-set on
+ `dca_machines.fiat_code`); a deposit's currency is fully determined
+ by the machine it's recorded against. The deposit dialog was
+ historically a freeform text input, which let an operator typo a
+ currency code (e.g., a "15 USD" row landed against an EUR Sintra
+ during 2026-05-16 testing — that mismatch silently inflated the LP's
+ nominal balance because the balance summary is currency-blind).
+
+ `aiolabs/satmachineadmin#26` locks the input side; this migration
+ fixes any rows already on disk. Idempotent: on a fresh install with
+ no mismatches it's a no-op UPDATE.
+ """
+ await db.execute("""
+ UPDATE satoshimachine.dca_deposits AS d
+ SET currency = (
+ SELECT m.fiat_code
+ FROM satoshimachine.dca_machines m
+ WHERE m.id = d.machine_id
+ )
+ WHERE EXISTS (
+ SELECT 1
+ FROM satoshimachine.dca_machines m
+ WHERE m.id = d.machine_id
+ AND m.fiat_code IS NOT NULL
+ AND m.fiat_code != d.currency
+ )
+ """)
diff --git a/models.py b/models.py
index 431e70d..c61a515 100644
--- a/models.py
+++ b/models.py
@@ -165,10 +165,19 @@ class ClientBalanceSummary(BaseModel):
class CreateDepositData(BaseModel):
+ """Operator records a fiat deposit against an LP enrolment.
+
+ `currency` is server-set from the target machine's `fiat_code` at
+ write time — the API ignores any value the client submits. Each
+ machine currently handles exactly one currency (`dca_machines.
+ fiat_code`); allowing the operator to pick a different one at
+ deposit time would either be a typo or a future multi-currency
+ feature that doesn't exist yet (`aiolabs/satmachineadmin#26`).
+ """
+
client_id: str
machine_id: str
amount: float
- currency: str = "GTQ"
notes: Optional[str] = None
@validator("amount")
@@ -192,8 +201,11 @@ class DcaDeposit(BaseModel):
class UpdateDepositData(BaseModel):
+ """Operator edits on a pending deposit. `currency` removed — see
+ `CreateDepositData`; the currency is bound to the machine and not
+ editable after the row lands."""
+
amount: Optional[float] = None
- currency: Optional[str] = None
notes: Optional[str] = None
@validator("amount")
diff --git a/static/js/index.js b/static/js/index.js
index fb2a470..f67aa46 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -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: ''
}
},
diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html
index 5972481..c43450c 100644
--- a/templates/satmachineadmin/index.html
+++ b/templates/satmachineadmin/index.html
@@ -1136,14 +1136,23 @@
-
-
+ :rules="[v => v > 0 || 'Must be > 0']">
+
+
+
+ Currency is set by the selected LP's machine
+ () — not
+ operator-editable. See aiolabs/satmachineadmin#26.
+
+
+
+