From b9f76b03fa94a6f4698b8194795125459a65710d Mon Sep 17 00:00:00 2001 From: Misaka Date: Sun, 24 May 2026 17:31:20 +0800 Subject: [PATCH] feat: add accessory Pydantic schemas Co-Authored-By: Claude Opus 4.6 --- app/schemas/accessory.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 app/schemas/accessory.py diff --git a/app/schemas/accessory.py b/app/schemas/accessory.py new file mode 100644 index 0000000..2647b3d --- /dev/null +++ b/app/schemas/accessory.py @@ -0,0 +1,135 @@ +# app/schemas/accessory.py +import re + +from pydantic import BaseModel, Field, field_validator + +PAICHAN_PATTERN = re.compile(r"^[A-Z]{1,2}\d{5}(-J)?$") +LOCATION_NORMAL_PATTERN = re.compile(r"^[A-Z]+\d+-\d+-\d+$") +LOCATION_TRANS_PATTERN = re.compile(r"^TRANS-\d+") + + +# ── Work order query ── + + +class WorkOrderGroup(BaseModel): + work_order_no: str + zongpai_nos: list[str] + + +class WorkOrderQueryResponse(BaseModel): + paichan_no: str + work_orders: list[WorkOrderGroup] + + +# ── Accessory CRUD ── + + +class AccessoryCreateRequest(BaseModel): + paichan_no: str + zongpai_no: str + accessory_type: str + quantity: int + location_code: str | None = None + + @field_validator("paichan_no") + @classmethod + def validate_paichan_no(cls, v: str) -> str: + v = v.strip().upper() + if not PAICHAN_PATTERN.match(v): + raise ValueError("INVALID_PAICHAN") + return v + + @field_validator("zongpai_no") + @classmethod + def validate_zongpai_no(cls, v: str) -> str: + v = v.strip().upper() + if not re.match(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$", v): + raise ValueError("INVALID_ZONGPAI") + return v + + @field_validator("quantity") + @classmethod + def validate_quantity(cls, v: int) -> int: + if v <= 0: + raise ValueError("INVALID_QUANTITY") + return v + + @field_validator("location_code") + @classmethod + def validate_location_code(cls, v: str | None) -> str | None: + if v is None: + return v + v = v.strip().upper() + if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v): + raise ValueError("INVALID_LOCATION") + return v + + +class AccessoryResponse(BaseModel): + id: int + paichan_no: str + zongpai_no: str + accessory_type: str + quantity: int + location_code: str | None = None + is_boxed: bool = False + created_at: str + + +class AccessoryListResponse(BaseModel): + paichan_no: str + items: list[AccessoryResponse] + + +class AccessoryUpdateRequest(BaseModel): + accessory_type: str | None = None + quantity: int | None = None + location_code: str | None = None + + @field_validator("quantity") + @classmethod + def validate_quantity(cls, v: int | None) -> int | None: + if v is not None and v <= 0: + raise ValueError("INVALID_QUANTITY") + return v + + @field_validator("location_code") + @classmethod + def validate_location_code(cls, v: str | None) -> str | None: + if v is None: + return v + v = v.strip().upper() + if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v): + raise ValueError("INVALID_LOCATION") + return v + + +# ── Accessory type management ── + + +class AccessoryTypeItem(BaseModel): + id: int + name: str + sort_order: int + + +class AccessoryTypeListResponse(BaseModel): + types: list[AccessoryTypeItem] + + +class AccessoryTypeCreateRequest(BaseModel): + name: str + sort_order: int = 0 + + @field_validator("name") + @classmethod + def validate_name(cls, v: str) -> str: + v = v.strip() + if not v: + raise ValueError("INVALID_TYPE_NAME") + return v + + +class AccessoryTypeUpdateRequest(BaseModel): + name: str | None = None + sort_order: int | None = None