Skip to main content

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

ConceptDescription
BusinessCore entity. Identified by a UUID primary key. Holds configuration, branding, and e-invoicing settings.
onboardingStatusTracks where the business owner is in the setup wizard (pending → complete → done).
felCertifierConfigJSON column that stores encrypted FEL (Guatemalan e-invoicing) certifier credentials.
felTaxDataJSON column for establishment-level FEL tax data (address, establishment code, VAT phrases).
FELFactura 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 layers
  • businesses-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

  1. FEL credentials are extracted from the DTO and encrypted via CryptoService.multiEncrypt inside BusinessesService.insertBusinessWithFel().
  2. The encrypted config is stored in business.felCertifierConfig (JSON).
  3. OnCreateBusinessEvent (business.create) is emitted for downstream listeners.
  4. The caller's Firebase custom claims are updated to assign the Owner role for the new business.

Update Business (with FEL)

PATCH /businesses/:id

  1. FEL credentials are extracted and encrypted inside BusinessesService.updateBusinessWithFel().
  2. For existing businesses, only changed credential values are re-encrypted.
  3. General fields (name, logo, etc.) are updated alongside FEL config.

Update Bill / FEL Configuration

PATCH /businesses/:id/bill

  1. If FEL credentials are provided, all three (felAccessCode, felPassword, felUsername) must be present — partial updates are rejected with HTTP 400.
  2. Tax ID is validated against the country's validator (via taxIdValidatorByCountryIso2).
  3. Credentials are encrypted and stored. Plain-text values are never persisted.
  4. onboardingStatus is advanced to complete unless already done.

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

MethodPathPermissionDescription
POST/businessesCreateCreate a business
GET/businessesReadList businesses (paginated, filterable by createdBy)
GET/businesses/:idReadGet business by ID
GET/businesses/:id/bill-configReadGet business with decrypted FEL credentials
PATCH/businesses/:idUpdateUpdate general business fields (FEL encrypted in service)
PATCH/businesses/:id/billUpdateUpdate bill / FEL configuration
DELETE/businesses/:idDeleteDelete 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 paramDescription
pagePage number (1-based)
sizeItems per page
searchFull-text search across name, taxId, legalName, legalAddress, messageIva, messageIsr, emailSender
orderBySort column: name, createdAt, updatedAt, isActive
orderSort direction: asc or desc
createdByFilter 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 DBStored as
felPasswordencryptedFelPassword (AES via CryptoService)
felAccessCodeencryptedFelAccessCode
felCertifierTokenencryptedFelToken
felUsernamePlain text (not a secret)
felCertifierPlain 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 nameClassEmitted whenPayload
business.createOnCreateBusinessEventBusiness is successfully insertedSelectableBusiness

Module Dependencies

ModulePurpose
CryptoModuleAES encryption/decryption of FEL credentials
DatabaseModuleKysely database connection
FirebaseModuleUpdating Firebase custom claims on business creation
UsersModuleResolving dbUserId from the Firebase token in PATCH /bill
FelModuleExtracting FEL certifier config structure from stored JSON

Bruno API Collection

Pre-built requests are in api-client/flowpos/collections/businesses/.

FileEndpoint
businesses.ymlGET /businesses (with pagination/sort params)
business.ymlPOST /businesses
business by Id.ymlGET /businesses/:id
business by Id - bill config.ymlGET /businesses/:id/bill-config
business config.ymlGET /businesses/:id (config inspection)
update-business.ymlPATCH /businesses/:id
business - bill config.ymlPATCH /businesses/:id/bill
business - fel.ymlPATCH /businesses/:id (FEL-focused update)
delete-business.ymlDELETE /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.