Design
Documentation for Modules
Product Catalog — Design Document
Full-featured business module with CRUD, variants (master-detail), workflow, media, cross-module integration, and embeddable widgets.
Architecture
Product Catalog is a complete business module — it owns its entities (Product, ProductVariant), has full CRUD pages, workflow-based status transitions, media integration, and exposes public actions and embeddable components for other modules.
product-catalog (Business Module)
├── Owns: Product, ProductVariant, ProductAttribute
├── Consumes: @libs-core/fiscal → TaxClass selector in product form
├── Consumes: @libs-core/brand-registry → Brand selector in product form
├── Consumes: @libs-core/taxonomy → Category selector, Tag input in product form
├── Consumes: @libs-core/workflow-engine → Status transitions (draft → active → discontinued)
├── Consumes: @libs-core/media → Product image upload and gallery
├── Consumes: @libs-core/module-base → CRUD composables, module contract, master-detail
├── Consumes: @libs-core/ui-framework → Shell components, form controls
├── Consumes: @libs-core/event-system → Domain event publishing/subscribing
├── Consumes: @libs-core/action-registry → Public action registration
└── Consumes: @libs-core/security → Guards, context service
Data Models
Product (Primary Entity)
interface Product extends BaseEntity {
tenantId: string;
name: string;
slug: string;
sku: string;
barcode?: string;
description?: string;
shortDescription?: string;
type: ProductType; // 'physical' | 'digital' | 'service'
status: ProductStatus; // 'draft' | 'active' | 'discontinued'
categoryId: string; // FK → Category (taxonomy)
brandId?: string; // FK → Brand (brand-registry)
tags: string[]; // Tag IDs (taxonomy)
unit?: UnitOfMeasure; // 'unit' | 'kg' | 'g' | 'lb' | 'oz' | 'l' | 'ml' | ...
taxClassId?: string; // FK → TaxClass (fiscal)
hasVariants: boolean;
basePrice: number;
costPrice?: number;
compareAtPrice?: number;
currency?: string;
weight?: number;
weightUnit?: string;
dimensions?: ProductDimensions; // { length, width, height, unit }
isOnlineAvailable?: boolean;
isFeatured?: boolean;
sortOrder?: number;
seo?: ProductSeo; // { metaTitle, metaDescription, metaKeywords }
notes?: string;
imageUrl?: string;
createdAt: string;
updatedAt: string;
}
ProductVariant (Child Entity — 1:N with Product)
interface ProductVariant extends BaseEntity {
productId: string; // FK → Product
sku: string;
barcode?: string;
attributeValues: Record<string, string>; // e.g., { "Size": "M", "Color": "Red" }
price: number;
costPrice?: number;
compareAtPrice?: number;
weight?: number;
isActive: boolean;
sortOrder: number;
imageId: string | null;
createdAt: string;
updatedAt: string;
}
ProductAttribute (Supporting Entity)
interface ProductAttribute {
id: string;
productId: string;
name: string; // e.g., "Size", "Color"
values: string[]; // e.g., ["S", "M", "L", "XL"]
sortOrder: number;
}
Permissions
| Permission ID | Description |
|---|---|
catalog:read |
View products and variants |
catalog:create |
Create new products |
catalog:update |
Edit existing products |
catalog:delete |
Delete products |
catalog:publish |
Publish products (draft → active) and reactivate (discontinued → active) |
catalog:manage |
Full catalog management including discontinuation |
catalog:variants:manage |
Manage product variants |
Workflow: Product Status
publish discontinue
┌───────┐ ────────► ┌──────┐ ────────► ┌──────────────┐
│ draft │ │active│ │ discontinued │
└───────┘ └──────┘ ◄──────── └──────────────┘
reactivate
Definition: PRODUCT_STATUS_WORKFLOW
| Transition | From | To | Permission | Confirmation |
|---|---|---|---|---|
| publish | draft | active | catalog:publish |
"Are you sure you want to publish this product? It will become visible to customers." |
| discontinue | active | discontinued | catalog:manage |
"Are you sure you want to discontinue this product? It will be removed from active listings." |
| reactivate | discontinued | active | catalog:publish |
— |
Integration: Uses <lib-status-transition> component in ProductDetailComponent. On transition completed, calls ProductApiService.updateProductStatus() and refreshes entity + workflow history.
Navigation: Hierarchical
navigation: {
menuItems: [
{
id: 'catalog-parent',
label: 'Catalog',
icon: 'box-open',
order: 300,
permissions: ['catalog:read'],
children: [
{ id: 'catalog-products', label: 'All Products', icon: 'list', route: 'list', order: 10, permissions: ['catalog:read'] },
{ id: 'catalog-new', label: 'New Product', icon: 'plus', route: 'create', order: 20, permissions: ['catalog:create'] },
],
},
],
},
Routes
| Path | Component | Permission | Data |
|---|---|---|---|
/catalog |
redirect → list |
— | — |
/catalog/list |
ProductListComponent | catalog:read |
title: 'Product Catalog' |
/catalog/create |
ProductFormComponent | catalog:create |
mode: 'create' |
/catalog/:id |
ProductDetailComponent | catalog:read |
title: 'Product Details' |
/catalog/:id/edit |
ProductFormComponent | catalog:update |
mode: 'edit' |
All routes guarded with: authGuard → contextGuard → permissionGuard(permission).
Workflow provider registered at route level:
{ path: '', providers: [provideProductWorkflow()], children: [...] }
Components
ProductListComponent
- Uses
useCrudList<Product>withProductApiService - Filters: type (select: physical/digital/service), status (select: draft/active/discontinued), search (text)
- Columns: name, sku, type, status (badge), basePrice (formatted), brand
- View modes: table (default), cards, compact
- Pagination: configurable page size
ProductDetailComponent
- Uses
useCrudDetail<Product>withProductApiService - Sections:
- Header: product name, SKU, status badge
- Status transition:
<lib-status-transition entityType="product" [entity]="product()"> - Info cards: pricing, dimensions, SEO
- Media gallery:
<lib-entity-media-section entityType="product-photo" [entityId]="product().id"> - Variants table:
<lib-variant-list [productId]="product().id"> - Workflow history: status transition timeline
ProductFormComponent
- Uses
useCrudForm<Product>withProductApiService - Sections:
- Basic info: name, slug, SKU, barcode, description
- Classification:
<lib-category-selector>,<lib-brand-selector>,<lib-tag-input> - Pricing: basePrice, costPrice, compareAtPrice, currency,
<lib-tax-class-selector> - Physical properties: weight, dimensions, unit of measure
- Options: type, isOnlineAvailable, isFeatured
- SEO: metaTitle, metaDescription, metaKeywords
- Media:
<lib-entity-media-section>(edit mode only)
VariantListComponent
- Embedded in ProductDetailComponent
- Uses
ProductVariantApiService - Displays variant table with: SKU, attribute values, price, status
- Inline add/edit/delete actions
- Attribute management (define attributes → generate combinations)
VariantFormComponent
- Create/edit form for a single variant
- Dynamic fields based on product's attribute definitions
- Fields: SKU, barcode, price, costPrice, weight, isActive
Master-Detail: Product → Variants
readonly crud = useMasterDetail<Product, ProductVariant>({
parentApiService: this.productApi,
childApiService: this.variantApi,
basePath: '/catalog',
childBasePath: 'variants',
childEntityName: 'ProductVariant',
});
The detail page shows:
- Parent product information at the top
- Child variants in a paginated table below
- Actions: add variant, edit variant (inline), delete variant, generate from attributes
Domain Events
Product Events
| Event Class | Event Name | Priority | Payload |
|---|---|---|---|
| ProductCreatedEvent | catalog.product.created |
normal | product, createdBy, tenantId |
| ProductUpdatedEvent | catalog.product.updated |
normal | product, previousData, updatedBy, tenantId |
| ProductDeletedEvent | catalog.product.deleted |
high | productId, productSku, deletedBy, tenantId |
| ProductStatusChangedEvent | catalog.product.status.changed |
normal | product, previousStatus, newStatus, changedBy, tenantId |
Variant Events
| Event Class | Event Name | Priority | Payload |
|---|---|---|---|
| VariantCreatedEvent | catalog.variant.created |
normal | variant, productId, createdBy, tenantId |
| VariantUpdatedEvent | catalog.variant.updated |
normal | variant, previousData, productId, updatedBy, tenantId |
| VariantDeletedEvent | catalog.variant.deleted |
high | variantId, variantSku, productId, deletedBy, tenantId |
Public Actions
| Action ID | Name | Category | Permission | Input | Output |
|---|---|---|---|---|---|
catalog:get-product |
Get Product | data | catalog:read |
{ productId } |
{ success, product? } |
catalog:search-products |
Search Products | data | catalog:read |
{ term, limit? } |
{ success, products? } |
catalog:get-variants |
Get Variants | data | catalog:read |
{ productId } |
{ success, variants? } |
catalog:create-product |
Create Product | data | catalog:create |
{ name, sku, type, categoryId, basePrice, ... } |
{ success, product? } |
Embeddable Components
ProductLookupComponent
- Widget ID:
catalog:product-lookup - Purpose: Search and select products (for use in other modules' forms, e.g., order line item selection)
- Inputs:
placeholder(string),multiSelect(boolean) - Output:
selectionChanged(Product[]) - Features: debounced search (300ms), min 2 chars, displays name + SKU + price
ProductCardComponent
- Widget ID:
catalog:product-card - Purpose: Display a product card (for dashboards, detail pages)
- Inputs:
product(Product),productId(string),showPrice(boolean),showStatus(boolean),compact(boolean) - Output:
productClicked(Product) - Features: fetches by ID if product not provided, status badge, compact mode
Mock API
Product Endpoints
| Method | Path | Query Params | Description |
|---|---|---|---|
| GET | /api/products |
page, limit, search, type[], status[], categoryId, brandId, tags[], minPrice, maxPrice, isFeatured, isOnlineAvailable, hasVariants | Paginated list with filters |
| GET | /api/products/search |
q, limit | Quick search (returns Product[]) |
| GET | /api/products/:id |
— | Get by ID |
| POST | /api/products |
— | Create (validates: name, sku, type, basePrice required; SKU unique) |
| PUT | /api/products/:id |
— | Update (partial) |
| DELETE | /api/products/:id |
— | Delete (cascades: removes variants and attributes) |
| PUT | /api/products/:id/status |
— | Update status only |
Variant Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/products/:productId/variants |
List variants for product |
| GET | /api/products/:productId/variants/:variantId |
Get variant by ID |
| POST | /api/products/:productId/variants |
Create variant (validates: sku required) |
| PUT | /api/products/:productId/variants/:variantId |
Update variant (partial) |
| DELETE | /api/products/:productId/variants/:variantId |
Delete variant |
Attribute Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/products/:productId/attributes |
Get all attributes for product |
| PUT | /api/products/:productId/attributes |
Replace all attributes |
| POST | /api/products/:productId/variants/generate |
Generate variants from attribute combinations |
Workflow History Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/workflow-history/product/:entityId |
Get transition history |
| POST | /api/workflow-history |
Record a transition |
Seed Data
15 products:
- 8 physical (2 with variants: T-Shirt with Size/Color, Sneakers with Size)
- 4 digital (e-books, software licenses)
- 3 service (consulting, training sessions)
- Distribution: 10 active, 3 draft, 2 discontinued
Services
ProductApiService
Extends BaseCrudApiService<Product>:
baseUrl:/api/products- Additional methods:
searchProducts(term, limit),updateProductStatus(id, status)
ProductVariantApiService
Standalone service (does not extend BaseCrudApiService — nested resource):
getVariantsByProduct(productId)→GET /api/products/:productId/variantscreateVariant(productId, data)→POST /api/products/:productId/variantsupdateVariant(productId, variantId, data)→PUT /api/products/:productId/variants/:variantIddeleteVariant(productId, variantId)→DELETE /api/products/:productId/variants/:variantIdgetAttributes(productId)→GET /api/products/:productId/attributessaveAttributes(productId, attributes)→PUT /api/products/:productId/attributesgenerateVariants(productId)→POST /api/products/:productId/variants/generate
VariantGeneratorService
Pure utility service:
generateCombinations(attributes: ProductAttribute[])→ cartesian product of attribute values
Building Block Integrations
| Building Block | Integration Point |
|---|---|
| Workflow Engine | PRODUCT_STATUS_WORKFLOW registered via provideProductWorkflow(). <lib-status-transition> in detail view. Status persisted via updateProductStatus(). |
| Media System | <lib-entity-media-section entityType="product-photo"> in detail and form (edit mode). Mock API routes for upload/gallery/delete. |
| Taxonomy Selectors | <lib-category-selector scope="product"> and <lib-tag-input scope="product"> in product form. |
| Brand Selector | <lib-brand-selector> in product form for brand assignment. |
| Fiscal Selector | <lib-tax-class-selector> in product form for tax class assignment. |
| Master-Detail | useMasterDetail<Product, ProductVariant> for variant management in detail page. |
Module Registration
@ModuleDecorator({
id: 'product-catalog',
version: '1.0.0',
name: 'Product Catalog',
description: 'Product catalog with CRUD, variants, workflow, and media',
basePath: 'catalog',
repositories: [ProductRepository, ProductVariantRepository],
relations: [
{
type: 'one-to-many',
sourceEntity: 'product',
targetEntity: 'product-variant',
foreignKey: 'productId',
displayField: 'sku',
},
],
permissions: [/* 7 permissions as listed above */],
navigation: {/* hierarchical as shown above */},
tags: ['product-catalog', 'crud', 'workflow', 'media', 'master-detail'],
})
Manifest includes:
- 4 public actions
- 2 embeddable components
- 7 domain events (product + variant lifecycle)