Files
lux_fiscal/backend/app/api/v1/receipts.py
T
lauadminandClaude Opus 5.5 518a99197f Add Checkbox ETTN receipts for Nova Poshta COD waybills
Cashier creates an ETTN receipt in Checkbox bound to the TTN with payment
control; Checkbox fiscalizes it itself when the parcel is paid for.

- cash_registers (Fernet-encrypted license key / PIN) and receipts tables
- Checkbox HTTP client + stub (ETTN does not work on test registers)
- two-phase create via ARQ job, timeout reconciliation, cron status polling
- /receipts and /cash-registers API, audit records
- dashboard: per-order and bulk create, prepayment, cancel; cash registers page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:39:00 +03:00

88 lines
3.1 KiB
Python

"""ЕТТН-чеки Checkbox: запрос создания (массово), просмотр, отмена."""
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select
from app.api.deps import (
CashierUser,
CheckboxClientDep,
SessionDep,
TaskQueueDep,
require_any,
)
from app.db.models.receipt import Receipt
from app.schemas.receipts import ReceiptCreateRequest, ReceiptCreateResponse, ReceiptOut
from app.services import receipts as receipts_service
from app.services.checkbox.client import CheckboxError
router = APIRouter(prefix="/receipts", tags=["receipts"], dependencies=[Depends(require_any)])
@router.post("", response_model=ReceiptCreateResponse, status_code=status.HTTP_202_ACCEPTED)
async def create_receipts(
payload: ReceiptCreateRequest,
request: Request,
session: SessionDep,
queue: TaskQueueDep,
actor: CashierUser,
) -> ReceiptCreateResponse:
result = await receipts_service.request_receipts(
session,
[(item.order_id, item.prepayment_kopecks) for item in payload.items],
user=actor,
request=request,
)
await session.commit()
# После commit: worker должен увидеть строку `pending`.
for receipt in result.created:
await queue.enqueue("create_ettn_receipt", str(receipt.id))
return ReceiptCreateResponse(
created=[ReceiptOut.from_receipt(receipt) for receipt in result.created],
errors=result.errors,
)
@router.get("", response_model=list[ReceiptOut])
async def list_receipts(session: SessionDep, order_id: str | None = None) -> list[ReceiptOut]:
query = select(Receipt).order_by(Receipt.created_at.desc()).limit(500)
if order_id is not None:
query = query.where(Receipt.order_id == order_id)
return [ReceiptOut.from_receipt(receipt) for receipt in await session.scalars(query)]
@router.get("/{receipt_id}", response_model=ReceiptOut)
async def get_receipt(receipt_id: uuid.UUID, session: SessionDep) -> ReceiptOut:
receipt = await session.get(Receipt, receipt_id)
if receipt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не найден")
return ReceiptOut.from_receipt(receipt)
@router.post("/{receipt_id}/cancel", response_model=ReceiptOut)
async def cancel_receipt(
receipt_id: uuid.UUID,
request: Request,
session: SessionDep,
checkbox: CheckboxClientDep,
actor: CashierUser,
) -> ReceiptOut:
try:
receipt = await receipts_service.cancel_receipt(
session, checkbox, receipt_id, user=actor, request=request
)
except receipts_service.ReceiptStateError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except CheckboxError as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Checkbox: {exc}"
) from exc
if receipt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Чек не найден")
return ReceiptOut.from_receipt(receipt)