refactor(catalog)!: decouple materials from categories (v2.7)
Materials are now global and category-free. The category lives only on the supplier (suppliers.category_id); every category-facing query goes through the supplier edge instead of the material row.
Schema (migration.sql + init_db DDL):
- drop the materials.category_id column, its FK, and the v2.5 first-link-wins automation (fn_derive_material_category + trg_supplier_materials_derive_category) — the DDL runs idempotently at API startup
Backend:
- GET /materials?category_id=… now filters via EXISTS(supplier_materials ⋈ suppliers.category_id)
- quote category filters (quotes list) use the quote's supplier category
- /pm/on-the-way derives the display category from the supplier of the quote
- supplier_materials links no longer derive/return a material category
- expand_service: material expand=category resolves to null (kept for contract compatibility)
- material create/update no longer validates or stores category_id; seed.py aligned
Frontend:
- material creation no longer sends category_id
- formatMaterialTitle reworked to a strict order with « . » separator: [Brand] . [Material] . [Spec 1] . [Spec 2], brand deduped when the stored name already starts with it; specs separator switched from « · » to « . »
Validated: python compile, api + frontend image builds (tsc + vite) clean; branch cut from the tip of prod_v4 (= origin/production, 5a9ef40 — the team's merge made them identical).
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
"""Supplier ↔ material link service (v2.5) — the direct edge of the new flow.
|
||||
"""Supplier ↔ material link service (v2.6/v2.7) — the direct edge of the catalog flow.
|
||||
|
||||
Linking a material to a supplier derives materials.category_id from the
|
||||
supplier's category when the material has none (first-link-wins). The same
|
||||
rule is enforced by the DB trigger trg_supplier_materials_derive_category;
|
||||
this service applies it in the application layer so the ORM state is
|
||||
consistent inside the same transaction.
|
||||
Suppliers can supply any global material across categories.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -17,7 +13,7 @@ from app.utils.errors import NotFoundError, ValidationError
|
||||
async def link_supplier_material(
|
||||
db: AsyncSession, supplier_id, material_id, is_active: bool = True
|
||||
) -> SupplierMaterial:
|
||||
"""Upsert a supplier<->material link (idempotent) + derive category (first-link-wins).
|
||||
"""Upsert a supplier<->material link (idempotent).
|
||||
|
||||
Caller is responsible for the surrounding transaction.
|
||||
"""
|
||||
@@ -42,15 +38,11 @@ async def link_supplier_material(
|
||||
link = SupplierMaterial(supplier_id=supplier_id, material_id=material_id, is_active=is_active)
|
||||
db.add(link)
|
||||
await db.flush()
|
||||
|
||||
# First-link-wins: only when the material has no category yet.
|
||||
if material.category_id is None:
|
||||
material.category_id = supplier.category_id
|
||||
return link
|
||||
|
||||
|
||||
async def unlink_supplier_material(db: AsyncSession, supplier_id, material_id) -> None:
|
||||
"""Remove the link. The material's category is NEVER auto-cleared (documented rule)."""
|
||||
"""Remove the link."""
|
||||
link = await db.scalar(
|
||||
select(SupplierMaterial).where(
|
||||
SupplierMaterial.supplier_id == supplier_id,
|
||||
|
||||
Reference in New Issue
Block a user