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
| Concept | Description |
|---|---|
| Unit of Measure | A measurement unit with a name, abbreviation, and optional description (e.g., Kilogramo / kg). |
| Global unit | A unit with businessId = NULL, available for any business to link. |
| Business-scoped unit | A unit with a specific businessId, visible only to that business. |
| Soft delete | Units 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
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Auto-generated primary key |
name | string | Display name (e.g., "Kilogramo") |
abbreviation | string | Short code (e.g., "kg") |
description | string? | Optional description |
businessId | UUID? (FK) | NULL = global unit; otherwise scoped to business |
isActive | boolean | Soft-delete flag (default: true) |
createdAt | timestamp | Auto-set on creation |
createdBy | UUID? (FK) | User who created the record |
updatedAt | timestamp? | Set on update |
updatedBy | UUID? (FK) | User who last updated |
Related tables:
business_unit_of_measure— Links businesses to available unitsbusiness_unit_conversion— Conversion factors between units within a business
API Endpoints
Base path: /units-of-measure
| Method | Path | Description |
|---|---|---|
POST | / | Create a unit of measure (global or business-scoped) |
GET | / | List units (paginated, searchable, filterable) |
GET | /business/:businessId | List active units for a specific business |
GET | /with-relation/:businessId | List all units with business relationship metadata |
GET | /:id | Get a single unit by ID |
PATCH | /:id | Partially update a unit |
DELETE | /:id | Soft-delete a unit |
Query Parameters (GET /)
| Param | Required | Description |
|---|---|---|
businessId | No | Filter by business UUID. Omit for global units. |
noRelatedToBusinessId | No | Exclude global units already linked to this business. |
search | No | Case-insensitive search across name, abbreviation, description. |
page | No | Page number (default: 1). |
size | No | Page size (default: 10). |
orderBy | No | Sort field: name, abbreviation, or description. |
order | No | Sort 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
| Event | Trigger | Consumer |
|---|---|---|
unit-of-measure.create | After creating a unit | BusinessUnitsOfMeasureService — auto-links business-scoped units |
Consumed Events
| Event | Source | Action |
|---|---|---|
business-unit-of-measure.delete | Business Units of Measure module | Replaces product references to the deleted unit (if replacement specified), then soft-deletes the unit |
Related Modules
- 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
unitOfMeasureIdfor their base selling/inventory unit
Design Decisions
- Global + business-scoped model: Global units reduce duplication; businesses get customization via their own units.
- Soft delete: Units are deactivated rather than removed to preserve referential integrity in historical orders, sales, and inventory records.
- Cascading replacement: When a business disables a unit, all products referencing it can be atomically reassigned to a replacement unit within a single transaction.
- Interface-based DI: The service depends on the
IUnitsOfMeasureRepositoryinterface via a symbol token, enabling easy testing and adapter swapping.