feat(dashboard): owed-cash worklist buckets, prefilled partial dispense, resume cash-out (ADR-005 §5–§6)
Three buckets render first on the worklist — cash_owed, partial_pending,
dispense_unreported (awaiting_dispense older than the threshold) — the
only ones whose meaning is "a customer is owed money". partial_pending
rows open the partial-dispense dialog pre-filled from the machine's
report: the fraction from dispensed_fiat_cents / fiat_amount and the
dispenser's error in the note, so the operator confirms a number the
hardware produced rather than typing one.
Machine detail shows a held-cash-out banner (code, time, reason) with a
Resume button; POST /machines/{id}/resume-cash-out records a
resume_cash_out op and publishes the window. The machine clears the hold
on receipt and the banner clears on its next state report.
This commit is contained in:
parent
44c2afa5bb
commit
fdba2d363f
3 changed files with 179 additions and 12 deletions
84
views_api.py
84
views_api.py
|
|
@ -19,6 +19,7 @@ from lnbits.core.services.nsec_bunker import (
|
|||
)
|
||||
from lnbits.decorators import check_super_user, check_user_exists
|
||||
from lnbits.utils.nostr import normalize_public_key
|
||||
from loguru import logger
|
||||
|
||||
from .calculations import MAX_FEE_FRACTION_PER_DIRECTION
|
||||
from .cassette_transport import (
|
||||
|
|
@ -28,15 +29,6 @@ from .cassette_transport import (
|
|||
SignerUnavailable,
|
||||
publish_ops_to_atm,
|
||||
)
|
||||
from .fee_transport import publish_fee_config
|
||||
from .pairing import (
|
||||
PairResult,
|
||||
PairingError,
|
||||
RevokeResult,
|
||||
default_relay_endpoint,
|
||||
pair_spire,
|
||||
revoke_spire,
|
||||
)
|
||||
from .crud import (
|
||||
append_settlement_note,
|
||||
count_completed_legs_for_settlement,
|
||||
|
|
@ -85,6 +77,7 @@ from .distribution import (
|
|||
process_settlement,
|
||||
settle_lp_balance,
|
||||
)
|
||||
from .fee_transport import publish_fee_config
|
||||
from .models import (
|
||||
AppendSettlementNoteData,
|
||||
CassetteConfig,
|
||||
|
|
@ -112,6 +105,14 @@ from .models import (
|
|||
UpdateMachineData,
|
||||
UpdateSuperConfigData,
|
||||
)
|
||||
from .pairing import (
|
||||
PairingError,
|
||||
PairResult,
|
||||
RevokeResult,
|
||||
default_relay_endpoint,
|
||||
pair_spire,
|
||||
revoke_spire,
|
||||
)
|
||||
|
||||
spirekeeper_api_router = APIRouter()
|
||||
|
||||
|
|
@ -768,7 +769,13 @@ async def api_list_stuck_settlements(
|
|||
) -> StuckSettlementsResponse:
|
||||
"""Operator worklist of settlements that didn't process cleanly.
|
||||
|
||||
Returns four lists:
|
||||
Returns seven lists. The first three (ADR-005 §6) mean a customer is owed
|
||||
money and render first:
|
||||
- cash_owed: the machine reported nothing dispensed; nothing moved
|
||||
- partial_pending: some notes out, value short; held until resolved
|
||||
- dispense_unreported: cash-out landed, machine never reported within
|
||||
the threshold
|
||||
Then:
|
||||
- rejected: Nostr attribution cross-check failed — signer didn't
|
||||
match the machine identity. Investigate; do not retry.
|
||||
- errored: distribution ran and failed; retry endpoint handles these
|
||||
|
|
@ -783,6 +790,9 @@ async def api_list_stuck_settlements(
|
|||
buckets = await get_stuck_settlements_for_operator(user.id, threshold_minutes)
|
||||
return StuckSettlementsResponse(
|
||||
threshold_minutes=threshold_minutes,
|
||||
cash_owed=buckets["cash_owed"],
|
||||
partial_pending=buckets["partial_pending"],
|
||||
dispense_unreported=buckets["dispense_unreported"],
|
||||
rejected=buckets["rejected"],
|
||||
errored=buckets["errored"],
|
||||
stuck_pending=buckets["stuck_pending"],
|
||||
|
|
@ -1229,3 +1239,57 @@ async def api_create_machine_cassette_op(
|
|||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) from exc
|
||||
|
||||
return op
|
||||
@spirekeeper_api_router.post(
|
||||
"/api/v1/dca/machines/{machine_id}/resume-cash-out",
|
||||
response_model=CassetteOp,
|
||||
)
|
||||
async def api_resume_cash_out(
|
||||
machine_id: str,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> CassetteOp:
|
||||
"""Release a machine's cash-out hold (bitspire ADR-005 §5).
|
||||
|
||||
After a terminal dispenser fault the machine refuses cash-out until an
|
||||
operator has been to it. A `recount` releases the hold as a side effect;
|
||||
this is for the case where the jam was cleared without touching a bay
|
||||
count. Recorded as a machine-wide op (position 0) and published on the
|
||||
same operator channel as the cassette ops — the machine honours it only if
|
||||
it is stamped after the hold began, so a re-delivered old resume cannot
|
||||
clear a newer fault.
|
||||
|
||||
Errors mirror the cassette-op endpoint: 400 unpaired, 503 signer/relay
|
||||
unavailable (the op is recorded and rides out with the next publish).
|
||||
"""
|
||||
machine = await _machine_owned_by(machine_id, user.id)
|
||||
if not machine.machine_npub:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"machine is not paired — there is no ATM identity to publish to",
|
||||
)
|
||||
if machine.cash_out_held_since is None:
|
||||
logger.info(
|
||||
f"spirekeeper: resume_cash_out for machine {machine_id} with no hold "
|
||||
"on file — publishing anyway (the machine is the authority)"
|
||||
)
|
||||
|
||||
op = await create_cassette_op(
|
||||
machine_id,
|
||||
CreateCassetteOpData(position=0, op_type="resume_cash_out"),
|
||||
created_by=user.id,
|
||||
)
|
||||
window = await get_cassette_ops_window(machine_id)
|
||||
try:
|
||||
await publish_ops_to_atm(machine, window, user.id)
|
||||
except OperatorIdentityMissing as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
except (SignerUnavailable, RelayUnavailable) as exc:
|
||||
raise HTTPException(
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
f"{exc} — the resume was recorded and will be delivered with the "
|
||||
"next publish",
|
||||
) from exc
|
||||
except CassetteTransportError as exc:
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) from exc
|
||||
return op
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue