Saltar al contenido principal

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: 200IOffsetPagination<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.