Businesses Module
Overview
The businesses module manages the business entity — the top-level unit in FlowPOS's multi-tenant hierarchy. Every location, employee, order, sale, and cash register session belongs to a business.
Domain Concepts
| Concept | Description |
|---|---|
| Business | Core entity. Identified by a UUID primary key. Holds configuration, branding, and e-invoicing settings. |
| onboardingStatus | Tracks where the business owner is in the setup wizard (pending → complete → done). |
| felCertifierConfig | JSON column that stores encrypted FEL (Guatemalan e-invoicing) certifier credentials. |
| felTaxData | JSON column for establishment-level FEL tax data (address, establishment code, VAT phrases). |
| FEL | Factura Electrónica en Línea — Guatemala's mandatory electronic invoicing system. See docs/fel/ for the full integration. |
Architecture
The module follows Hexagonal Architecture with strict layer boundaries:
domain/ ← Constants, ports, and interfaces (framework-agnostic)
application/ ← Use cases (service), domain event classes
infrastructure/ ← Kysely repository (DB adapter)
interfaces/ ← HTTP adapters (controller, DTOs, query objects)
Dependency flow
Controller → BusinessesService → IBusinessesRepository ← BusinessesRepository
↓
CryptoService (FEL credential encryption)
FelService (FEL config extraction helpers)
EventEmitter2 (emits business.create)
Domain purity
The domain layer (domain/) is framework-agnostic:
businesses.constants.ts— sortable column keys shared across layersbusinesses-repository.domain.ts— repository port with concrete method signatures (no Kysely expression builders)businesses-service.domain.ts— service port defining all use cases and domain helper contracts
No Kysely types, NestJS decorators, or infrastructure concerns appear in the domain layer.
Key Use Cases
Create Business
POST /businesses
- FEL credentials are extracted from the DTO and encrypted via
CryptoService.multiEncryptinsideBusinessesService.insertBusinessWithFel(). - The encrypted config is stored in
business.felCertifierConfig(JSON). OnCreateBusinessEvent(business.create) is emitted for downstream listeners.- The caller's Firebase custom claims are updated to assign the
Ownerrole for the new business.
Update Business (with FEL)
PATCH /businesses/:id
- FEL credentials are extracted and encrypted inside
BusinessesService.updateBusinessWithFel(). - For existing businesses, only changed credential values are re-encrypted.
- General fields (name, logo, etc.) are updated alongside FEL config.
Update Bill / FEL Configuration
PATCH /businesses/:id/bill
- If FEL credentials are provided, all three (
felAccessCode,felPassword,felUsername) must be present — partial updates are rejected with HTTP 400. - Tax ID is validated against the country's validator (via
taxIdValidatorByCountryIso2). - Credentials are encrypted and stored. Plain-text values are never persisted.
onboardingStatusis advanced tocompleteunless alreadydone.
Get Bill Config (Decrypted)
GET /businesses/:id/bill-config
Returns the business with FEL credentials decrypted in-memory for display in the configuration UI. Decryption is best-effort — if the config is incomplete (partially configured business) it is returned as-is with a warning log.
API Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
POST | /businesses | Create | Create a business |
GET | /businesses | Read | List businesses (paginated, filterable by createdBy) |
GET | /businesses/:id | Read | Get business by ID |
GET | /businesses/:id/bill-config | Read | Get business with decrypted FEL credentials |
PATCH | /businesses/:id | Update | Update general business fields (FEL encrypted in service) |
PATCH | /businesses/:id/bill | Update | Update bill / FEL configuration |
DELETE | /businesses/:id | Delete | Delete business (hard delete) |
All endpoints require a valid Firebase ID token (cookie flowpos-id-token or Authorization: Bearer <token>).
All :id parameters are validated as UUIDs via ParseUUIDPipe.
Pagination & Sorting (GET /businesses)
| Query param | Description |
|---|---|
page | Page number (1-based) |
size | Items per page |
search | Full-text search across name, taxId, legalName, legalAddress, messageIva, messageIsr, emailSender |
orderBy | Sort column: name, createdAt, updatedAt, isActive |
order | Sort direction: asc or desc |
createdBy | Filter by creator user UUID |
FEL Credential Security
FEL credentials are sensitive (third-party e-invoicing passwords and access codes). The system encrypts them before storage and decrypts them only on demand for display:
| Field stored in DB | Stored as |
|---|---|
felPassword | encryptedFelPassword (AES via CryptoService) |
felAccessCode | encryptedFelAccessCode |
felCertifierToken | encryptedFelToken |
felUsername | Plain text (not a secret) |
felCertifier | Plain text enum value |
The encryption key is ENCRYPTION_KEY (32-byte hex) from environment variables.
FEL encryption/decryption is handled entirely within BusinessesService — controllers do not interact with CryptoService directly.
Events
| Event name | Class | Emitted when | Payload |
|---|---|---|---|
business.create | OnCreateBusinessEvent | Business is successfully inserted | SelectableBusiness |
Module Dependencies
| Module | Purpose |
|---|---|
CryptoModule | AES encryption/decryption of FEL credentials |
DatabaseModule | Kysely database connection |
FirebaseModule | Updating Firebase custom claims on business creation |
UsersModule | Resolving dbUserId from the Firebase token in PATCH /bill |
FelModule | Extracting FEL certifier config structure from stored JSON |
Bruno API Collection
Pre-built requests are in api-client/flowpos/collections/businesses/.
| File | Endpoint |
|---|---|
businesses.yml | GET /businesses (with pagination/sort params) |
business.yml | POST /businesses |
business by Id.yml | GET /businesses/:id |
business by Id - bill config.yml | GET /businesses/:id/bill-config |
business config.yml | GET /businesses/:id (config inspection) |
update-business.yml | PATCH /businesses/:id |
business - bill config.yml | PATCH /businesses/:id/bill |
business - fel.yml | PATCH /businesses/:id (FEL-focused update) |
delete-business.yml | DELETE /businesses/:id |
Design Decisions
Hard delete
DELETE /businesses/:id performs a hard delete. There is currently no soft-delete (isActive = false) path in the delete endpoint. If a soft-delete is needed, updateBusiness can be used to set isActive: false.
FEL encryption encapsulated in service layer
FEL credential encryption is handled by insertBusinessWithFel() and updateBusinessWithFel() in BusinessesService. The controller passes DTOs directly to the service without touching CryptoService. This keeps controllers thin and business logic testable.
Firebase claims updated in controller (known limitation)
The Firebase custom-claims update after business creation happens inside the controller rather than in an event listener. This is a known architectural issue tracked as a future improvement. The ideal solution is a dedicated OnCreateBusinessListener that subscribes to business.create and handles the Firebase update asynchronously.
Domain layer is framework-agnostic
Repository and service domain interfaces use concrete method signatures (findByIds, findByUserId) instead of leaking Kysely expression builders. This ensures the domain layer can be tested and reasoned about without knowledge of the persistence framework.