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.