fix(v2): use payment_hash as settlement idempotency key

The initial m005 made bitspire_event_id the UNIQUE idempotency key on
dca_settlements, but settlements arriving through LNbits' invoice
listener (the canonical path per nostr-transport-branch architecture)
don't carry a Nostr event id at the Payment level — that's the
underlying transport's concern, not exposed to extensions.

The natural unique key is payment_hash:
- every LN invoice has a globally unique payment_hash
- subscription replays / dispatcher double-fires dedup via UNIQUE
- it's always present on the Payment object the invoice_listener delivers

Reshape the dca_settlements column constraints:
- payment_hash: TEXT NOT NULL UNIQUE (was: NOT NULL + separate index)
- bitspire_event_id: TEXT (was: NOT NULL UNIQUE) — kept nullable for
  a future path where we subscribe to raw kind-21000 Nostr events
  directly, bypassing the Payment system

Also rename the CRUD helper: get_settlement_by_event_id →
get_settlement_by_payment_hash, and update create_settlement_idempotent
to dedup on payment_hash. CreateDcaSettlementData / DcaSettlement
adjust accordingly.

The schema is unshipped (v2-bitspire branch is local only) — fixing
m005 in-place is appropriate. The separate dca_telemetry path for
kind-30078/30079 events already uses (machine_id, beacon_received_at)
semantics, so the UNIQUE-by-Nostr-event-id pattern isn't needed there
either.

Caught during P1a design before subscribing to register_invoice_listener.

Refs: aiolabs/satmachineadmin#9, aiolabs/lamassu-next#44

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 14:46:08 +02:00
commit cba327d0f0
3 changed files with 34 additions and 29 deletions

View file

@ -185,9 +185,9 @@ class UpdateDepositStatusData(BaseModel):
class CreateDcaSettlementData(BaseModel):
machine_id: str
bitspire_event_id: str # nostr event id — the idempotency key
payment_hash: str # the idempotency key (UNIQUE in the dca_settlements table)
bitspire_event_id: Optional[str] = None # reserved for direct-Nostr ingestion
bitspire_txid: Optional[str] = None
payment_hash: str
gross_sats: int
fiat_amount: float
fiat_code: str = "GTQ"
@ -205,9 +205,9 @@ class CreateDcaSettlementData(BaseModel):
class DcaSettlement(BaseModel):
id: str
machine_id: str
bitspire_event_id: str
bitspire_txid: Optional[str]
payment_hash: str
bitspire_event_id: Optional[str]
bitspire_txid: Optional[str]
gross_sats: int
fiat_amount: float
fiat_code: str