Product Catalog Prerequisites
Phase 6A.1 (2026-07-15/16). Product Catalog Templates shipped
categories only, deliberately deferring items because the real Product/ProductCategory model
depends on tenant master data — BusinessType, ProductDepartment, UnitOfMeasure — that no
seed/apply flow created for a new tenant. Live DB verification during Phase 6A confirmed this wasn't
hypothetical: the designated E2E test tenant genuinely had zero ProductDepartment rows. This phase
closes that gap.
The tenant DB hierarchy this unblocks
UnitOfMeasure must exist before ProductUnits; BusinessType must exist before ProductDepartment/
Product; ProductDepartment must exist before ProductCategory; ProductCategory must exist before
Product; Product must exist before its ProductUnits.
Schema (SaaS DB)
| Table | Purpose |
|---|---|
Saas.ProductCatalogPrerequisiteTemplates | Same shape as ProductCatalogTemplate — Code, Name/FName, BusinessActivityId FK, Version, IsDefault |
Saas.ProductCatalogPrerequisiteTemplateBusinessTypes | TemplateId FK, Code (template-internal identity only — the real BusinessType table has no Code column), Name/FName, IsDefault (exactly one per template) |
Saas.ProductCatalogPrerequisiteTemplateDepartments | TemplateId FK, Code (template-internal identity — the real ProductDepartment table has no Code column either), Name/FName, Description/FDescription |
Saas.ProductCatalogPrerequisiteTemplateUnits | TemplateId FK, Code (maps directly to the real UnitOfMeasure.UnitCode column — the one prerequisite where Code-based matching is genuinely possible), Name/FName, ConversionRate, IsDefault (marks the base "Piece" unit) |
Saas.TenantProductCatalogPrerequisiteTemplateApplyLogs | Central apply log |
Design simplification (deliberate): normally exactly one BusinessType row per template — modeled
as a child table for schema consistency with siblings and future extensibility, not because multiple
business types per template is a common real case. Departments don't carry their own BusinessType
reference — every department in a template is applied under that template's single default
BusinessType row.
Why UnitOfMeasure was included even though a fallback "Piece" seeder already exists
ApplicationDbSeeder.SeedUnitOfMeasuresAsync already creates one default unit
(Name="وحدة", FName="Piece", UnitCode="Pc", Conversation_Rate=1) per tenant. Per explicit
business direction, that seeder is treated as a minimum tenant fallback, not the source of truth —
Phase 6B needed a richer, business-activity-aware unit set (Box, Carton, Kilogram, Liter, Hour,
Service) the generic single-unit seeder was never meant to provide. Every template's base unit is
deliberately coded "Pc" so apply-time matching (by UnitCode, the real column) recognizes and skips
the tenant's already-seeded row instead of duplicating it. Live DB verification found the designated
test tenant had zero UnitOfMeasure rows at all at the time — a real-world confirmation that Units
genuinely needed to be in scope, not deferred as "already covered."
Apply order and idempotency
BusinessTypes first (the template's IsDefault item becomes the one every Department attaches to),
then Departments, then Units (fully independent of the other two — a BusinessType/Department failure
never blocks Units).
| Kind | Matched by | Reason |
|---|---|---|
| BusinessType | Name (case-insensitive) | Real table has no Code column |
| ProductDepartment | Name (case-insensitive) | Real table has no Code column |
| UnitOfMeasure | UnitCode | Real table's own column genuinely exists |
Force=false (default): an existing row is never touched. Force=true: fills only a currently-blank
FName — never the match key, never any structural field. Never deletes, never deactivates, never
resurrects a soft-deleted row (reports Failed). An ambiguous existing match (more than one row sharing
the same Name/UnitCode) blocks the whole apply rather than guessing. ResolvedBusinessTypeId and
ResolvedProductDepartmentId are exposed on the apply result specifically so a caller that just ran
this apply can immediately follow with a category apply targeting
the department it just created — see
Product Catalog — Starter Kit Integration.
API
api/Saas/ProductCatalogPrerequisiteTemplates (Swagger group MultiTenancy):
| Route | Permission |
|---|---|
GET Get / GetActive / GetByBusinessActivity/{code} / GetDefaultByBusinessActivity/{code} | .ViewAll |
GET {id}/Detail | .View |
POST Create / Copy | .Create |
PUT Update/{id} | .Edit |
DELETE Delete/{id} | .Delete |
POST Active/{id} / Inactive/{id} | .Activate / .Deactivate |
POST/PUT/DELETE BusinessTypes/*, Departments/*, Units/* | .Edit |
POST {id}/ApplyToTenant/Preview / POST {id}/ApplyToTenant | .Apply |
POST api/internal/product-catalog-prerequisite-templates/apply (Shumoul.Api) — same internal-key
bridge mechanism as every other apply flow in this initiative, no new secret.
Entitlement
Features.ProductsManagement — the same feature key used by
categories and items
apply. No new feature key was introduced.
Seed data
5 templates (one per activity), each IsDefault=true, one default BusinessType each, one Department
each, 18 units total across all 5 templates — every template's "Piece" unit coded Pc.
What was deliberately not done
No Product Items yet (closed by Phase 6B). No stock quantities, no opening stock, no inventory transactions, no accounting postings. Not auto-triggered by onboarding or first login on its own — only reachable via an explicit admin apply, or (Phase 6C) as the first step of the Tenant Starter Kit product chain.
