Files
data-entry-app/backend/app/seed_ordering.py
T
2026-06-11 23:56:02 +12:00

173 lines
6.0 KiB
Python

"""Idempotent seed data for the B2B ordering portal.
Creates a small demo catalogue, a demo ordering customer + buyer user, a price
list, and a customer-specific price so the acceptance-criteria flow works
out of the box. Safe to run on every startup — it no-ops once seeded.
"""
from __future__ import annotations
import logging
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.config import settings
from app.models.client_access import ClientAccount, ClientFeatureAccess, ClientUser
from app.models.ordering import (
CatalogueProduct,
CustomerPriceAssignment,
CustomerProductPrice,
NotificationSetting,
PriceList,
PriceListItem,
ProductCategory,
)
from app.services.client_access_service import (
MODULE_INDEX,
ensure_user_module_permissions,
)
logger = logging.getLogger("data_entry_app.seed")
ORDERING_TENANT = settings.client_tenant_id
_CATEGORIES = [
("grains", "Grains", 10),
("premixed", "Premixed Products", 20),
("bags", "Bags", 30),
("bulk_loads", "Bulk Loads", 40),
("custom_blends", "Custom Blends", 50),
("services", "Services & Delivery", 60),
]
_PRODUCTS = [
# (name, sku, category, uom, unit_size, moq, base_price, stock, requires_quote)
("Cracked Maize", "GRN-MAIZE-20", "grains", "20kg bag", "20kg", 1, 24.50, "in_stock", False),
("Whole Barley", "GRN-BARLEY-20", "grains", "20kg bag", "20kg", 1, 21.00, "in_stock", False),
("Layer Premix", "PMX-LAYER-20", "premixed", "20kg bag", "20kg", 1, 32.75, "in_stock", False),
("Calf Starter Premix", "PMX-CALF-20", "premixed", "20kg bag", "20kg", 5, 38.40, "low_stock", False),
("Bulka Bag (1T)", "BAG-BULKA-1T", "bags", "each", "1 tonne", 1, 9.50, "in_stock", False),
("Bulk Maize Load", "BLK-MAIZE-T", "bulk_loads", "tonne", "per tonne", 1, 685.00, "made_to_order", False),
("Custom Horse Blend", "CST-HORSE-20", "custom_blends", "20kg bag", "20kg", 10, None, "made_to_order", True),
("Delivery (per pallet)", "SVC-DELIVERY", "services", "pallet", "1 pallet", 1, 45.00, "in_stock", False),
]
def seed_ordering(db: Session) -> dict[str, int]:
created = {"categories": 0, "products": 0, "customer": 0, "pricing": 0}
# Categories
existing_categories = {
c.slug for c in db.scalars(select(ProductCategory).where(ProductCategory.tenant_id == ORDERING_TENANT)).all()
}
for slug, name, sort_order in _CATEGORIES:
if slug in existing_categories:
continue
db.add(ProductCategory(tenant_id=ORDERING_TENANT, slug=slug, name=name, sort_order=sort_order))
created["categories"] += 1
# Catalogue products
existing_skus = {
p.sku for p in db.scalars(select(CatalogueProduct).where(CatalogueProduct.tenant_id == ORDERING_TENANT)).all()
}
product_by_sku: dict[str, CatalogueProduct] = {}
for name, sku, category, uom, unit_size, moq, base_price, stock, requires_quote in _PRODUCTS:
if sku in existing_skus:
continue
product = CatalogueProduct(
tenant_id=ORDERING_TENANT,
name=name,
sku=sku,
category=category,
unit_of_measure=uom,
unit_size=unit_size,
min_order_quantity=moq,
base_price=base_price,
stock_status=stock,
requires_quote=requires_quote,
)
db.add(product)
product_by_sku[sku] = product
created["products"] += 1
db.flush()
# Notification settings row
if db.scalar(select(NotificationSetting).where(NotificationSetting.tenant_id == ORDERING_TENANT)) is None:
db.add(
NotificationSetting(
tenant_id=ORDERING_TENANT,
internal_recipients=settings.admin_email,
send_customer_confirmation=True,
require_po_number=False,
from_email=settings.admin_email,
)
)
# Demo ordering customer + buyer user
demo = db.scalar(select(ClientAccount).where(ClientAccount.client_code == "RIVERSIDE"))
if demo is None:
demo = ClientAccount(
tenant_id="riverside-stockfeeds",
name="Riverside Stockfeeds",
client_code="RIVERSIDE",
status="active",
notes="Demo B2B ordering customer",
)
db.add(demo)
db.flush()
created["customer"] += 1
info = MODULE_INDEX["ordering"]
db.add(
ClientFeatureAccess(
tenant_id=demo.tenant_id,
client_account_id=demo.id,
feature_key="ordering",
feature_name=info["module_name"],
feature_group=info["module_group"],
description=info["description"],
enabled=True,
)
)
buyer = ClientUser(
tenant_id=demo.tenant_id,
client_account_id=demo.id,
full_name="Riverside Buyer",
email="buyer@riverside.example",
role="buyer",
status="active",
is_new_user=False,
)
db.add(buyer)
db.flush()
ensure_user_module_permissions(db, buyer)
# A customer-specific contract price + a small discount on the rest.
maize = db.scalar(
select(CatalogueProduct).where(
CatalogueProduct.tenant_id == ORDERING_TENANT, CatalogueProduct.sku == "GRN-MAIZE-20"
)
)
if maize is not None:
db.add(
CustomerProductPrice(
tenant_id=ORDERING_TENANT,
client_account_id=demo.id,
product_id=maize.id,
unit_price=22.00,
rule_type="contract",
contract_reference="2026 supply agreement",
)
)
db.add(
CustomerPriceAssignment(
tenant_id=ORDERING_TENANT,
client_account_id=demo.id,
price_list_id=None,
discount_percent=5.0,
)
)
created["pricing"] += 1
return created