v0.2: refactor monitor + scheduler around InvoiceStore Protocol
This commit is contained in:
parent
409005415e
commit
fb1e1b73a6
6 changed files with 952 additions and 596 deletions
|
|
@ -8,10 +8,17 @@ The SDK does not prescribe a database. Consumers implement
|
|||
All methods are async so the same Protocol works cleanly for both
|
||||
asyncpg/asyncio-sqlalchemy backends and synchronous backends wrapped
|
||||
with ``asyncio.to_thread``.
|
||||
|
||||
This module also ships :class:`InMemoryStore` — a reference implementation
|
||||
used by the test suite and useful as a drop-in for local development or
|
||||
ephemeral workflows that don't need durability.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Protocol, runtime_checkable
|
||||
|
||||
from cardano_checkout.invoice import Invoice, InvoiceStatus
|
||||
|
|
@ -67,3 +74,83 @@ class InvoiceStore(Protocol):
|
|||
state.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference implementation: InMemoryStore
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class InMemoryStore:
|
||||
"""In-memory :class:`InvoiceStore` — intended for tests and local dev.
|
||||
|
||||
Uses an ``asyncio.Lock`` around mutating operations so concurrent callers
|
||||
see a consistent view. Objects are deep-copied on read so callers can't
|
||||
mutate the stored state by accident.
|
||||
"""
|
||||
|
||||
_invoices: dict[str, Invoice] = field(default_factory=dict)
|
||||
_tx_log: dict[tuple[str, str], int] = field(default_factory=dict)
|
||||
_index_counters: dict[str, int] = field(default_factory=dict)
|
||||
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
|
||||
async def create(self, invoice: Invoice) -> None:
|
||||
async with self._lock:
|
||||
if invoice.id in self._invoices:
|
||||
raise ValueError(f"Invoice {invoice.id!r} already exists")
|
||||
self._invoices[invoice.id] = copy.deepcopy(invoice)
|
||||
# Bump the per-merchant index cursor so next_derivation_index doesn't
|
||||
# hand out the same slot again.
|
||||
cur = self._index_counters.get(invoice.merchant_id, -1)
|
||||
if invoice.derivation_index > cur:
|
||||
self._index_counters[invoice.merchant_id] = invoice.derivation_index
|
||||
|
||||
async def get(self, invoice_id: str) -> Optional[Invoice]:
|
||||
async with self._lock:
|
||||
stored = self._invoices.get(invoice_id)
|
||||
return copy.deepcopy(stored) if stored else None
|
||||
|
||||
async def list_by_status(
|
||||
self, status: InvoiceStatus, limit: int = 100
|
||||
) -> list[Invoice]:
|
||||
async with self._lock:
|
||||
matching = [
|
||||
copy.deepcopy(inv)
|
||||
for inv in self._invoices.values()
|
||||
if inv.status == status
|
||||
]
|
||||
# Newest first per contract.
|
||||
matching.sort(key=lambda inv: inv.created_at, reverse=True)
|
||||
return matching[:limit]
|
||||
|
||||
async def update(self, invoice: Invoice) -> None:
|
||||
async with self._lock:
|
||||
if invoice.id not in self._invoices:
|
||||
raise KeyError(f"Invoice {invoice.id!r} not found")
|
||||
self._invoices[invoice.id] = copy.deepcopy(invoice)
|
||||
|
||||
async def next_derivation_index(self, merchant_id: str) -> int:
|
||||
async with self._lock:
|
||||
nxt = self._index_counters.get(merchant_id, -1) + 1
|
||||
self._index_counters[merchant_id] = nxt
|
||||
return nxt
|
||||
|
||||
async def record_tx(
|
||||
self, invoice_id: str, tx_hash: str, lovelace_delta: int
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
# Idempotent: (invoice_id, tx_hash) overwrites the delta rather than
|
||||
# adding it a second time — see InvoiceStore.record_tx contract.
|
||||
self._tx_log[(invoice_id, tx_hash)] = lovelace_delta
|
||||
inv = self._invoices.get(invoice_id)
|
||||
if inv and tx_hash not in inv.tx_hashes:
|
||||
inv.tx_hashes.append(tx_hash)
|
||||
|
||||
# --- test helpers (not part of the Protocol) ---
|
||||
|
||||
def _all(self) -> list[Invoice]:
|
||||
return [copy.deepcopy(inv) for inv in self._invoices.values()]
|
||||
|
||||
def _tx_records(self) -> dict[tuple[str, str], int]:
|
||||
return dict(self._tx_log)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue