refactor(api): unify typed error contract across draft pricing and publish flows
standardize typed error responses across draft, pricing and publish endpoints reduce contract drift between related flows keep client-side handling more predictable and consistent
This commit is contained in:
@@ -30,6 +30,7 @@ from app.schemas.pricing import (
|
||||
PricingCategoryUpdateResponse,
|
||||
)
|
||||
from app.security.auth import require_api_key
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
from app.services.draft_guard import validate_expected_draft_version_if_provided
|
||||
|
||||
router = APIRouter()
|
||||
@@ -213,9 +214,10 @@ async def create_price_rule_endpoint(
|
||||
try:
|
||||
amount = Decimal(payload.amount)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Некорректная сумма",
|
||||
raise_unprocessable(
|
||||
code="invalid_amount",
|
||||
message="Некорректная сумма",
|
||||
amount=payload.amount,
|
||||
)
|
||||
|
||||
price_rule_id = await create_price_rule(
|
||||
@@ -273,9 +275,10 @@ async def update_price_rule_endpoint(
|
||||
try:
|
||||
amount = Decimal(payload.amount)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Некорректная сумма",
|
||||
raise_unprocessable(
|
||||
code="invalid_amount",
|
||||
message="Некорректная сумма",
|
||||
amount=payload.amount,
|
||||
)
|
||||
|
||||
row = await update_price_rule(
|
||||
|
||||
39
backend/app/services/api_errors.py
Normal file
39
backend/app/services/api_errors.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
def raise_conflict(*, code: str, message: str, **extra) -> None:
|
||||
detail = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
**extra,
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
def raise_unprocessable(*, code: str, message: str, **extra) -> None:
|
||||
detail = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
**extra,
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
def raise_not_found(*, code: str, message: str, **extra) -> None:
|
||||
detail = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
**extra,
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=detail,
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.repositories.scheme_versions import get_current_scheme_version
|
||||
from app.repositories.schemes import get_scheme_record_by_scheme_id
|
||||
from app.services.api_errors import raise_conflict
|
||||
|
||||
|
||||
def build_stale_draft_version_detail(
|
||||
@@ -28,19 +27,19 @@ async def get_current_draft_context(
|
||||
)
|
||||
|
||||
if version.status != "draft" or scheme.status != "draft":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Current scheme version is not editable because it is not in draft state",
|
||||
raise_conflict(
|
||||
code="draft_not_editable",
|
||||
message="Current scheme version is not editable because it is not in draft state",
|
||||
scheme_status=scheme.status,
|
||||
scheme_version_status=version.status,
|
||||
actual_scheme_version_id=version.scheme_version_id,
|
||||
)
|
||||
|
||||
if expected_scheme_version_id and expected_scheme_version_id != version.scheme_version_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=build_stale_draft_version_detail(
|
||||
raise_conflict(**build_stale_draft_version_detail(
|
||||
expected_scheme_version_id=expected_scheme_version_id,
|
||||
actual_scheme_version_id=version.scheme_version_id,
|
||||
),
|
||||
)
|
||||
))
|
||||
|
||||
return scheme, version
|
||||
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.repositories.scheme_groups import list_scheme_version_groups
|
||||
from app.repositories.scheme_seats import list_scheme_version_seats
|
||||
from app.repositories.scheme_sectors import list_scheme_version_sectors
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
|
||||
|
||||
def _raise_uniqueness_error(message: str, detail: dict | None = None) -> None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=detail or {"code": "editor_uniqueness_error", "message": message},
|
||||
)
|
||||
payload = detail or {"code": "editor_uniqueness_error", "message": message}
|
||||
raise_unprocessable(**payload)
|
||||
|
||||
|
||||
def _raise_reference_error(message: str, detail: dict | None = None) -> None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=detail or {"code": "editor_reference_error", "message": message},
|
||||
)
|
||||
payload = detail or {"code": "editor_reference_error", "message": message}
|
||||
raise_unprocessable(**payload)
|
||||
|
||||
|
||||
async def validate_single_seat_patch_uniqueness(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.repositories.audit import create_audit_event
|
||||
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
|
||||
from app.services.draft_guard import get_current_draft_context
|
||||
from app.repositories.schemes import publish_scheme
|
||||
from app.services.api_errors import raise_conflict
|
||||
from app.services.scheme_validation import build_scheme_validation_report
|
||||
|
||||
|
||||
@@ -23,9 +22,11 @@ async def publish_current_draft_scheme(
|
||||
)
|
||||
|
||||
if not validation["summary"]["is_publishable"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Scheme is not publishable in current state",
|
||||
raise_conflict(
|
||||
code="publish_validation_failed",
|
||||
message="Scheme is not publishable in current state",
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
validation_summary=validation["summary"],
|
||||
)
|
||||
|
||||
snapshot = await replace_scheme_version_pricing_snapshot(
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.repositories.scheme_seats import (
|
||||
bulk_remap_scheme_version_seats,
|
||||
list_scheme_version_seats,
|
||||
)
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
from app.services.editor_validation import validate_remap_target_references
|
||||
|
||||
|
||||
@@ -35,9 +34,9 @@ async def preview_remap(
|
||||
to_group_id: str | None,
|
||||
) -> list[dict]:
|
||||
if not any([seat_record_ids, from_sector_id, from_group_id]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="At least one remap filter must be provided",
|
||||
raise_unprocessable(
|
||||
code="remap_filter_required",
|
||||
message="At least one remap filter must be provided",
|
||||
)
|
||||
|
||||
await validate_remap_target_references(
|
||||
|
||||
@@ -164,3 +164,18 @@ Additional draft read-side stale checks:
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}?expected_scheme_version_id={current_version_id} -> 200
|
||||
- same endpoints with stale expected_scheme_version_id -> 409
|
||||
|
||||
|
||||
## 11. Typed error contract
|
||||
|
||||
Validate representative failures return JSON detail objects with:
|
||||
- code
|
||||
- message
|
||||
|
||||
Recommended checks:
|
||||
- stale_draft_version -> 409
|
||||
- stale_current_version -> 409
|
||||
- duplicate_sector_id / duplicate_group_id -> 422
|
||||
- unknown_sector_id / unknown_group_id -> 422
|
||||
- invalid_amount -> 422
|
||||
- publish_validation_failed -> 409
|
||||
|
||||
|
||||
Reference in New Issue
Block a user