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

ProductDetailComponent

ProductFormComponent

VariantListComponent

VariantFormComponent


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:


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

ProductCardComponent


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:


Services

ProductApiService

Extends BaseCrudApiService<Product>:

ProductVariantApiService

Standalone service (does not extend BaseCrudApiService — nested resource):

VariantGeneratorService

Pure utility service:


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: