feat(v2): partial-dispense + operator notes on settlements (P3d)
Closes the v1 feature request satmachineadmin#3 (partial transaction
processing) and adds operator-authored audit notes on settlements.
Schema (m006_add_settlement_notes):
ALTER TABLE dca_settlements ADD COLUMN notes TEXT
The notes column is append-only (prepend with timestamp, never edit in
place). Stores both system-generated audit memos (partial-dispense
recompute provenance) and operator-authored free-form notes (cash-
drawer reconciliation context, off-LN refund records, etc.).
Partial-dispense endpoint:
POST /api/v1/dca/settlements/{id}/partial-dispense
body: PartialDispenseData {dispensed_fraction OR dispensed_sats, notes}
Recompute path (in distribution.apply_partial_dispense_and_redistribute):
1. Refuse if any leg has status='completed' (Lightning can't claw back)
2. Resolve new_gross from dispensed_fraction or dispensed_sats
3. Linear-scale net/commission/fiat — preserves the original commission
ratio exactly; only rounding may drift by 1 sat
4. Re-stage-1 split using the CURRENT super_fee_pct (super may have
changed the rate since the original landed)
5. Build a memo capturing original values + reason + new values
6. Void pending/failed legs (status → 'voided')
7. Overwrite the settlement's monetary fields + prepend memo to notes
8. Reset status to 'pending' → process_settlement re-runs distribution
Operator notes endpoint:
POST /api/v1/dca/settlements/{id}/notes
body: AppendSettlementNoteData {note}
Each operator note is timestamped (UTC) and tagged with the author's
user_id so the audit trail is accountable. Non-empty, max 2000 chars.
72/72 tests still pass. 30 routes total. The full-directory ruff number
ballooned to ~500 because it includes legacy transaction_processor.py
(orphaned, not imported anywhere) and other v1 cruft on the branch.
Files I actively maintain are clean.
Note: a richer queryable audit history (filter by author / time range /
action type / etc.) is being tracked as a separate future-work issue.
The notes-column approach here is the v1 audit story; the dedicated
history table will be additive.
Refs: aiolabs/satmachineadmin#9, closes #3 (in spirit, marked
once verified end-to-end)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e8dcbfe26e
commit
2883eb7b79
5 changed files with 352 additions and 3 deletions
67
views_api.py
67
views_api.py
|
|
@ -12,6 +12,7 @@ from lnbits.core.models import User
|
|||
from lnbits.decorators import check_super_user, check_user_exists
|
||||
|
||||
from .crud import (
|
||||
append_settlement_note,
|
||||
create_dca_client,
|
||||
create_deposit,
|
||||
create_machine,
|
||||
|
|
@ -41,7 +42,9 @@ from .crud import (
|
|||
update_machine,
|
||||
update_super_config,
|
||||
)
|
||||
from .distribution import apply_partial_dispense_and_redistribute
|
||||
from .models import (
|
||||
AppendSettlementNoteData,
|
||||
ClientBalanceSummary,
|
||||
CommissionSplit,
|
||||
CreateDcaClientData,
|
||||
|
|
@ -52,6 +55,7 @@ from .models import (
|
|||
DcaPayment,
|
||||
DcaSettlement,
|
||||
Machine,
|
||||
PartialDispenseData,
|
||||
SetCommissionSplitsData,
|
||||
SuperConfig,
|
||||
UpdateDcaClientData,
|
||||
|
|
@ -374,6 +378,69 @@ async def api_get_settlement(
|
|||
return settlement
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post(
|
||||
"/api/v1/dca/settlements/{settlement_id}/partial-dispense",
|
||||
response_model=DcaSettlement,
|
||||
)
|
||||
async def api_partial_dispense(
|
||||
settlement_id: str,
|
||||
data: PartialDispenseData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> DcaSettlement:
|
||||
"""Operator UX — resolves satmachineadmin#3.
|
||||
|
||||
Recompute the split for a settlement that didn't dispense the full
|
||||
amount (jam, mid-tx error). Provide one of dispensed_fraction (0..1)
|
||||
or dispensed_sats. Optionally include a reason in notes.
|
||||
|
||||
Refuses when any leg has already completed — Lightning payments can't
|
||||
be clawed back. Use balance settlement (P3e) for those cases.
|
||||
"""
|
||||
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 (data.dispensed_fraction is None) == (data.dispensed_sats is None):
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Provide exactly one of dispensed_fraction or dispensed_sats",
|
||||
)
|
||||
try:
|
||||
return await apply_partial_dispense_and_redistribute(settlement_id, data)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post(
|
||||
"/api/v1/dca/settlements/{settlement_id}/notes",
|
||||
response_model=DcaSettlement,
|
||||
)
|
||||
async def api_append_settlement_note(
|
||||
settlement_id: str,
|
||||
data: AppendSettlementNoteData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> DcaSettlement:
|
||||
"""Operator appends a free-form note to the settlement. Useful for cash-
|
||||
drawer reconciliation context, off-LN refund records, or any narrative
|
||||
an operator wants to attach. Each entry is timestamped (UTC) and tagged
|
||||
with the author's user id; existing entries are never modified.
|
||||
|
||||
For richer queryable audit (filter by author, time range, action type),
|
||||
see aiolabs/satmachineadmin (future audit-table feature)."""
|
||||
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")
|
||||
updated = await append_settlement_note(settlement_id, data.note, user.id)
|
||||
if updated is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found")
|
||||
return updated
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Payments (read-only — the leg-typed breakdown of distributions)
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue