Skip to main content

Units of Measure Module

Overview

The Units of Measure module manages measurement units used across inventory, purchasing, sales, and product catalog workflows. Units can be global (shared across all businesses, businessId = NULL) or business-scoped (owned by a specific business).

26 global units are seeded at database initialization (weight, volume, length, area, quantity). Businesses can create their own custom units and associate global units via the related Business Units of Measure module.


Domain Concepts

ConceptDescription
Unit of MeasureA measurement unit with a name, abbreviation, and optional description (e.g., Kilogramo / kg).
Global unitA unit with businessId = NULL, available for any business to link.
Business-scoped unitA unit with a specific businessId, visible only to that business.
Soft deleteUnits are deactivated (isActive = false) rather than hard-deleted to preserve historical references.

Architecture

This module follows Hexagonal Architecture (Ports & Adapters).

units-of-measure/
├── units-of-measure.module.ts
├── domain/
│ └── units-of-measure-repository.domain.ts # Repository port + types
├── application/
│ ├── units-of-measure.service.ts # Use cases + event handlers
│ └── events/
│ └── on-unit-of-measure-create.event.ts # Domain event
├── infrastructure/
│ └── units-of-measure.repository.ts # Kysely DB adapter
└── interfaces/
├── units-of-measure.controller.ts # HTTP routes
├── dtos/
│ ├── create-unit-of-measure.dto.ts
│ └── update-unit-of-measure.dto.ts
└── query/
└── paginate-units-of-measure.query.ts

Dependency flow: Controller → Service → Repository (via interface token)

The repository is injected via UNITS_OF_MEASURE_REPOSITORY symbol token, ensuring the service depends only on the domain interface — not the concrete Kysely implementation.


Database Schema

Table: unit_of_measure

ColumnTypeDescription
idUUID (PK)Auto-generated primary key
namestringDisplay name (e.g., "Kilogramo")
abbreviationstringShort code (e.g., "kg")
descriptionstring?Optional description
businessIdUUID? (FK)NULL = global unit; otherwise scoped to business
isActivebooleanSoft-delete flag (default: true)
createdAttimestampAuto-set on creation
createdByUUID? (FK)User who created the record
updatedAttimestamp?Set on update
updatedByUUID? (FK)User who last updated

Related tables:

  • business_unit_of_measure — Links businesses to available units
  • business_unit_conversion — Conversion factors between units within a business

API Endpoints

Base path: /units-of-measure

MethodPathDescription
POST/Create a unit of measure (global or business-scoped)
GET/List units (paginated, searchable, filterable)
GET/business/:businessIdList active units for a specific business
GET/with-relation/:businessIdList all units with business relationship metadata
GET/:idGet a single unit by ID
PATCH/:idPartially update a unit
DELETE/:idSoft-delete a unit

Query Parameters (GET /)

ParamRequiredDescription
businessIdNoFilter by business UUID. Omit for global units.
noRelatedToBusinessIdNoExclude global units already linked to this business.
searchNoCase-insensitive search across name, abbreviation, description.
pageNoPage number (default: 1).
sizeNoPage size (default: 10).
orderByNoSort field: name, abbreviation, or description.
orderNoSort direction: asc or desc.

Example: Create Unit

POST /units-of-measure
{
"name": "Pound",
"abbreviation": "lb",
"description": "A unit of weight equal to 16 oz.",
"createdBy": "550e8400-e29b-41d4-a716-446655440001"
}

Example: Create Business-Scoped Unit

POST /units-of-measure
{
"name": "Custom Box",
"abbreviation": "cbox",
"businessId": "550e8400-e29b-41d4-a716-446655440000",
"createdBy": "550e8400-e29b-41d4-a716-446655440001"
}

Event System

Published Events

EventTriggerConsumer
unit-of-measure.createAfter creating a unitBusinessUnitsOfMeasureService — auto-links business-scoped units

Consumed Events

EventSourceAction
business-unit-of-measure.deleteBusiness Units of Measure moduleReplaces product references to the deleted unit (if replacement specified), then soft-deletes the unit

  • Business Units of Measure — Manages many-to-many business ↔ unit associations
  • Business Unit Conversion — Manages conversion rules between units within a business
  • Products — Products reference unitOfMeasureId for their base selling/inventory unit

Design Decisions

  1. Global + business-scoped model: Global units reduce duplication; businesses get customization via their own units.
  2. Soft delete: Units are deactivated rather than removed to preserve referential integrity in historical orders, sales, and inventory records.
  3. Cascading replacement: When a business disables a unit, all products referencing it can be atomically reassigned to a replacement unit within a single transaction.
  4. Interface-based DI: The service depends on the IUnitsOfMeasureRepository interface via a symbol token, enabling easy testing and adapter swapping.