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,
|
PricingCategoryUpdateResponse,
|
||||||
)
|
)
|
||||||
from app.security.auth import require_api_key
|
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
|
from app.services.draft_guard import validate_expected_draft_version_if_provided
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -213,9 +214,10 @@ async def create_price_rule_endpoint(
|
|||||||
try:
|
try:
|
||||||
amount = Decimal(payload.amount)
|
amount = Decimal(payload.amount)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(
|
raise_unprocessable(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
code="invalid_amount",
|
||||||
detail="Некорректная сумма",
|
message="Некорректная сумма",
|
||||||
|
amount=payload.amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
price_rule_id = await create_price_rule(
|
price_rule_id = await create_price_rule(
|
||||||
@@ -273,9 +275,10 @@ async def update_price_rule_endpoint(
|
|||||||
try:
|
try:
|
||||||
amount = Decimal(payload.amount)
|
amount = Decimal(payload.amount)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(
|
raise_unprocessable(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
code="invalid_amount",
|
||||||
detail="Некорректная сумма",
|
message="Некорректная сумма",
|
||||||
|
amount=payload.amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
row = await update_price_rule(
|
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.scheme_versions import get_current_scheme_version
|
||||||
from app.repositories.schemes import get_scheme_record_by_scheme_id
|
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(
|
def build_stale_draft_version_detail(
|
||||||
@@ -28,19 +27,19 @@ async def get_current_draft_context(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if version.status != "draft" or scheme.status != "draft":
|
if version.status != "draft" or scheme.status != "draft":
|
||||||
raise HTTPException(
|
raise_conflict(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
code="draft_not_editable",
|
||||||
detail="Current scheme version is not editable because it is not in draft state",
|
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:
|
if expected_scheme_version_id and expected_scheme_version_id != version.scheme_version_id:
|
||||||
raise HTTPException(
|
raise_conflict(**build_stale_draft_version_detail(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
expected_scheme_version_id=expected_scheme_version_id,
|
||||||
detail=build_stale_draft_version_detail(
|
actual_scheme_version_id=version.scheme_version_id,
|
||||||
expected_scheme_version_id=expected_scheme_version_id,
|
))
|
||||||
actual_scheme_version_id=version.scheme_version_id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return scheme, version
|
return scheme, version
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
|
|
||||||
from app.repositories.scheme_groups import list_scheme_version_groups
|
from app.repositories.scheme_groups import list_scheme_version_groups
|
||||||
from app.repositories.scheme_seats import list_scheme_version_seats
|
from app.repositories.scheme_seats import list_scheme_version_seats
|
||||||
from app.repositories.scheme_sectors import list_scheme_version_sectors
|
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:
|
def _raise_uniqueness_error(message: str, detail: dict | None = None) -> None:
|
||||||
raise HTTPException(
|
payload = detail or {"code": "editor_uniqueness_error", "message": message}
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
raise_unprocessable(**payload)
|
||||||
detail=detail or {"code": "editor_uniqueness_error", "message": message},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _raise_reference_error(message: str, detail: dict | None = None) -> None:
|
def _raise_reference_error(message: str, detail: dict | None = None) -> None:
|
||||||
raise HTTPException(
|
payload = detail or {"code": "editor_reference_error", "message": message}
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
raise_unprocessable(**payload)
|
||||||
detail=detail or {"code": "editor_reference_error", "message": message},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_single_seat_patch_uniqueness(
|
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.audit import create_audit_event
|
||||||
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
|
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
|
||||||
from app.services.draft_guard import get_current_draft_context
|
from app.services.draft_guard import get_current_draft_context
|
||||||
from app.repositories.schemes import publish_scheme
|
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
|
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"]:
|
if not validation["summary"]["is_publishable"]:
|
||||||
raise HTTPException(
|
raise_conflict(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
code="publish_validation_failed",
|
||||||
detail="Scheme is not publishable in current state",
|
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(
|
snapshot = await replace_scheme_version_pricing_snapshot(
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
|
|
||||||
from app.repositories.scheme_seats import (
|
from app.repositories.scheme_seats import (
|
||||||
bulk_remap_scheme_version_seats,
|
bulk_remap_scheme_version_seats,
|
||||||
list_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
|
from app.services.editor_validation import validate_remap_target_references
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +34,9 @@ async def preview_remap(
|
|||||||
to_group_id: str | None,
|
to_group_id: str | None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
if not any([seat_record_ids, from_sector_id, from_group_id]):
|
if not any([seat_record_ids, from_sector_id, from_group_id]):
|
||||||
raise HTTPException(
|
raise_unprocessable(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
code="remap_filter_required",
|
||||||
detail="At least one remap filter must be provided",
|
message="At least one remap filter must be provided",
|
||||||
)
|
)
|
||||||
|
|
||||||
await validate_remap_target_references(
|
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
|
- 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
|
- 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