Products Module
Overview
The products module manages the product catalog for the FlowPOS system. Products are the core entity in inventory, purchasing, and sales workflows. Each product belongs to a business (multi-tenancy) and is linked to a category, currency, brand, and unit of measure.
Architecture
products/
├── products.module.ts # Module definition
├── application/
│ ├── products.service.ts # CRUD + barcode use cases
│ └── product-inventory.listener.ts # Inventory event handlers
├── domain/
│ └── products-repository.domain.ts # Repository port (interface)
├── infrastructure/
│ └── products.repository.ts # Kysely DB adapter
└── interfaces/
├── products.controller.ts # HTTP endpoints
├── dtos/
│ ├── create-product.dto.ts
│ └── update-product.dto.ts
└── query/
└── paginate-products.query.ts
Layer Responsibilities
- Domain: Repository interface (
IProductsRepository) — defines CRUD and inventory stock adjustment contracts. - Application:
ProductsServiceorchestrates product CRUD, category validation, default variant creation, and barcode lifecycle.ProductInventoryListenerreacts to inventory domain events to update product-level stock counters. - Infrastructure:
ProductsRepositoryimplements the domain port using Kysely queries against PostgreSQL. - Interfaces:
ProductsControllermaps HTTP routes to service methods. DTOs validate request payloads.
Domain Concepts
Product
A product represents a catalog item with pricing, tax, and inventory metadata. Products can be:
- Simple (
hasVariants: false): A single sellable item. A defaultproductVariantis auto-created on product creation. - With Variants (
hasVariants: true): A catalog container. Sellable SKUs areproductVariantrecords (e.g., size × color combinations).
Inventory Stock Fields
Products track aggregate stock across multiple buckets:
quantity/cost— available stockreservedStock/reservedStockCost— allocated for pending transfersinTransitOutgoing/inTransitOutgoingCost— shipped, not yet receivedinTransitIncoming/inTransitIncomingCost— expected incomingpendingInspection/pendingInspectionCost— awaiting quality checkdamaged/damagedCost— damaged goodsunderRepair/underRepairCost— items being repaired
Barcode Lifecycle
Simple products support barcode assignment with full lifecycle:
- Assign — validate format, check uniqueness, create assignment history
- Retire — mark barcode as inactive (preserves value)
- Reactivate — re-enable a retired barcode if still available
Products with variants cannot have barcodes assigned directly — barcodes are assigned at the variant level.
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /products | Create a new product |
GET | /products | List products (paginated, filterable) |
GET | /products/:id | Get product by ID |
PATCH | /products/:id | Update a product |
DELETE | /products/:id | Delete a product |
PATCH | /products/:id/barcode | Assign barcode |
PATCH | /products/:id/barcode/retire | Retire barcode |
PATCH | /products/:id/barcode/reactivate | Reactivate barcode |
GET | /products/:id/collections | Get product collections |
Query Parameters (GET /products)
| Parameter | Type | Required | Description |
|---|---|---|---|
businessId | UUID | Yes | Business scope |
search | string | No | Search name, description, SKU, barcode |
page | number | No | Page number (1-based) |
size | number | No | Page size |
orderBy | string | No | Sort field: name, description, sku, barcode |
order | string | No | Sort direction: asc, desc |
categoryId | UUID | No | Filter by category |
brandId | UUID | No | Filter by brand |
inventoryType | string | No | Single type filter |
inventoryTypes | string | No | Comma-separated type filter |
Inventory Event Handling
The ProductInventoryListener subscribes to domain events emitted by the inventory module and updates product-level stock counters accordingly. All event handlers run within database transactions.
| Event | Effect on Product |
|---|---|
OnCreateInventory | Increase quantity + cost |
OnInventoryDecreased | Decrease quantity (cost proportional) |
OnInventoryIncreased | Increase quantity + cost |
OnInventoryReducedByReservedStock | Move from quantity → reservedStock |
OnReservedStockReducedByInTransitOutgoing | Move from reservedStock → inTransitOutgoing |
OnInTransitOutgoingReduced | Move from inTransitOutgoing → quantity |
OnStockIncreasedByInventoryAdjustment | Increase quantity + cost |
OnStockDecreasedByInventoryAdjustment | Decrease quantity |
OnPendingInspectionIncreased | Increase pendingInspection |
OnPendingInspectionDecreased | Decrease pendingInspection |
OnDamagedIncreased | Increase damaged |
OnDamagedDecreased | Decrease damaged |
OnUnderRepairIncreased | Increase underRepair |
OnUnderRepairDecreased | Decrease underRepair |
OnReservedStockDecreased | Decrease reservedStock |
Design Decisions
-
Default variant auto-creation: Simple products get a default
productVarianton creation so that all inventory, sales, and purchasing flows can consistently operate at the variant level. -
Deletion guard: Products with variants that have inventory or adjustment history cannot be deleted — they must be deactivated instead. This preserves audit trails.
-
Event-driven stock updates: Product-level stock counters are updated reactively via events rather than being called directly. This decouples the inventory and product modules.
-
Category ownership validation: When creating or updating a product, the category is validated to belong to the same business. This prevents cross-tenant data leakage.
Related Documentation
- Variant Management — product variant architecture
- Barcode Generation — barcode format and label printing
- Collections — merchandising collections
- Promotions — promotion engine
- Seasonal Markdown — markdown pricing