Saltar al contenido principal

Addresses Module

Overview

The addresses module manages physical/postal address records for a business. Addresses are used across the platform wherever a structured location is required (e.g., supplier addresses, customer shipping addresses, business locations).

All address data is scoped to a businessId — no cross-business data access is possible.


Domain Concepts

ConceptDescription
AddressA structured postal address with a country reference, department, municipality, postal code, and up to two address lines
businessIdMulti-tenancy boundary. Every query filters by this field
countryIdReferences the country table (e.g., "gt" for Guatemala)
createdBy / updatedByAudit trail — employee/user UUID who performed the operation

Architecture

The module follows strict Hexagonal Architecture:

domain/
addresses-repository.domain.ts ← IAddressesRepository port (interface) + ADDRESSES_REPOSITORY token

application/
addresses.service.ts ← Orchestrates use cases (depends on domain port via @Inject)

infrastructure/
addresses.repository.ts ← Kysely adapter (implements the port)

interfaces/
addresses.controller.ts ← HTTP REST adapter
dtos/
create-address.dto.ts
update-address.dto.ts
upsert-address.dto.ts
query/
paginate-addresses.query.ts

Dependency flow

interfaces → application → domain ← infrastructure

Infrastructure depends on domain (implements its port); the application layer depends only on the domain interface. The repository is wired via a Symbol-based injection token (ADDRESSES_REPOSITORY) defined in the domain layer.

Known design debt: addresses-repository.domain.ts imports sortableAddressKeys from the interfaces layer. This is a consistent project-wide pattern (present in all modules) and is accepted as a pragmatic trade-off until a cross-module refactor is done.


Use Cases

Use CaseMethodHTTP
Create addresscreateAddressPOST /addresses
List addresses (paginated)getAllAddressesGET /addresses
Get address by IDgetAddressByIdGET /addresses/:id
Update addressupdateAddressPATCH /addresses/:id
Delete addressdeleteAddressDELETE /addresses/:id
Upsert addressupsertAddressPUT /addresses

Authorization

  • Authentication: Bearer token (Firebase) — validated by global AuthGuard
  • Authorization: RolesGuard + CASL permissions
    • Class-level: @PermissionResource(PolicyResource.Address)
    • Method-level: @PermissionAction(PolicyAction.Create | Read | Update | Delete)
  • Swagger: @ApiBearerAuth() marks all endpoints as requiring authentication

API Endpoints

POST /addresses — Create Address

Auth: Bearer token required Permission: Address:Create

Request body:

{
"countryId": "gt",
"department": "Chimaltenango",
"municipality": "Tecpan",
"postalCode": "00406",
"lineOne": "5ta calle 2-78",
"lineTwo": "Colonia El Naranjo",
"isActive": true,
"createdBy": "<user-uuid>",
"businessId": "<business-uuid>"
}

Response: 201 Created — full address object.


PUT /addresses — Upsert Address

Permission: Address:Update

When id is absent → creates a new address (requires createdBy). When id is present → updates the existing address (requires updatedBy).

Request body (create):

{
"businessId": "<business-uuid>",
"createdBy": "<user-uuid>",
"countryId": "gt",
"postalCode": "00406",
"lineOne": "5ta calle 2-78"
}

Request body (update):

{
"id": "<address-uuid>",
"businessId": "<business-uuid>",
"updatedBy": "<user-uuid>",
"postalCode": "00407"
}

Response: 200 OK — full address object.


GET /addresses — List Addresses

Permission: Address:Read

Query params:

ParamRequiredDescription
businessIdyesUUID — scopes the query
pagenoPage number (default: 1)
sizenoPage size (default: 20, 0 = all)
searchnoFull-text search across postalCode, municipality, department, lineOne, lineTwo
orderBynoColumn to sort by
ordernoasc or desc

Sortable columns: postalCode, municipality, department, lineOne, lineTwo

Response: 200 OKIOffsetPagination<Address>

{
"data": [...],
"total": 42,
"page": 1,
"size": 20,
"totalPages": 3
}

GET /addresses/:id — Get Address by ID

Permission: Address:Read

Query params: businessId (required)

Response: 200 OK — address object, or 404 Not Found.


PATCH /addresses/:id — Update Address

Permission: Address:Update

Request body:

{
"businessId": "<business-uuid>",
"updatedBy": "<user-uuid>",
"postalCode": "00407"
}

All fields except businessId and updatedBy are optional.

Response: 200 OK — updated address object.


DELETE /addresses/:id — Delete Address

Permission: Address:Delete

Query params: businessId (required)

Response: 200 OK (no body). Returns 404 if not found.


Design Decisions

Hard delete

DELETE performs a hard delete (deleteFrom). There is no soft-delete or isActive flag toggle on delete. The isActive field is managed manually through create/update operations.

businessId is not updatable

The PATCH endpoint destructures businessId out of the entity before passing it to the repository. This prevents accidentally moving an address from one business to another through a PATCH call.

findAll uses a transaction

The count + data query pair runs inside a Kysely transaction for snapshot isolation. This is a consistent project pattern even though it provides no correctness benefit for read-only queries.

Symbol-based repository injection

The repository is bound via ADDRESSES_REPOSITORY Symbol token in the module, and injected into the service via @Inject(ADDRESSES_REPOSITORY). This ensures the application layer depends only on the domain port (IAddressesRepository), not the concrete Kysely implementation.


Bruno API Collection

Located at: api-client/flowpos/collections/addresses/

FileMethodRoute
addresses.ymlGET/addresses
address.ymlPOST/addresses
upsert-address.ymlPUT/addresses
address by Id.ymlGET/addresses/:id
update-address.ymlPATCH/addresses/:id
delete-address.ymlDELETE/addresses/:id

Environment variables used: BASE_URL, ID_TOKEN, businessId, userId, addressId.

All requests include after-response test scripts for status code validation.