Skip to main content

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: ProductsService orchestrates product CRUD, category validation, default variant creation, and barcode lifecycle. ProductInventoryListener reacts to inventory domain events to update product-level stock counters.
  • Infrastructure: ProductsRepository implements the domain port using Kysely queries against PostgreSQL.
  • Interfaces: ProductsController maps 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 default productVariant is auto-created on product creation.
  • With Variants (hasVariants: true): A catalog container. Sellable SKUs are productVariant records (e.g., size × color combinations).

Inventory Stock Fields​

Products track aggregate stock across multiple buckets:

  • quantity / cost — available stock
  • reservedStock / reservedStockCost — allocated for pending transfers
  • inTransitOutgoing / inTransitOutgoingCost — shipped, not yet received
  • inTransitIncoming / inTransitIncomingCost — expected incoming
  • pendingInspection / pendingInspectionCost — awaiting quality check
  • damaged / damagedCost — damaged goods
  • underRepair / underRepairCost — items being repaired

Barcode Lifecycle​

Simple products support barcode assignment with full lifecycle:

  1. Assign — validate format, check uniqueness, create assignment history
  2. Retire — mark barcode as inactive (preserves value)
  3. 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​

MethodPathDescription
POST/productsCreate a new product
GET/productsList products (paginated, filterable)
GET/products/:idGet product by ID
PATCH/products/:idUpdate a product
DELETE/products/:idDelete a product
PATCH/products/:id/barcodeAssign barcode
PATCH/products/:id/barcode/retireRetire barcode
PATCH/products/:id/barcode/reactivateReactivate barcode
GET/products/:id/collectionsGet product collections

Query Parameters (GET /products)​

ParameterTypeRequiredDescription
businessIdUUIDYesBusiness scope
searchstringNoSearch name, description, SKU, barcode
pagenumberNoPage number (1-based)
sizenumberNoPage size
orderBystringNoSort field: name, description, sku, barcode
orderstringNoSort direction: asc, desc
categoryIdUUIDNoFilter by category
brandIdUUIDNoFilter by brand
inventoryTypestringNoSingle type filter
inventoryTypesstringNoComma-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.

EventEffect on Product
OnCreateInventoryIncrease quantity + cost
OnInventoryDecreasedDecrease quantity (cost proportional)
OnInventoryIncreasedIncrease quantity + cost
OnInventoryReducedByReservedStockMove from quantity → reservedStock
OnReservedStockReducedByInTransitOutgoingMove from reservedStock → inTransitOutgoing
OnInTransitOutgoingReducedMove from inTransitOutgoing → quantity
OnStockIncreasedByInventoryAdjustmentIncrease quantity + cost
OnStockDecreasedByInventoryAdjustmentDecrease quantity
OnPendingInspectionIncreasedIncrease pendingInspection
OnPendingInspectionDecreasedDecrease pendingInspection
OnDamagedIncreasedIncrease damaged
OnDamagedDecreasedDecrease damaged
OnUnderRepairIncreasedIncrease underRepair
OnUnderRepairDecreasedDecrease underRepair
OnReservedStockDecreasedDecrease reservedStock

Design Decisions​

  1. Default variant auto-creation: Simple products get a default productVariant on creation so that all inventory, sales, and purchasing flows can consistently operate at the variant level.

  2. Deletion guard: Products with variants that have inventory or adjustment history cannot be deleted — they must be deactivated instead. This preserves audit trails.

  3. 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.

  4. 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.