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>
88 lines
3.1 KiB
Python
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)
|