Files
plm-lottery/app/api/routes/withdrawals.py
T
davideandClaude Opus 5 0cf35147ad Answer user-facing API failures with a machine-readable error code
The dashboard now speaks seven languages but every failure path still showed
the API's raw English text ("insufficient balance", "current password is
incorrect"), which is the most frequent and least forgiving part of the UI to
leave untranslated.

Rather than teach the API about locales, it keeps answering in one language
and hands the client something to translate: `detail` becomes
{code, message, params}, where message stays English for non-dashboard
consumers (curl, tests) and code maps onto `error.<code>` in i18n.js. An
unknown code falls back to message, so a client older or newer than the server
degrades to English instead of a blank toast.

Domain exceptions (BetError, WithdrawalError) subclass the new ApiError and
carry the code from where the failure actually happens; str(exc) is still the
English message, so existing tests keep matching on it. Interpolated values
travel in params rather than baked into the English sentence — amounts as
*_sats, from which the frontend derives a *_plm sibling, so each language can
place them wherever its grammar wants.

admin.js reads detail.message defensively: the admin endpoints still return a
bare string, but the shared auth dependencies now return the structured form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:44:39 +02:00

55 lines
1.7 KiB
Python

from fastapi import APIRouter, Depends, Request, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.errors import from_api_error, http_error
from app.auth.dependencies import get_current_user
from app.db.models import User
from app.db.session import get_session
from app.withdrawals.service import WithdrawalError, request_withdrawal
router = APIRouter(prefix="/withdrawals", tags=["withdrawals"])
class WithdrawalRequest(BaseModel):
external_address: str
amount_sats: int
class WithdrawalResponse(BaseModel):
txid: str
amount_requested_sats: int
amount_sent_sats: int
status: str
@router.post("", response_model=WithdrawalResponse, status_code=status.HTTP_201_CREATED)
async def create_withdrawal(
body: WithdrawalRequest,
request: Request,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> WithdrawalResponse:
listener = request.app.state.electrum_listener
if listener.client is None:
raise http_error(
status.HTTP_503_SERVICE_UNAVAILABLE,
"network_unavailable",
"not connected to the network, try again shortly",
)
async with request.app.state.user_locks.acquire(user.id):
try:
withdrawal = await request_withdrawal(
session, listener.client, user, body.external_address, body.amount_sats
)
except WithdrawalError as exc:
raise from_api_error(status.HTTP_400_BAD_REQUEST, exc) from exc
return WithdrawalResponse(
txid=withdrawal.txid,
amount_requested_sats=withdrawal.amount_requested_sats,
amount_sent_sats=withdrawal.amount_sent_sats,
status=withdrawal.status,
)