| """Pydantic schemas for VoiceLedger transactions.""" |
|
|
| from __future__ import annotations |
|
|
| from typing import Literal |
|
|
| from pydantic import BaseModel, Field, field_validator, model_validator |
|
|
|
|
| TransactionType = Literal[ |
| "sale", |
| "expense", |
| "inventory_purchase", |
| "customer_credit", |
| "customer_payment", |
| "unknown", |
| ] |
| PaymentStatus = Literal["paid", "unpaid", "credit", "unknown"] |
|
|
|
|
| class Transaction(BaseModel): |
| """Structured bookkeeping record parsed from a user's note.""" |
|
|
| transaction_type: TransactionType = Field( |
| default="unknown", |
| description="Business classification for the transaction.", |
| ) |
| item: str | None = Field(default=None, description="Product or expense item.") |
| quantity: float | None = Field(default=None, ge=0) |
| unit_price: float | None = Field(default=None, ge=0) |
| amount: float | None = Field(default=None, ge=0) |
| customer: str | None = Field(default=None) |
| payment_status: PaymentStatus = Field(default="unknown") |
| notes: str = Field(default="") |
| confidence: float = Field(default=0.0, ge=0.0, le=1.0) |
|
|
| @field_validator("item", "customer", mode="before") |
| @classmethod |
| def normalize_optional_text(cls, value: object) -> str | None: |
| """Trim optional text fields and convert blanks to None.""" |
| if value is None: |
| return None |
| text = str(value).strip() |
| return text or None |
|
|
| @field_validator("notes", mode="before") |
| @classmethod |
| def normalize_notes(cls, value: object) -> str: |
| """Store notes as a stripped string.""" |
| if value is None: |
| return "" |
| return str(value).strip() |
|
|
| @model_validator(mode="after") |
| def derive_amount(self) -> "Transaction": |
| """Derive amount from quantity and unit price when possible.""" |
| if self.amount is None and self.quantity is not None and self.unit_price is not None: |
| self.amount = round(self.quantity * self.unit_price, 2) |
| return self |
|
|