feat(v2): deposit CRUD + confirmation endpoints (P3b)

Adds 6 operator-scoped deposit endpoints:

  POST   /api/v1/dca/deposits                  — record fiat from an LP
                                                  (creator_user_id = the
                                                  operator who recorded)
  GET    /api/v1/dca/deposits                  — operator's deposits (all)
  GET    /api/v1/dca/deposits?client_id=X      — scoped to one LP
  GET    /api/v1/dca/deposits/{id}             — single
  PUT    /api/v1/dca/deposits/{id}             — edit (pending only)
  PUT    /api/v1/dca/deposits/{id}/status      — confirm/reject
  DELETE /api/v1/dca/deposits/{id}             — delete (pending only)

Cross-checks (client_id, machine_id) at create to prevent operators
binding deposits across machines incorrectly. Edits + deletes are
restricted to pending status so confirmed deposits become immutable
audit records (consistent with v1's existing behaviour from commit
28241e7).

Refs: aiolabs/satmachineadmin#9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 15:36:04 +02:00
commit b7f6f0a696

View file

@ -13,13 +13,18 @@ from lnbits.decorators import check_super_user, check_user_exists
from .crud import (
create_dca_client,
create_deposit,
create_machine,
delete_dca_client,
delete_deposit,
delete_machine,
get_client_balance_summary,
get_dca_client,
get_dca_clients_for_machine,
get_dca_clients_for_operator,
get_deposit,
get_deposits_for_client,
get_deposits_for_operator,
get_machine,
get_machines_for_operator,
get_payments_for_operator,
@ -28,19 +33,25 @@ from .crud import (
get_settlements_for_operator,
get_super_config,
update_dca_client,
update_deposit,
update_deposit_status,
update_machine,
update_super_config,
)
from .models import (
ClientBalanceSummary,
CreateDcaClientData,
CreateDepositData,
CreateMachineData,
DcaClient,
DcaDeposit,
DcaPayment,
DcaSettlement,
Machine,
SuperConfig,
UpdateDcaClientData,
UpdateDepositData,
UpdateDepositStatusData,
UpdateMachineData,
UpdateSuperConfigData,
)
@ -211,6 +222,111 @@ async def api_get_client_balance(
return summary
# =============================================================================
# Deposits — operator records fiat handed in by an LP at a machine.
# =============================================================================
async def _deposit_owned_by(deposit_id: str, user_id: str) -> DcaDeposit:
deposit = await get_deposit(deposit_id)
if deposit is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Deposit not found")
machine = await get_machine(deposit.machine_id)
if machine is None or machine.operator_user_id != user_id:
raise HTTPException(HTTPStatus.NOT_FOUND, "Deposit not found")
return deposit
@satmachineadmin_api_router.post(
"/api/v1/dca/deposits", response_model=DcaDeposit
)
async def api_create_deposit(
data: CreateDepositData, user: User = Depends(check_user_exists)
) -> DcaDeposit:
# Verify the (client_id, machine_id) pair belongs to the operator.
client = await _client_owned_by(data.client_id, user.id)
if client.machine_id != data.machine_id:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"client_id and machine_id refer to different machines",
)
return await create_deposit(user.id, data)
@satmachineadmin_api_router.get(
"/api/v1/dca/deposits", response_model=list[DcaDeposit]
)
async def api_list_deposits(
client_id: str | None = None,
user: User = Depends(check_user_exists),
) -> list[DcaDeposit]:
"""Operator's deposits across all their machines; ?client_id scopes to
a single LP (with ownership check)."""
if client_id is not None:
await _client_owned_by(client_id, user.id)
return await get_deposits_for_client(client_id)
return await get_deposits_for_operator(user.id)
@satmachineadmin_api_router.get(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
)
async def api_get_deposit(
deposit_id: str, user: User = Depends(check_user_exists)
) -> DcaDeposit:
return await _deposit_owned_by(deposit_id, user.id)
@satmachineadmin_api_router.put(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
)
async def api_update_deposit(
deposit_id: str,
data: UpdateDepositData,
user: User = Depends(check_user_exists),
) -> DcaDeposit:
existing = await _deposit_owned_by(deposit_id, user.id)
if existing.status != "pending":
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Only pending deposits can be edited",
)
updated = await update_deposit(deposit_id, data)
if updated is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Deposit not found")
return updated
@satmachineadmin_api_router.put(
"/api/v1/dca/deposits/{deposit_id}/status", response_model=DcaDeposit
)
async def api_update_deposit_status(
deposit_id: str,
data: UpdateDepositStatusData,
user: User = Depends(check_user_exists),
) -> DcaDeposit:
await _deposit_owned_by(deposit_id, user.id)
updated = await update_deposit_status(deposit_id, data)
if updated is None:
raise HTTPException(HTTPStatus.NOT_FOUND, "Deposit not found")
return updated
@satmachineadmin_api_router.delete(
"/api/v1/dca/deposits/{deposit_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_deposit(
deposit_id: str, user: User = Depends(check_user_exists)
) -> None:
existing = await _deposit_owned_by(deposit_id, user.id)
if existing.status != "pending":
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Only pending deposits can be deleted",
)
await delete_deposit(deposit_id)
# =============================================================================
# Settlements (read-only at this phase; landing happens in tasks.py)
# =============================================================================