aditya0103's picture
Initial commit: schemas, extraction engine, dataset prep, 54 passing tests
44c2f50
Raw
History Blame Contribute Delete
3.05 kB
"""Receipt schema — for consumer receipts (retail, restaurant, gas, etc.).
Receipts are simpler than invoices: no PO, no billing party, no payment terms.
Typically we care about merchant, transaction time, line items, and totals.
"""
from __future__ import annotations
from datetime import date, time
from pydantic import Field, field_validator
from src.schemas.base import StrictModel
from src.schemas.common import Address, MoneyAmount, normalize_currency, round_money
class ReceiptLineItem(StrictModel):
"""A row on a consumer receipt — usually less structured than an invoice line."""
description: str = Field(description="What was purchased.")
quantity: float | None = Field(default=None, ge=0)
unit_price: MoneyAmount | None = None
total: MoneyAmount | None = None
@field_validator("unit_price", "total", mode="before")
@classmethod
def _round(cls, v):
return round_money(v) if v is not None else v
class Receipt(StrictModel):
"""A consumer-facing receipt.
Required fields: merchant name, total, currency. Everything else optional
since many receipts omit fields (e.g. gas receipts often lack line items).
"""
# Merchant
merchant: str = Field(description="Merchant / business name as printed on the receipt.")
merchant_address: Address | None = Field(
default=None, description="Structured address of the merchant if legible."
)
merchant_phone: str | None = None
# Transaction
transaction_date: date | None = Field(
default=None, description="Date of the transaction (ISO 8601)."
)
transaction_time: time | None = Field(
default=None, description="Time of day (24h, ISO 8601)."
)
receipt_number: str | None = Field(
default=None, description="Receipt / transaction / order number, if present."
)
# Items and totals
line_items: list[ReceiptLineItem] = Field(
default_factory=list, description="Purchased items. Empty is allowed for e.g. lump-sum receipts."
)
subtotal: MoneyAmount | None = None
tax: MoneyAmount | None = None
tip: MoneyAmount | None = Field(default=None, description="Gratuity, if applicable (restaurants).")
total: MoneyAmount = Field(description="Grand total charged.")
# Payment
currency: str = Field(description="ISO 4217 currency code (e.g. USD).")
payment_method: str | None = Field(
default=None,
description="Free text: 'Visa **** 1234', 'Cash', 'Apple Pay', etc.",
)
# --- Validators ---
@field_validator("subtotal", "tax", "tip", "total", mode="before")
@classmethod
def _round_money(cls, v):
return round_money(v) if v is not None else v
@field_validator("currency", mode="before")
@classmethod
def _normalize_currency(cls, v):
return normalize_currency(v)
@field_validator("total")
@classmethod
def _total_nonneg(cls, v):
if v < 0:
raise ValueError(f"Receipt total cannot be negative: {v}")
return v