Skip to main content

Business Units Conversion Module

Overview​

The Business Units Conversion module manages conversion factors between units of measure within a business. It enables the system to express quantities in different units across inventory, purchasing, and sales workflows (e.g., purchasing goods in pounds but tracking stock in kilograms).


Domain Concepts​

ConceptDescription
Conversion RuleA directional mapping from one unit (fromUnit) to another (toUnit) with a multiplication factor (conversionFactor).
fromUnit / toUnitUUIDs referencing records in the unit_of_measure table.
conversionFactorA positive decimal that, when multiplied by a quantity in fromUnit, yields the equivalent quantity in toUnit.
businessIdScopes each rule to a specific business (multi-tenancy).
isActiveFlags whether the rule is currently applicable.

Formula:

quantity_in_toUnit = quantity_in_fromUnit × conversionFactor

Example: 1 lb × 0.453592 = 0.453592 kg


Architecture​

This module follows Hexagonal Architecture (Ports & Adapters).

business-units-conversion/
├── business-units-conversion.module.ts
├── application/
│ └── business-units-conversion.service.ts # Use cases (orchestration)
├── domain/
│ └── business-units-conversion-repository.domain.ts # Repository port + sortable keys + injection token
├── infrastructure/
│ └── business-units-conversion.repository.ts # Kysely adapter
└── interfaces/
├── business-units-conversion.controller.ts # HTTP adapter
├── dtos/
│ ├── create-business-unit-conversion.dto.ts
│ └── update-business-unit-conversion.dto.ts
└── query/
└── paginate-business-units-conversion.query.ts

Layer responsibilities​

LayerFileRole
Domainbusiness-units-conversion-repository.domain.tsDefines the IBusinessUnitsConversionRepository port, the BUSINESS_UNITS_CONVERSION_REPOSITORY injection token, and the allowed sortable fields. No framework dependencies.
Applicationbusiness-units-conversion.service.tsOrchestrates CRUD use cases; generates paginated responses. Depends only on the domain interface via @Inject(BUSINESS_UNITS_CONVERSION_REPOSITORY).
Infrastructurebusiness-units-conversion.repository.tsImplements the domain port using Kysely. All queries are scoped by businessId for multi-tenancy isolation. Runs a transaction for findAll to fetch count and results atomically.
Interfacesbusiness-units-conversion.controller.tsMaps HTTP requests to service calls; handles error propagation. Protected by RolesGuard with PolicyResource.BusinessUnitConversion.

Dependency rule​

interfaces → application → domain ← infrastructure

The domain defines what it needs (IBusinessUnitsConversionRepository). Infrastructure implements it. The module wires the concrete repository via provide/useClass with the BUSINESS_UNITS_CONVERSION_REPOSITORY Symbol token. The application and interfaces layers both depend inward on the domain; the domain depends on nothing inside the application.


Database​

Table: business_unit_conversion

ColumnTypeNotes
idUUIDPK, auto-generated
business_idUUIDFK → business(id)
from_unit_idUUIDFK → unit_of_measure(id)
to_unit_idUUIDFK → unit_of_measure(id)
conversion_factorNUMERICMust be ≥ 0
descriptionVARCHARNullable
is_activeBOOLEANDefault: true
created_atTIMESTAMPTZAuto-set
created_byUUIDFK → user(id)
updated_atTIMESTAMPTZNullable
updated_byUUIDNullable, FK → user(id)

API Endpoints​

Base path: /business-units-conversion

MethodPathDescriptionAuth
POST/Create a new conversion ruleRequired
GET/List all rules (paginated, searchable)Required
GET/:id?businessId=Get a single rule by UUIDRequired
PATCH/:idPartially update a ruleRequired
DELETE/:id?businessId=Delete a ruleRequired

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

Create — POST /business-units-conversion​

Request body:

{
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"fromUnitId": "550e8400-e29b-41d4-a716-446655440010",
"toUnitId": "550e8400-e29b-41d4-a716-446655440011",
"conversionFactor": 0.453592,
"description": "Conversion from pounds to kilograms",
"isActive": true,
"createdBy": "550e8400-e29b-41d4-a716-446655440000"
}

Response: 201 — the created record.

List — GET /business-units-conversion​

Query parameters:

ParamTypeRequiredDescription
businessIdUUIDNoFilter by business
searchstringNoPartial match on description
pagenumberNoPage number (default: 1)
sizenumberNoPage size (default: 10; 0 = all)
orderBystringNodescription
orderstringNoasc or desc

Response: 200 — IOffsetPagination<SelectableBusinessUnitConversion>.

Get by ID — GET /business-units-conversion/:id​

Query parameters:

ParamTypeRequiredDescription
businessIdUUIDYesBusiness the record belongs to

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

Update — PATCH /business-units-conversion/:id​

Request body (all fields optional except updatedBy):

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

Response: 200 — updated record.

Delete — DELETE /business-units-conversion/:id​

Query parameters:

ParamTypeRequiredDescription
businessIdUUIDYesBusiness the record belongs to

Response: 200 — no body.


Design Decisions​

Interface-based injection​

The domain layer exports a BUSINESS_UNITS_CONVERSION_REPOSITORY Symbol token alongside the IBusinessUnitsConversionRepository interface. The module wires the concrete BusinessUnitsConversionRepository via provide/useClass, and the service injects the interface via @Inject(BUSINESS_UNITS_CONVERSION_REPOSITORY). This keeps the application layer decoupled from the infrastructure implementation and enables adapter swaps and testing with mocks.

Multi-tenancy scoping​

All repository queries (findAll, findById, update, delete) are scoped by businessId to enforce tenant isolation. The findAll method accepts businessId via the filter object; findById, update, and delete require it as a parameter via IRepositoryActionByIdWithBusiness / IRepositoryWithBusinessUpdate.

Sortable keys defined in domain, not interfaces​

sortableBusinessUnitConversionKeys is declared in business-units-conversion-repository.domain.ts and re-exported from the query file. This keeps the domain as the single source of truth about what fields are query-able.

description is optional​

The database schema declares description as nullable. The DTO correctly marks it @IsOptional() so conversion rules without a description are valid.

conversionFactor must be ≥ 0​

A @Min(0) constraint is enforced at the DTO level. Zero is technically allowed (a degenerate no-conversion edge case), but negative factors are rejected as meaningless.

Bidirectionality is not automatic​

A rule from A → B does not create an implicit B → A rule. Create a second rule with the inverse factor (1 / conversionFactor) if the reverse direction is needed.


Bruno API Collection​

Collection files are located at:

api-client/flowpos/collections/business-units-conversion/
FileRequest
business units of conversion.ymlGET / (list) — includes businessId query param
business unit of conversion.ymlPOST / (create)
business unit of conversion by Id.ymlGET /:id — requires businessId query param
update business unit of conversion.ymlPATCH /:id
delete business unit of conversion.ymlDELETE /:id — requires businessId query param

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