Implement display artifacts, pricing integrity, draft base and publish preview bundle
This commit is contained in:
194
backend/app/schemas/editor.py
Normal file
194
backend/app/schemas/editor.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DraftSeatItem(BaseModel):
|
||||
seat_record_id: str
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
seat_id: str | None
|
||||
sector_id: str | None
|
||||
group_id: str | None
|
||||
row_label: str | None
|
||||
seat_number: str | None
|
||||
tag: str | None
|
||||
classes_raw: str | None
|
||||
x: float | None
|
||||
y: float | None
|
||||
cx: float | None
|
||||
cy: float | None
|
||||
width: float | None
|
||||
height: float | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class DraftSectorItem(BaseModel):
|
||||
sector_record_id: str
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
sector_id: str | None
|
||||
name: str | None
|
||||
classes_raw: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class DraftGroupItem(BaseModel):
|
||||
group_record_id: str
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
group_id: str | None
|
||||
name: str | None
|
||||
classes_raw: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class DraftStructureResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
status: str
|
||||
seats: list[DraftSeatItem]
|
||||
sectors: list[DraftSectorItem]
|
||||
groups: list[DraftGroupItem]
|
||||
total_seats: int
|
||||
total_sectors: int
|
||||
total_groups: int
|
||||
|
||||
|
||||
class SeatPatchRequest(BaseModel):
|
||||
seat_id: str | None = Field(default=None, max_length=128)
|
||||
sector_id: str | None = Field(default=None, max_length=128)
|
||||
group_id: str | None = Field(default=None, max_length=128)
|
||||
row_label: str | None = Field(default=None, max_length=64)
|
||||
seat_number: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class SeatPatchResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
seat_id: str | None
|
||||
sector_id: str | None
|
||||
group_id: str | None
|
||||
row_label: str | None
|
||||
seat_number: str | None
|
||||
|
||||
|
||||
class BulkSeatPatchItem(BaseModel):
|
||||
seat_record_id: str = Field(..., max_length=32)
|
||||
seat_id: str | None = Field(default=None, max_length=128)
|
||||
sector_id: str | None = Field(default=None, max_length=128)
|
||||
group_id: str | None = Field(default=None, max_length=128)
|
||||
row_label: str | None = Field(default=None, max_length=64)
|
||||
seat_number: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class BulkSeatPatchRequest(BaseModel):
|
||||
items: list[BulkSeatPatchItem] = Field(..., min_length=1, max_length=500)
|
||||
|
||||
|
||||
class BulkSeatPatchResultItem(BaseModel):
|
||||
seat_record_id: str
|
||||
updated_seat_id: str | None
|
||||
sector_id: str | None
|
||||
group_id: str | None
|
||||
row_label: str | None
|
||||
seat_number: str | None
|
||||
|
||||
|
||||
class BulkSeatPatchResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
updated_count: int
|
||||
items: list[BulkSeatPatchResultItem]
|
||||
|
||||
|
||||
class SectorPatchRequest(BaseModel):
|
||||
sector_id: str | None = Field(default=None, max_length=128)
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class SectorPatchResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
sector_id: str | None
|
||||
name: str | None
|
||||
|
||||
|
||||
class GroupPatchRequest(BaseModel):
|
||||
group_id: str | None = Field(default=None, max_length=128)
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class GroupPatchResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
element_id: str | None
|
||||
group_id: str | None
|
||||
name: str | None
|
||||
|
||||
|
||||
class CreateSectorRequest(BaseModel):
|
||||
element_id: str | None = Field(default=None, max_length=255)
|
||||
sector_id: str = Field(..., max_length=128)
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
classes_raw: str | None = Field(default=None, max_length=4000)
|
||||
|
||||
|
||||
class CreateGroupRequest(BaseModel):
|
||||
element_id: str | None = Field(default=None, max_length=255)
|
||||
group_id: str = Field(..., max_length=128)
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
classes_raw: str | None = Field(default=None, max_length=4000)
|
||||
|
||||
|
||||
class CreateSectorResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
sector_record_id: str
|
||||
element_id: str | None
|
||||
sector_id: str
|
||||
name: str | None
|
||||
|
||||
|
||||
class CreateGroupResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
group_record_id: str
|
||||
element_id: str | None
|
||||
group_id: str
|
||||
name: str | None
|
||||
|
||||
|
||||
class DeleteEntityResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
deleted: bool
|
||||
record_id: str
|
||||
|
||||
|
||||
class RepairReferencesResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
repaired_sector_refs_count: int
|
||||
repaired_group_refs_count: int
|
||||
details: dict
|
||||
|
||||
|
||||
class StructureDiffEntityItem(BaseModel):
|
||||
key: str
|
||||
status: str
|
||||
before: dict | None
|
||||
after: dict | None
|
||||
|
||||
|
||||
class StructureDiffResponse(BaseModel):
|
||||
scheme_id: str
|
||||
draft_scheme_version_id: str
|
||||
baseline_scheme_version_id: str | None
|
||||
summary: dict
|
||||
sectors: list[StructureDiffEntityItem]
|
||||
groups: list[StructureDiffEntityItem]
|
||||
seats: list[StructureDiffEntityItem]
|
||||
@@ -1,17 +1,40 @@
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
def _validate_decimal_amount(value: Decimal) -> Decimal:
|
||||
try:
|
||||
normalized = Decimal(value)
|
||||
except (InvalidOperation, TypeError, ValueError) as exc:
|
||||
raise ValueError("Некорректная сумма") from exc
|
||||
|
||||
if not normalized.is_finite():
|
||||
raise ValueError("Некорректная сумма")
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
class DeleteResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class PricingCategoryCreateRequest(BaseModel):
|
||||
name: str
|
||||
code: str | None = None
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
code: str | None = Field(default=None, max_length=128)
|
||||
|
||||
|
||||
class PricingCategoryUpdateRequest(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
code: str | None = Field(default=None, max_length=128)
|
||||
|
||||
|
||||
class PricingCategoryItem(BaseModel):
|
||||
pricing_category_id: str
|
||||
scheme_id: str
|
||||
name: str
|
||||
code: str | None = None
|
||||
code: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class PricingCategoryCreateResponse(BaseModel):
|
||||
@@ -28,98 +51,41 @@ class PricingCategoryUpdateResponse(BaseModel):
|
||||
code: str | None
|
||||
|
||||
|
||||
class DeleteResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class PriceRuleCreateRequest(BaseModel):
|
||||
pricing_category_id: str | None = None
|
||||
target_type: str
|
||||
target_ref: str
|
||||
pricing_category_id: str | None = Field(default=None, max_length=32)
|
||||
target_type: str = Field(..., pattern="^(seat|group|sector)$")
|
||||
target_ref: str = Field(..., min_length=1, max_length=128)
|
||||
amount: Decimal
|
||||
currency: str = "RUB"
|
||||
|
||||
@field_validator("target_type")
|
||||
@classmethod
|
||||
def validate_target_type(cls, value: str) -> str:
|
||||
allowed = {"sector", "group", "seat"}
|
||||
if value not in allowed:
|
||||
raise ValueError("Поле target_type должно быть одним из: sector, group, seat")
|
||||
return value
|
||||
|
||||
@field_validator("currency")
|
||||
@classmethod
|
||||
def validate_currency(cls, value: str) -> str:
|
||||
if value != "RUB":
|
||||
raise ValueError("В v1 поддерживается только валюта RUB")
|
||||
return value
|
||||
|
||||
@field_validator("amount", mode="before")
|
||||
@classmethod
|
||||
def parse_amount(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("Поле amount обязательно")
|
||||
text = str(value).strip()
|
||||
if text == "":
|
||||
raise ValueError("Поле amount обязательно")
|
||||
try:
|
||||
return Decimal(text)
|
||||
except (InvalidOperation, ValueError):
|
||||
raise ValueError("Некорректная сумма. Используйте формат 2500.00")
|
||||
currency: str = Field(default="RUB", min_length=3, max_length=8)
|
||||
|
||||
@field_validator("amount")
|
||||
@classmethod
|
||||
def validate_amount(cls, value: Decimal) -> Decimal:
|
||||
if value < Decimal("0.00"):
|
||||
raise ValueError("Сумма не может быть отрицательной")
|
||||
if value.quantize(Decimal("0.01")) != value:
|
||||
raise ValueError("Сумма должна быть с точностью до 2 знаков после запятой")
|
||||
return value
|
||||
return _validate_decimal_amount(value)
|
||||
|
||||
|
||||
class PriceRuleUpdateRequest(BaseModel):
|
||||
pricing_category_id: str | None = None
|
||||
target_type: str
|
||||
target_ref: str
|
||||
pricing_category_id: str | None = Field(default=None, max_length=32)
|
||||
target_type: str = Field(..., pattern="^(seat|group|sector)$")
|
||||
target_ref: str = Field(..., min_length=1, max_length=128)
|
||||
amount: Decimal
|
||||
currency: str = "RUB"
|
||||
|
||||
@field_validator("target_type")
|
||||
@classmethod
|
||||
def validate_target_type(cls, value: str) -> str:
|
||||
allowed = {"sector", "group", "seat"}
|
||||
if value not in allowed:
|
||||
raise ValueError("Поле target_type должно быть одним из: sector, group, seat")
|
||||
return value
|
||||
|
||||
@field_validator("currency")
|
||||
@classmethod
|
||||
def validate_currency(cls, value: str) -> str:
|
||||
if value != "RUB":
|
||||
raise ValueError("В v1 поддерживается только валюта RUB")
|
||||
return value
|
||||
|
||||
@field_validator("amount", mode="before")
|
||||
@classmethod
|
||||
def parse_amount(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("Поле amount обязательно")
|
||||
text = str(value).strip()
|
||||
if text == "":
|
||||
raise ValueError("Поле amount обязательно")
|
||||
try:
|
||||
return Decimal(text)
|
||||
except (InvalidOperation, ValueError):
|
||||
raise ValueError("Некорректная сумма. Используйте формат 2500.00")
|
||||
currency: str = Field(default="RUB", min_length=3, max_length=8)
|
||||
|
||||
@field_validator("amount")
|
||||
@classmethod
|
||||
def validate_amount(cls, value: Decimal) -> Decimal:
|
||||
if value < Decimal("0.00"):
|
||||
raise ValueError("Сумма не может быть отрицательной")
|
||||
if value.quantize(Decimal("0.01")) != value:
|
||||
raise ValueError("Сумма должна быть с точностью до 2 знаков после запятой")
|
||||
return value
|
||||
return _validate_decimal_amount(value)
|
||||
|
||||
|
||||
class PriceRuleItem(BaseModel):
|
||||
price_rule_id: str
|
||||
scheme_id: str
|
||||
pricing_category_id: str | None
|
||||
target_type: str
|
||||
target_ref: str
|
||||
amount: Decimal | str
|
||||
currency: str
|
||||
created_at: str
|
||||
|
||||
|
||||
class PriceRuleCreateResponse(BaseModel):
|
||||
@@ -142,30 +108,6 @@ class PriceRuleUpdateResponse(BaseModel):
|
||||
currency: str
|
||||
|
||||
|
||||
class PricingCategoryItem(BaseModel):
|
||||
pricing_category_id: str
|
||||
scheme_id: str
|
||||
name: str
|
||||
code: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
class PriceRuleItem(BaseModel):
|
||||
price_rule_id: str
|
||||
scheme_id: str
|
||||
pricing_category_id: str | None
|
||||
target_type: str
|
||||
target_ref: str
|
||||
amount: Decimal
|
||||
currency: str
|
||||
created_at: str
|
||||
|
||||
|
||||
class SchemePricingResponse(BaseModel):
|
||||
categories: List[PricingCategoryItem]
|
||||
rules: List[PriceRuleItem]
|
||||
|
||||
|
||||
class EffectiveSeatPriceResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
@@ -175,5 +117,15 @@ class EffectiveSeatPriceResponse(BaseModel):
|
||||
matched_rule_level: str
|
||||
matched_target_ref: str
|
||||
pricing_category_id: str | None
|
||||
amount: Decimal
|
||||
amount: Decimal | str
|
||||
currency: str
|
||||
|
||||
|
||||
class SchemePricingResponse(BaseModel):
|
||||
categories: list[PricingCategoryItem]
|
||||
rules: list[PriceRuleItem]
|
||||
|
||||
|
||||
class PricingBundleResponse(BaseModel):
|
||||
categories: list[PricingCategoryItem]
|
||||
rules: list[PriceRuleItem]
|
||||
|
||||
50
backend/app/schemas/publish_preview.py
Normal file
50
backend/app/schemas/publish_preview.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RemapPreviewRequest(BaseModel):
|
||||
seat_record_ids: list[str] | None = Field(default=None, max_length=500)
|
||||
from_sector_id: str | None = Field(default=None, max_length=128)
|
||||
to_sector_id: str | None = Field(default=None, max_length=128)
|
||||
from_group_id: str | None = Field(default=None, max_length=128)
|
||||
to_group_id: str | None = Field(default=None, max_length=128)
|
||||
|
||||
|
||||
class RemapPreviewSeatItem(BaseModel):
|
||||
seat_record_id: str
|
||||
seat_id: str | None
|
||||
before_sector_id: str | None
|
||||
after_sector_id: str | None
|
||||
before_group_id: str | None
|
||||
after_group_id: str | None
|
||||
|
||||
|
||||
class RemapPreviewResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
matched_count: int
|
||||
items: list[RemapPreviewSeatItem]
|
||||
|
||||
|
||||
class RemapApplyRequest(BaseModel):
|
||||
seat_record_ids: list[str] | None = Field(default=None, max_length=500)
|
||||
from_sector_id: str | None = Field(default=None, max_length=128)
|
||||
to_sector_id: str | None = Field(default=None, max_length=128)
|
||||
from_group_id: str | None = Field(default=None, max_length=128)
|
||||
to_group_id: str | None = Field(default=None, max_length=128)
|
||||
|
||||
|
||||
class RemapApplyResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
updated_count: int
|
||||
items: list[RemapPreviewSeatItem]
|
||||
|
||||
|
||||
class PublishPreviewResponse(BaseModel):
|
||||
scheme_id: str
|
||||
scheme_version_id: str
|
||||
artifacts: dict
|
||||
validation: dict
|
||||
structure_diff: dict
|
||||
pricing_coverage: dict
|
||||
summary: dict
|
||||
Reference in New Issue
Block a user