Skip to main content

Business Units of Measure Module

Overview

The Business Units of Measure module manages the association between a business and the units of measure available to it. Each business has its own catalog of units (e.g., kg, lb, unit, each) that are used across inventory, purchasing, and sales workflows.

Two default units — unit and each — are automatically linked to every new business at creation time. When a business-scoped unit of measure is created through the global units-of-measure module, it is also automatically registered here.


Domain Concepts

ConceptDescription
Business Unit of MeasureA join record linking a business to a unitOfMeasure. Represents which units are available within a business.
unitOfMeasureIdUUID referencing a record in the unit_of_measure table.
businessIdScopes each record to a specific business (multi-tenancy).
isActiveWhen false the unit has been soft-deleted and is no longer available for selection in new transactions.

Architecture

This module follows Hexagonal Architecture (Ports & Adapters).

business-units-of-measure/
├── business-units-of-measure.module.ts
├── application/
│ ├── business-units-of-measure.service.ts # Use cases + event listeners
│ └── events/
│ └── on-delete-business-unit-of-measure.event.ts # Domain event (consumed by units-of-measure)
├── domain/
│ └── business-units-of-measure-repository.domain.ts # Repository port + sortable keys
├── infrastructure/
│ └── business-units-of-measure.repository.ts # Kysely adapter
└── interfaces/
├── business-units-of-measure.controller.ts # HTTP adapter
├── dtos/
│ ├── create-business-unit-of-measure.dto.ts
│ └── update-business-unit-of-measure.dto.ts
└── query/
└── paginate-business-units-of-measure.query.ts

Layer responsibilities

LayerFileRole
Domainbusiness-units-of-measure-repository.domain.tsDefines IBusinessUnitsOfMeasureRepository port and sortableBusinessUnitOfMeasureKeys. No framework dependencies.
Applicationbusiness-units-of-measure.service.tsOrchestrates CRUD use cases; listens to OnCreateBusinessEvent and OnUnitOfMeasureCreateEvent to auto-provision records.
Infrastructurebusiness-units-of-measure.repository.tsImplements the domain port using Kysely. Uses soft-delete (sets isActive: false) instead of hard-delete.
Interfacesbusiness-units-of-measure.controller.tsMaps HTTP requests to service calls; handles error propagation.

Dependency rule

interfaces → application → domain ← infrastructure

The domain defines what it needs (IBusinessUnitsOfMeasureRepository, BUSINESS_UNITS_OF_MEASURE_REPOSITORY token, BusinessUnitOfMeasureSortKey, and sortableBusinessUnitOfMeasureKeys). Infrastructure implements the port. The service depends only on the domain interface via @Inject(BUSINESS_UNITS_OF_MEASURE_REPOSITORY) — never on the concrete repository class.


Event Integrations

Consumed events

EventSource moduleAction
OnCreateBusinessEventbusinessesAuto-creates unit and each records for the new business.
OnUnitOfMeasureCreateEventunits-of-measureIf a UOM is created with a businessId, auto-creates the association for that business.

Published events

EventTriggerConsumers
OnDeleteBusinessUnitOfMeasureEventSoft-delete of a business UOMunits-of-measure service (handles cascading cleanup) — currently not emitted; infrastructure is in place for future activation.

Database

Table: business_unit_of_measure

ColumnTypeNotes
idUUIDPK, auto-generated
business_idUUIDFK → business(id)
unit_of_measure_idUUIDFK → unit_of_measure(id)
is_activeBOOLEANDefault: true; false = soft-deleted
created_atTIMESTAMPTZAuto-set
created_byUUIDFK → user(id)
updated_atTIMESTAMPTZNullable
updated_byUUIDNullable, FK → user(id)

API Endpoints

Base path: /business-units-of-measure

MethodPathDescriptionAuth
POST/Link a unit of measure to a businessRequired
GET/List (paginated, searchable)Required
GET/:idGet a single record by UUIDRequired
PATCH/:idPartially update a recordRequired
PATCH/disable-uom/:idSoft-delete (disable) a recordRequired

All endpoints are protected by RolesGuard with the BusinessUnitOfMeasure CASL resource. Fine-grained PermissionAction (Create / Read / Update / Delete) is enforced per endpoint.

Create — POST /business-units-of-measure

Request body:

{
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"unitOfMeasureId": "550e8400-e29b-41d4-a716-446655440010",
"isActive": true,
"createdBy": "550e8400-e29b-41d4-a716-446655440000"
}

Response: 201 — the created record.

List — GET /business-units-of-measure

Query parameters:

ParamTypeDescription
businessIdUUIDFilter to a specific business
searchstringPartial match on unit name, description, or abbreviation
pagenumberPage number (default: 1)
sizenumberPage size (default: 10; 0 = all)

Response: 200IOffsetPagination<SelectableBusinessUnitOfMeasure>.

Get by ID — GET /business-units-of-measure/:id

Query parameters:

ParamTypeRequiredDescription
businessIdUUIDYesScopes lookup to the business

Response: 200 — single record, 404 if not found.

Update — PATCH /business-units-of-measure/:id

Request body (all fields optional except businessId and updatedBy):

{
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"isActive": true,
"updatedBy": "550e8400-e29b-41d4-a716-446655440000"
}

Response: 200 — updated record.

Disable — PATCH /business-units-of-measure/disable-uom/:id

Query parameters:

ParamTypeRequiredDescription
businessIdUUIDYesScopes the operation to the business
userIdUUIDYesAudit trail — who performed the disable

Response: 200 — no body.


Design Decisions

Soft-delete instead of hard-delete

Units of measure may be referenced by historical transactions. Hard-deleting them would break referential integrity. Setting isActive: false preserves the data while preventing the unit from being selected in new transactions.

disable-uom/:id endpoint naming

The disable operation is a PATCH (not DELETE) because it modifies an existing record rather than removing it. The /disable-uom/ prefix avoids route ambiguity with PATCH /:id (general update).

sortableBusinessUnitOfMeasureKeys is empty

The association table itself has no user-facing sortable fields — sorting should be performed via the related unitOfMeasure join fields (name, abbreviation). An empty array is intentional and means the orderBy query parameter is a no-op. Future sortable fields can be added directly in domain/business-units-of-measure-repository.domain.ts.

Auto-provisioning via events

Rather than requiring callers to manually create associations, the module subscribes to OnCreateBusinessEvent to ensure every business always starts with a minimum viable UOM catalog. This keeps the creation flow side-effect-free for the caller.


Bruno API Collection

Collection files are located at:

api-client/flowpos/collections/business-units-of-measure/
FileRequest
list business units of measure.ymlGET / (list)
create business unit of measure.ymlPOST / (create)
get business unit of measure by id.ymlGET /:id
update business unit of measure.ymlPATCH /:id (update)
disable business unit of measure.ymlPATCH /disable-uom/:id (disable)

Use the {{businessUnitOfMeasureId}} global environment variable (set automatically by the POST response script) when running PATCH/disable requests.