Skip to main content

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 OK — IOffsetPagination<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.