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
| Concept | Description |
|---|---|
Address | A structured postal address with a country reference, department, municipality, postal code, and up to two address lines |
businessId | Multi-tenancy boundary. Every query filters by this field |
countryId | References the country table (e.g., "gt" for Guatemala) |
createdBy / updatedBy | Audit 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.tsimportssortableAddressKeysfrom 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 Case | Method | HTTP |
|---|---|---|
| Create address | createAddress | POST /addresses |
| List addresses (paginated) | getAllAddresses | GET /addresses |
| Get address by ID | getAddressById | GET /addresses/:id |
| Update address | updateAddress | PATCH /addresses/:id |
| Delete address | deleteAddress | DELETE /addresses/:id |
| Upsert address | upsertAddress | PUT /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)
- Class-level:
- 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:
| Param | Required | Description |
|---|---|---|
businessId | yes | UUID — scopes the query |
page | no | Page number (default: 1) |
size | no | Page size (default: 20, 0 = all) |
search | no | Full-text search across postalCode, municipality, department, lineOne, lineTwo |
orderBy | no | Column to sort by |
order | no | asc 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/
| File | Method | Route |
|---|---|---|
addresses.yml | GET | /addresses |
address.yml | POST | /addresses |
upsert-address.yml | PUT | /addresses |
address by Id.yml | GET | /addresses/:id |
update-address.yml | PATCH | /addresses/:id |
delete-address.yml | DELETE | /addresses/:id |
Environment variables used: BASE_URL, ID_TOKEN, businessId, userId, addressId.
All requests include after-response test scripts for status code validation.