feat(v2): commission splits CRUD endpoints (P3c)
Adds 3 operator-scoped endpoints for managing the commission remainder
ruleset:
GET /api/v1/dca/commission-splits
— operator's default ruleset
GET /api/v1/dca/commission-splits?machine_id=X
— per-machine override (just the
override, not the default)
GET /api/v1/dca/commission-splits?machine_id=X&effective=true
— what the settlement processor
actually applies (override if
set, else operator default)
PUT /api/v1/dca/commission-splits — atomic replace; model validator
enforces legs sum to 1.0
DELETE /api/v1/dca/commission-splits — clear default (per-machine
overrides still apply)
DELETE /api/v1/dca/commission-splits?machine_id=X
— clear per-machine override
(falls back to default)
All routes verify operator owns the referenced machine (404 not 403 if
not). The DELETE path bypasses SetCommissionSplitsData's sum-to-1.0
validator by calling replace_commission_splits([]) directly, since an
empty ruleset is the correct "no rules" state — distribution.py logs a
warning and leaves operator_fee_sats in the machine wallet when this
happens.
28 routes registered total. 72/72 tests pass.
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b7f6f0a696
commit
e8dcbfe26e
1 changed files with 65 additions and 0 deletions
65
views_api.py
65
views_api.py
|
|
@ -19,12 +19,14 @@ from .crud import (
|
|||
delete_deposit,
|
||||
delete_machine,
|
||||
get_client_balance_summary,
|
||||
get_commission_splits,
|
||||
get_dca_client,
|
||||
get_dca_clients_for_machine,
|
||||
get_dca_clients_for_operator,
|
||||
get_deposit,
|
||||
get_deposits_for_client,
|
||||
get_deposits_for_operator,
|
||||
get_effective_commission_splits,
|
||||
get_machine,
|
||||
get_machines_for_operator,
|
||||
get_payments_for_operator,
|
||||
|
|
@ -32,6 +34,7 @@ from .crud import (
|
|||
get_settlements_for_machine,
|
||||
get_settlements_for_operator,
|
||||
get_super_config,
|
||||
replace_commission_splits,
|
||||
update_dca_client,
|
||||
update_deposit,
|
||||
update_deposit_status,
|
||||
|
|
@ -40,6 +43,7 @@ from .crud import (
|
|||
)
|
||||
from .models import (
|
||||
ClientBalanceSummary,
|
||||
CommissionSplit,
|
||||
CreateDcaClientData,
|
||||
CreateDepositData,
|
||||
CreateMachineData,
|
||||
|
|
@ -48,6 +52,7 @@ from .models import (
|
|||
DcaPayment,
|
||||
DcaSettlement,
|
||||
Machine,
|
||||
SetCommissionSplitsData,
|
||||
SuperConfig,
|
||||
UpdateDcaClientData,
|
||||
UpdateDepositData,
|
||||
|
|
@ -384,6 +389,66 @@ async def api_list_payments(
|
|||
return await get_payments_for_operator(user.id, leg_type=leg_type)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Commission splits — operator's rules for distributing the commission
|
||||
# remainder (post-super-fee). Sum-to-1.0 invariant enforced at the model
|
||||
# boundary by SetCommissionSplitsData.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
|
||||
)
|
||||
async def api_get_commission_splits(
|
||||
machine_id: str | None = None,
|
||||
effective: bool = False,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[CommissionSplit]:
|
||||
"""No machine_id: operator's default ruleset (rows where machine_id IS NULL).
|
||||
With machine_id: per-machine override only (404 the machine if not yours).
|
||||
With machine_id and ?effective=true: per-machine override if set, else
|
||||
operator default — what the settlement processor actually applies."""
|
||||
if machine_id is not None:
|
||||
await _machine_owned_by(machine_id, user.id)
|
||||
if effective:
|
||||
return await get_effective_commission_splits(user.id, machine_id)
|
||||
return await get_commission_splits(user.id, machine_id)
|
||||
return await get_commission_splits(user.id, None)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.put(
|
||||
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
|
||||
)
|
||||
async def api_replace_commission_splits(
|
||||
data: SetCommissionSplitsData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[CommissionSplit]:
|
||||
"""Atomic replace for the (operator, machine) scope. If
|
||||
data.machine_id is None, replaces the operator's default ruleset;
|
||||
otherwise replaces the per-machine override (machine must be owned).
|
||||
Sum-to-1.0 invariant enforced upstream by the Pydantic validator."""
|
||||
if data.machine_id is not None:
|
||||
await _machine_owned_by(data.machine_id, user.id)
|
||||
return await replace_commission_splits(user.id, data.machine_id, data.legs)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.delete(
|
||||
"/api/v1/dca/commission-splits",
|
||||
status_code=HTTPStatus.NO_CONTENT,
|
||||
)
|
||||
async def api_delete_commission_splits(
|
||||
machine_id: str | None = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> None:
|
||||
"""Clear a ruleset. With machine_id: clears the per-machine override
|
||||
(machine falls back to operator default). Without: clears the operator
|
||||
default (any per-machine overrides keep applying)."""
|
||||
if machine_id is not None:
|
||||
await _machine_owned_by(machine_id, user.id)
|
||||
# Atomic replace with an empty leg list — same effect as DELETE WHERE.
|
||||
await replace_commission_splits(user.id, machine_id, [])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Super config — operators read; super (LNbits instance admin) writes.
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue