391 lines
18 KiB
Python
391 lines
18 KiB
Python
|
|
"""B2B ordering portal data model.
|
||
|
|
|
||
|
|
This module backs the private customer ordering portal. It deliberately reuses
|
||
|
|
the existing tenant/customer primitives rather than introducing parallel ones:
|
||
|
|
|
||
|
|
* A **customer/company** is an existing :class:`ClientAccount`.
|
||
|
|
* A **customer user** is an existing :class:`ClientUser` (owner/buyer/viewer/
|
||
|
|
accounts roles map onto the existing ``ordering`` module access levels).
|
||
|
|
|
||
|
|
Everything here is private: every row is tenant-scoped, and customer-facing
|
||
|
|
rows are additionally scoped to a ``client_account_id``. The catalogue itself is
|
||
|
|
global to the seller (one tenant), with per-customer visibility and pricing
|
||
|
|
layered on top.
|
||
|
|
|
||
|
|
Money is stored GST-exclusive (see ``CLAUDE.MD`` costing conventions) as floats,
|
||
|
|
matching the rest of the costing platform.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
from sqlalchemy import (
|
||
|
|
Boolean,
|
||
|
|
DateTime,
|
||
|
|
Float,
|
||
|
|
ForeignKey,
|
||
|
|
Integer,
|
||
|
|
String,
|
||
|
|
Text,
|
||
|
|
UniqueConstraint,
|
||
|
|
)
|
||
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
|
|
||
|
|
from app.db.session import Base
|
||
|
|
|
||
|
|
# --- Controlled vocabularies -------------------------------------------------
|
||
|
|
# Plain string constants (not DB enums) keep SQLite migrations trivial and match
|
||
|
|
# the existing model style.
|
||
|
|
|
||
|
|
PRODUCT_CATEGORIES = (
|
||
|
|
"grains",
|
||
|
|
"premixed",
|
||
|
|
"bags",
|
||
|
|
"bulk_loads",
|
||
|
|
"custom_blends",
|
||
|
|
"services",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Full internal order lifecycle. Customers see a simplified subset (see
|
||
|
|
# ``CUSTOMER_VISIBLE_STATUS`` in the ordering service).
|
||
|
|
ORDER_STATUSES = (
|
||
|
|
"draft",
|
||
|
|
"submitted",
|
||
|
|
"under_review",
|
||
|
|
"confirmed",
|
||
|
|
"sent_to_xero",
|
||
|
|
"in_production",
|
||
|
|
"ready_for_pickup",
|
||
|
|
"dispatched",
|
||
|
|
"completed",
|
||
|
|
"cancelled",
|
||
|
|
)
|
||
|
|
|
||
|
|
# How a resolved unit price was derived. Stored on each order line so future
|
||
|
|
# reporting can always explain which rule applied.
|
||
|
|
PRICE_SOURCES = (
|
||
|
|
"fixed", # CustomerProductPrice with rule_type fixed
|
||
|
|
"contract", # CustomerProductPrice with rule_type contract
|
||
|
|
"price_list", # PriceListItem via an assigned price list
|
||
|
|
"tiered", # a quantity tier won over the base rate
|
||
|
|
"base", # catalogue list price (optionally with customer discount)
|
||
|
|
"quote", # manual quote required; no automatic price
|
||
|
|
)
|
||
|
|
|
||
|
|
FULFILMENT_METHODS = ("delivery", "pickup")
|
||
|
|
|
||
|
|
|
||
|
|
class ProductCategory(Base):
|
||
|
|
__tablename__ = "product_categories"
|
||
|
|
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_product_category_slug"),)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
slug: Mapped[str] = mapped_column(String(64), index=True)
|
||
|
|
name: Mapped[str] = mapped_column(String(255))
|
||
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||
|
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class CatalogueProduct(Base):
|
||
|
|
"""A private catalogue product. Named ``CatalogueProduct`` to avoid clashing
|
||
|
|
with the existing costing ``Product`` (a separate concern)."""
|
||
|
|
|
||
|
|
__tablename__ = "catalogue_products"
|
||
|
|
__table_args__ = (UniqueConstraint("tenant_id", "sku", name="uq_catalogue_product_sku"),)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
name: Mapped[str] = mapped_column(String(255), index=True)
|
||
|
|
sku: Mapped[str] = mapped_column(String(64), index=True)
|
||
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
category: Mapped[str] = mapped_column(String(64), default="grains", index=True)
|
||
|
|
image_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||
|
|
unit_size: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
|
|
unit_of_measure: Mapped[str] = mapped_column(String(64), default="each")
|
||
|
|
min_order_quantity: Mapped[float] = mapped_column(Float, default=1.0)
|
||
|
|
# Base/list price, GST-exclusive. Used as the fallback price source and as
|
||
|
|
# the basis for a customer discount percentage.
|
||
|
|
base_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||
|
|
stock_status: Mapped[str] = mapped_column(String(32), default="in_stock")
|
||
|
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class CustomerProductVisibility(Base):
|
||
|
|
"""Per-customer override of catalogue visibility.
|
||
|
|
|
||
|
|
Products are visible to every customer by default (the catalogue is
|
||
|
|
"standard globally"). A row here with ``visible=False`` hides a product from
|
||
|
|
one customer; ``visible=True`` is an explicit allow (no-op unless a future
|
||
|
|
default flips to opt-in).
|
||
|
|
"""
|
||
|
|
|
||
|
|
__tablename__ = "customer_product_visibility"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint("client_account_id", "product_id", name="uq_customer_product_visibility"),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
|
||
|
|
product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
|
||
|
|
visible: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class PriceList(Base):
|
||
|
|
__tablename__ = "price_lists"
|
||
|
|
__table_args__ = (UniqueConstraint("tenant_id", "code", name="uq_price_list_code"),)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
code: Mapped[str] = mapped_column(String(64), index=True)
|
||
|
|
name: Mapped[str] = mapped_column(String(255))
|
||
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
items: Mapped[list["PriceListItem"]] = relationship(
|
||
|
|
back_populates="price_list",
|
||
|
|
cascade="all, delete-orphan",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class PriceListItem(Base):
|
||
|
|
__tablename__ = "price_list_items"
|
||
|
|
__table_args__ = (UniqueConstraint("price_list_id", "product_id", name="uq_price_list_item"),)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
price_list_id: Mapped[int] = mapped_column(ForeignKey("price_lists.id"), index=True)
|
||
|
|
product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
|
||
|
|
unit_price: Mapped[float] = mapped_column(Float)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
|
||
|
|
price_list: Mapped[PriceList] = relationship(back_populates="items")
|
||
|
|
|
||
|
|
|
||
|
|
class CustomerPriceAssignment(Base):
|
||
|
|
"""Links a customer to an assigned price list and a default discount."""
|
||
|
|
|
||
|
|
__tablename__ = "customer_price_assignments"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint("client_account_id", name="uq_customer_price_assignment"),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
|
||
|
|
price_list_id: Mapped[int | None] = mapped_column(ForeignKey("price_lists.id"), nullable=True, index=True)
|
||
|
|
# Default percentage discount applied to base prices for this customer when
|
||
|
|
# no more specific rule (fixed/contract/price-list) applies. 0..100.
|
||
|
|
discount_percent: Mapped[float] = mapped_column(Float, default=0.0)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
price_list: Mapped[PriceList | None] = relationship()
|
||
|
|
|
||
|
|
|
||
|
|
class CustomerProductPrice(Base):
|
||
|
|
"""A fixed or contract price for a specific customer + product. Highest
|
||
|
|
priority pricing rule."""
|
||
|
|
|
||
|
|
__tablename__ = "customer_product_prices"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint("client_account_id", "product_id", name="uq_customer_product_price"),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
|
||
|
|
product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
|
||
|
|
unit_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||
|
|
# "fixed" | "contract" | "quote" (quote forces a manual-quote workflow for
|
||
|
|
# this customer+product even if the product itself doesn't require one).
|
||
|
|
rule_type: Mapped[str] = mapped_column(String(32), default="fixed")
|
||
|
|
contract_reference: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class PriceTier(Base):
|
||
|
|
"""Quantity break for a pricing source. Exactly one of
|
||
|
|
``customer_product_price_id`` / ``price_list_item_id`` is set."""
|
||
|
|
|
||
|
|
__tablename__ = "price_tiers"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
customer_product_price_id: Mapped[int | None] = mapped_column(
|
||
|
|
ForeignKey("customer_product_prices.id"), nullable=True, index=True
|
||
|
|
)
|
||
|
|
price_list_item_id: Mapped[int | None] = mapped_column(
|
||
|
|
ForeignKey("price_list_items.id"), nullable=True, index=True
|
||
|
|
)
|
||
|
|
# Tier applies when ordered quantity >= min_quantity. The highest qualifying
|
||
|
|
# min_quantity wins.
|
||
|
|
min_quantity: Mapped[float] = mapped_column(Float)
|
||
|
|
unit_price: Mapped[float] = mapped_column(Float)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class Order(Base):
|
||
|
|
__tablename__ = "orders"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
client_account_id: Mapped[int] = mapped_column(ForeignKey("client_accounts.id"), index=True)
|
||
|
|
# Human-friendly order reference, assigned on submit (e.g. ORD-000123).
|
||
|
|
order_number: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||
|
|
status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
|
||
|
|
|
||
|
|
# Who created / submitted it (ClientUser ids; nullable for admin-created).
|
||
|
|
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("client_users.id"), nullable=True)
|
||
|
|
created_by_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||
|
|
submitted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||
|
|
|
||
|
|
# Customer-supplied order details.
|
||
|
|
purchase_order_number: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
|
|
delivery_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
requested_delivery_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||
|
|
fulfilment_method: Mapped[str] = mapped_column(String(32), default="delivery")
|
||
|
|
|
||
|
|
# Admin/internal handling.
|
||
|
|
admin_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
reopened: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
|
|
||
|
|
# Cached totals (authoritative figures live on lines; this is for listing).
|
||
|
|
subtotal_ex_gst: Mapped[float] = mapped_column(Float, default=0.0)
|
||
|
|
requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
|
|
||
|
|
# Xero integration tracking.
|
||
|
|
xero_status: Mapped[str] = mapped_column(String(32), default="not_sent")
|
||
|
|
xero_invoice_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
|
|
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
|
||
|
|
lines: Mapped[list["OrderLine"]] = relationship(
|
||
|
|
back_populates="order",
|
||
|
|
cascade="all, delete-orphan",
|
||
|
|
order_by="OrderLine.sort_order",
|
||
|
|
)
|
||
|
|
status_history: Mapped[list["OrderStatusHistory"]] = relationship(
|
||
|
|
back_populates="order",
|
||
|
|
cascade="all, delete-orphan",
|
||
|
|
order_by="OrderStatusHistory.created_at",
|
||
|
|
)
|
||
|
|
attachments: Mapped[list["OrderAttachment"]] = relationship(
|
||
|
|
back_populates="order",
|
||
|
|
cascade="all, delete-orphan",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class OrderLine(Base):
|
||
|
|
__tablename__ = "order_lines"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
|
||
|
|
product_id: Mapped[int] = mapped_column(ForeignKey("catalogue_products.id"), index=True)
|
||
|
|
|
||
|
|
# Snapshot of product identity at order time (so later catalogue edits don't
|
||
|
|
# rewrite historical orders).
|
||
|
|
product_name: Mapped[str] = mapped_column(String(255))
|
||
|
|
product_sku: Mapped[str] = mapped_column(String(64))
|
||
|
|
|
||
|
|
quantity: Mapped[float] = mapped_column(Float, default=1.0)
|
||
|
|
|
||
|
|
# Authoritative unit price, GST-exclusive, computed by the backend pricing
|
||
|
|
# engine and frozen at submission. Null only while quote-only.
|
||
|
|
unit_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||
|
|
line_total: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||
|
|
requires_quote: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
|
|
||
|
|
# Pricing provenance — enough to explain which rule applied in reporting.
|
||
|
|
price_source: Mapped[str] = mapped_column(String(32), default="base")
|
||
|
|
price_rule_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
|
|
discount_percent: Mapped[float] = mapped_column(Float, default=0.0)
|
||
|
|
|
||
|
|
# Admin override of the resolved unit price before confirmation.
|
||
|
|
admin_override_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||
|
|
admin_override_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
|
||
|
|
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||
|
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
|
||
|
|
order: Mapped[Order] = relationship(back_populates="lines")
|
||
|
|
|
||
|
|
|
||
|
|
class OrderStatusHistory(Base):
|
||
|
|
__tablename__ = "order_status_history"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
|
||
|
|
from_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
|
|
to_status: Mapped[str] = mapped_column(String(32))
|
||
|
|
actor_type: Mapped[str] = mapped_column(String(32), default="system")
|
||
|
|
actor_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||
|
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
order: Mapped[Order] = relationship(back_populates="status_history")
|
||
|
|
|
||
|
|
|
||
|
|
class OrderAttachment(Base):
|
||
|
|
__tablename__ = "order_attachments"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
|
||
|
|
# "purchase_order" | "confirmation_pdf" | "other"
|
||
|
|
kind: Mapped[str] = mapped_column(String(32), default="other")
|
||
|
|
filename: Mapped[str] = mapped_column(String(255))
|
||
|
|
content_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
|
|
url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||
|
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
order: Mapped[Order] = relationship(back_populates="attachments")
|
||
|
|
|
||
|
|
|
||
|
|
class NotificationSetting(Base):
|
||
|
|
"""Per-tenant notification configuration for the ordering portal."""
|
||
|
|
|
||
|
|
__tablename__ = "notification_settings"
|
||
|
|
__table_args__ = (UniqueConstraint("tenant_id", name="uq_notification_settings_tenant"),)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
# Comma-separated internal recipients notified on new order submissions.
|
||
|
|
internal_recipients: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
send_customer_confirmation: Mapped[bool] = mapped_column(Boolean, default=True)
|
||
|
|
require_po_number: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
|
|
from_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
|
|
||
|
|
|
||
|
|
class XeroSyncLog(Base):
|
||
|
|
__tablename__ = "xero_sync_log"
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||
|
|
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
|
||
|
|
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), index=True)
|
||
|
|
# "queued" | "success" | "failed"
|
||
|
|
status: Mapped[str] = mapped_column(String(32), default="queued")
|
||
|
|
request_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
xero_invoice_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
|
|
response_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|