feat(api): settle a cash_owed / partial_pending settlement off-machine
POST /api/v1/dca/settlements/{id}/settle-cash-owed: the operator paid
the customer by hand. Appends the provenance note, distributes at the
full amount (the customer is whole), and publishes a settle_transaction
op so the machine flips its own dispense_error/partial row to
remediated. Until now the only way to close an owed row was to dispense
more cash from the machine that had just jammed.
This commit is contained in:
parent
8d19da43c2
commit
7375112638
2 changed files with 88 additions and 0 deletions
63
views_api.py
63
views_api.py
|
|
@ -74,6 +74,7 @@ from .crud import (
|
|||
)
|
||||
from .distribution import (
|
||||
apply_partial_dispense_and_redistribute,
|
||||
settle_cash_owed_settlement,
|
||||
process_settlement,
|
||||
settle_lp_balance,
|
||||
)
|
||||
|
|
@ -95,6 +96,7 @@ from .models import (
|
|||
Machine,
|
||||
PairMachineData,
|
||||
PartialDispenseData,
|
||||
SettleCashOwedData,
|
||||
SetCommissionSplitsData,
|
||||
SettleBalanceData,
|
||||
StuckSettlementsResponse,
|
||||
|
|
@ -815,6 +817,67 @@ async def api_get_settlement(
|
|||
return settlement
|
||||
|
||||
|
||||
|
||||
@spirekeeper_api_router.post(
|
||||
"/api/v1/dca/settlements/{settlement_id}/settle-cash-owed",
|
||||
response_model=DcaSettlement,
|
||||
)
|
||||
async def api_settle_cash_owed(
|
||||
settlement_id: str,
|
||||
data: SettleCashOwedData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> DcaSettlement:
|
||||
"""ADR-005 §6 — the operator paid the customer outside the machine.
|
||||
|
||||
For a settlement in cash_owed or partial_pending: appends the provenance
|
||||
note, moves it to pending and distributes at the FULL amount (the customer
|
||||
is whole, so the sale is the full sale), and publishes a `settle_transaction`
|
||||
op so the machine flips its own `dispense_error` / `partial` row to
|
||||
`remediated`. Both ledgers close on one act; nothing is dispensed.
|
||||
|
||||
Until now the only way to clear an owed-cash row was to dispense more cash
|
||||
from the machine that had just jammed.
|
||||
|
||||
Errors: 404 not yours; 409 wrong status. The machine op is best-effort —
|
||||
recorded, and delivered with the next publish if the relay is down.
|
||||
"""
|
||||
settlement = await get_settlement(settlement_id)
|
||||
if settlement is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found")
|
||||
machine = await get_machine(settlement.machine_id)
|
||||
if machine is None or machine.operator_user_id != user.id:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found")
|
||||
if settlement.status not in ("cash_owed", "partial_pending"):
|
||||
raise HTTPException(
|
||||
HTTPStatus.CONFLICT,
|
||||
f"settlement is {settlement.status!r}; only cash_owed / partial_pending "
|
||||
"can be settled off-machine",
|
||||
)
|
||||
updated = await settle_cash_owed_settlement(settlement, data.note, user.id)
|
||||
|
||||
if machine.machine_npub and settlement.bitspire_txid:
|
||||
try:
|
||||
await create_cassette_op(
|
||||
machine.id,
|
||||
CreateCassetteOpData(
|
||||
position=0,
|
||||
op_type="settle_transaction",
|
||||
txid=settlement.bitspire_txid,
|
||||
note=data.note,
|
||||
),
|
||||
created_by=user.id,
|
||||
)
|
||||
window = await get_cassette_ops_window(machine.id)
|
||||
await publish_ops_to_atm(machine, window, user.id)
|
||||
except (OperatorIdentityMissing, SignerUnavailable, RelayUnavailable) as exc:
|
||||
logger.warning(
|
||||
f"spirekeeper: settle_transaction op for {settlement.bitspire_txid} "
|
||||
f"recorded but not published yet: {exc}"
|
||||
)
|
||||
except CassetteTransportError as exc:
|
||||
logger.error(f"spirekeeper: settle_transaction publish failed: {exc}")
|
||||
return updated
|
||||
|
||||
@spirekeeper_api_router.post(
|
||||
"/api/v1/dca/settlements/{settlement_id}/partial-dispense",
|
||||
response_model=DcaSettlement,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue