Implement display artifacts, pricing integrity, draft base and publish preview bundle

This commit is contained in:
greebo
2026-03-19 17:58:17 +03:00
parent 85fb2f4bb9
commit c91c5abf15
35 changed files with 3283 additions and 302 deletions

View 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]

View File

@@ -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]

View 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