From b7f6f0a6965490137d2ee4890db88867254efdeb Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 14 May 2026 15:36:04 +0200 Subject: [PATCH] feat(v2): deposit CRUD + confirmation endpoints (P3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- views_api.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/views_api.py b/views_api.py index 62a87bc..ccab37a 100644 --- a/views_api.py +++ b/views_api.py @@ -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) # =============================================================================