refactor(v2): cassette consumer + API endpoint — position-keyed (#29 v1.1)
API endpoint:
- api_publish_machine_cassettes validates incoming payload.positions
set matches stored cassette_configs.position set (was: denomination
set match). Error message updated to "slot count is hardware-fixed
— re-provision the ATM via atm-tui to add/remove physical bays."
- Per-row upsert loop iterates payload.positions and passes
UpsertCassetteConfigData(denomination=, count=) — operator edits
denomination + count for a fixed slot.
Bootstrap consumer task: just a log-message field rename (now reports
"N cassettes" from len(payload.positions) instead of len(payload.
denominations)). Per-event handler already routes through the
transport's decrypt_and_parse_state_event, which returns a
PublishCassettesPayload that's now position-keyed via the model.
Tests still red until commit f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5dbd7314f4
commit
34e324b4c5
2 changed files with 33 additions and 31 deletions
62
views_api.py
62
views_api.py
|
|
@ -774,15 +774,16 @@ async def api_update_super_config(
|
|||
|
||||
|
||||
# =============================================================================
|
||||
# Cassette configs (#29 v1) — per-machine ATM cassette inventory
|
||||
# Cassette configs (#29 v1.1) — per-machine ATM cassette inventory
|
||||
# =============================================================================
|
||||
# v1 surface, paired with aiolabs/lamassu-next#56 ATM-side. Two endpoints:
|
||||
# v1.1 surface, paired with aiolabs/lamassu-next#56 ATM-side. Two endpoints:
|
||||
# GET /machines/{id}/cassettes — list rows for the operator UI
|
||||
# POST /machines/{id}/cassettes/publish — apply edits + publish kind-30078
|
||||
#
|
||||
# Row creation (new (machine_id, denomination) pairs) is admin-only via the
|
||||
# bootstrap consumer task — denomination set is hardware-determined.
|
||||
# Operator-side flow is edit-and-publish over the existing rows only.
|
||||
# Row creation (new (machine_id, position) pairs) is admin-only via the
|
||||
# bootstrap consumer task — slot count is hardware-determined. Operator-
|
||||
# side flow is edit-and-publish over the existing rows only; the editable
|
||||
# fields per row are denomination and count.
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
|
|
@ -793,9 +794,9 @@ async def api_list_machine_cassettes(
|
|||
machine_id: str, user: User = Depends(check_user_exists)
|
||||
) -> list[CassetteConfig]:
|
||||
"""List the cassette config rows for one of the operator's machines,
|
||||
ordered by position then denomination. Empty list = ATM hasn't yet
|
||||
published its bootstrap event (or the bootstrap consumer hasn't
|
||||
processed it yet); UI should show a "waiting for ATM" state."""
|
||||
ordered by position. Empty list = ATM hasn't yet published its
|
||||
bootstrap event (or the bootstrap consumer hasn't processed it yet);
|
||||
UI should show a "waiting for ATM" state."""
|
||||
await _machine_owned_by(machine_id, user.id)
|
||||
return await list_cassette_configs_for_machine(machine_id)
|
||||
|
||||
|
|
@ -810,11 +811,11 @@ async def api_publish_machine_cassettes(
|
|||
user: User = Depends(check_user_exists),
|
||||
) -> list[CassetteConfig]:
|
||||
"""Operator submits the full per-machine cassette state for publish to
|
||||
the ATM. Validates the denomination set matches what's currently in
|
||||
cassette_configs for the machine (defensive — UI prevents add/remove
|
||||
but API enforces), upserts each row, then encrypts + signs + publishes
|
||||
a kind-30078 event tagged with d=bitspire-cassettes:<atm_pubkey_hex>
|
||||
and p=<atm_pubkey_hex>.
|
||||
the ATM. Validates the position set matches what's currently in
|
||||
cassette_configs for the machine (slot count is hardware-fixed),
|
||||
upserts each row, then encrypts + signs + publishes a kind-30078
|
||||
event tagged with d=bitspire-cassettes:<atm_pubkey_hex> and
|
||||
p=<atm_pubkey_hex>.
|
||||
|
||||
The `<m>` placeholder in the published d-tag is the ATM's hex pubkey
|
||||
from machine.machine_npub (canonicalised via normalize_public_key),
|
||||
|
|
@ -825,8 +826,8 @@ async def api_publish_machine_cassettes(
|
|||
can refresh its table from one round-trip.
|
||||
|
||||
Errors:
|
||||
400 — payload denomination set doesn't match the machine's stored
|
||||
set (operator publishing a cassette that doesn't exist on the
|
||||
400 — payload position set doesn't match the machine's stored set
|
||||
(operator publishing for a slot that doesn't exist on the
|
||||
ATM; or the bootstrap hasn't landed yet so no rows exist)
|
||||
400 — operator hasn't onboarded a Nostr identity
|
||||
503 — signer offline / client-side-only, or nostrclient extension
|
||||
|
|
@ -836,8 +837,8 @@ async def api_publish_machine_cassettes(
|
|||
machine = await _machine_owned_by(machine_id, user.id)
|
||||
|
||||
existing = await list_cassette_configs_for_machine(machine_id)
|
||||
existing_denoms = {row.denomination for row in existing}
|
||||
incoming_denoms = set(payload.denominations.keys())
|
||||
existing_positions = {row.position for row in existing}
|
||||
incoming_positions = set(payload.positions.keys())
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(
|
||||
|
|
@ -850,29 +851,30 @@ async def api_publish_machine_cassettes(
|
|||
"receipt."
|
||||
),
|
||||
)
|
||||
if existing_denoms != incoming_denoms:
|
||||
missing = existing_denoms - incoming_denoms
|
||||
extra = incoming_denoms - existing_denoms
|
||||
if existing_positions != incoming_positions:
|
||||
missing = existing_positions - incoming_positions
|
||||
extra = incoming_positions - existing_positions
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
(
|
||||
"Payload denomination set doesn't match the machine's "
|
||||
f"stored set. Missing from payload: {sorted(missing)}; "
|
||||
f"extra in payload: {sorted(extra)}. "
|
||||
"Denomination set is hardware-determined — re-provision "
|
||||
"the ATM via atm-tui to add/remove cassettes, then "
|
||||
"re-publish."
|
||||
"Payload position set doesn't match the machine's stored "
|
||||
f"set. Missing from payload: {sorted(missing)}; extra in "
|
||||
f"payload: {sorted(extra)}. Slot count is hardware-fixed "
|
||||
"— re-provision the ATM via atm-tui to add/remove physical "
|
||||
"bays, then re-publish."
|
||||
),
|
||||
)
|
||||
|
||||
# Apply each per-row edit so the operator-believed state on
|
||||
# satmachineadmin reflects the published payload, even if the ATM
|
||||
# ack lands later (v2). updated_by audit-stamps the operator user id.
|
||||
for denom, row in payload.denominations.items():
|
||||
for pos, row in payload.positions.items():
|
||||
updated = await update_cassette_config(
|
||||
machine_id,
|
||||
denom,
|
||||
UpsertCassetteConfigData(count=row.count, position=row.position),
|
||||
pos,
|
||||
UpsertCassetteConfigData(
|
||||
denomination=row.denomination, count=row.count
|
||||
),
|
||||
updated_by=user.id,
|
||||
)
|
||||
if updated is None:
|
||||
|
|
@ -880,7 +882,7 @@ async def api_publish_machine_cassettes(
|
|||
# concurrent delete could land between. Surface as 500.
|
||||
raise HTTPException(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
f"cassette row for denomination {denom} disappeared mid-publish",
|
||||
f"cassette row for position {pos} disappeared mid-publish",
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue