Saltar al contenido principal

Business Modules

Overview

The business-modules module manages the many-to-many relationship between a business and the global module catalog. Each record indicates that a specific feature module (e.g., POS, Restaurant, Inventory) is associated with a business, and whether it is currently active.

When a new business is created — or when its industry_segment / business_type is updated — the system automatically syncs the module set to match the business profile via BusinessModulesService.syncModulesForBusinessProfile. Modules an admin has explicitly toggled (is_manual_override = true) are never touched by the sync.


Domain Concepts

ConceptDescription
BusinessModuleJoin record linking a business to a module. Carries isActive to enable/disable the feature.
Module catalogGlobal list of available feature modules, seeded at deploy time from packages/backend/database/src/seeds/data/global/module.data.ts.
isActiveSoft enable/disable flag. Hard deletes are also supported but uncommon.
isManualOverrideWhen true, an admin explicitly created or disabled this module — the profile-sync algorithm skips this row entirely, preserving admin intent across profile updates.
Profile-driven defaultsThe resolver resolveBusinessModuleKeys() in packages/global/constants/default-business-modules.ts is the single source of truth. It maps industry_segment + business_type to a de-duplicated set of module keys. See the Resolver rules section.

Resolver rules

resolveBusinessModuleKeys({ industrySegment, businessType }) lives in packages/global/constants/default-business-modules.ts and is the single source of truth — used by the sync service, the create-event handler, and the backfill script.

Core modules (always on)

settingsModule, billingModule, goodsAndServicesModule, adminModule, accountsReceivableModule, accountsPayableModule, customersModule, reportsModule, communicationModule, dataImportModule

Segment rules

ModuleEnabled when industry_segment is…
inventoryModule, purchaseModuleany segment except services
restaurantModulefood_and_beverage only
serviceBookingsModulefood_and_beverage, services, vehicles_and_dealerships
productionModulefood_and_beverage, retail_and_goods, health_and_specialty
projectsModuleservices, vehicles_and_dealerships, other
payrollModuleall segments — except when business_type = mobile_food_vendor
posModulefood_and_beverage, retail_and_goods, health_and_specialty, sports_and_outdoors, vehicles_and_dealerships

Business-type overrides (additive)

business_typeAdds
jewelry_accessoriesjewelryModule

Extending the resolver

To add a new type-specific module:

  1. Add the module seed row to packages/backend/database/src/seeds/data/global/module.data.ts.
  2. Run the migration and regenerate types: pnpm run generate:types.
  3. Add the rule to resolveBusinessModuleKeys().
  4. Optionally add a pwaMenu.json entry so the sidebar renders it.
  5. Run pnpm --filter @flowpos-workspace/global test to verify the resolver matrix.

Sync algorithm

BusinessModulesService.syncModulesForBusinessProfile runs inside a single DB transaction and is idempotent — safe to re-run or double-fire.

  1. Resolve desired module UUIDs from moduleSeedData using resolveBusinessModuleKeys.
  2. Load all existing business_module rows for the business (including inactive ones).
  3. For each desired module: insert if missing, or re-enable if auto-managed and currently inactive.
  4. For each auto-managed active module not in the desired set: disable it.
  5. Rows with isManualOverride = true are never touched — they preserve explicit admin choices.

Sync triggers

EventCode pathGuard
Business createdhandleBusinessCreateEvent (@OnEvent)
Onboarding step 1 savedOnboardingOrchestratorService.upsertOnboardingBusinessupdate path only
Business profile editedBusinessesService.updateBusinessonly when businessType or industrySegment changed

Manual override marking

Any admin action that creates or disables a module via the API sets isManualOverride = true on that row, protecting it from future sync operations.


Backfill script

Run once per environment after the is_manual_override migration to align existing businesses with their profiles:

# Dry-run (no writes, logs what would change)
pnpm --filter @flowpos-workspace/backend-scripts run backfill:business-modules:dry-run

# Apply to a single business
pnpm --filter @flowpos-workspace/backend-scripts run backfill:business-modules -- --business-id=<uuid>

# Apply to all businesses
pnpm --filter @flowpos-workspace/backend-scripts run backfill:business-modules

The script imports resolveBusinessModuleKeys directly — no SQL CASE duplication.


Architecture

The module follows strict Hexagonal Architecture:

interfaces/            ← HTTP adapter (controller, DTOs, query)
application/ ← Use cases (service)
domain/ ← Port interface + injection token (framework-agnostic)
infrastructure/ ← Kysely repository (DB adapter)

Dependency flow

BusinessModulesController

BusinessModulesService
↓ (injected via BUSINESS_MODULES_REPOSITORY token)
IBusinessModulesRepository ← BusinessModulesRepository (Kysely)

The service depends on the IBusinessModulesRepository interface (domain port), not the concrete Kysely implementation. The binding is configured in BusinessModulesModule using a NestJS custom provider with the BUSINESS_MODULES_REPOSITORY symbol token.

Key design decisions

  • Interface-based injection — The service is decoupled from Kysely via @Inject(BUSINESS_MODULES_REPOSITORY). This enables adapter swaps and testing with mocks.
  • Domain purity — The domain port contains no Kysely types (Transaction, SimplifySingleResult). Return types use plain TypeScript (T | undefined instead of SimplifySingleResult<T>).
  • Audit fields from auth tokencreatedBy and updatedBy are resolved from the bearer token via the @UserId() decorator. They are not accepted in the request body, preventing client impersonation.
  • SortableBusinessModuleKey is defined in the domain layer (not the interface layer) to keep the port free of UI concerns.
  • createdAt/updatedAt are server-managed; they are not accepted in the create/update DTOs.

Main Use Cases

List active business modules

GET /business-modules?businessId=<uuid>&size=50

Returns all active (isActive=true) module associations for a business. The businessId filter is required in practice; without it the query returns all records across all businesses.

Create a business module association

POST /business-modules

Manually associates a module with a business. Most modules are seeded automatically on business creation — this endpoint is primarily for admin or migration use. The createdBy field is set from the authenticated user's token.

Body:

{
"businessId": "550e8400-e29b-41d4-a716-446655440001",
"moduleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Get a business module by ID

GET /business-modules/:id

Returns a single business module record. Returns 404 if not found.

Update a business module

PATCH /business-modules/:id

Partially updates any field on the association (e.g., toggling isActive). The updatedBy field is set from the authenticated user's token.

Body (example — toggle off):

{ "isActive": false }

Disable a business module (soft delete)

PATCH /business-modules/disable-module/:id?businessId=<uuid>

Sets isActive = false. The acting user is resolved from the auth token. Returns the updated record including the related module entity.

Prefer this over hard delete when the module may need to be re-enabled later.

Delete a business module (hard delete)

DELETE /business-modules/:id

Permanently removes the association. Returns 204 No Content. Cannot be undone.


API Endpoints

MethodPathStatusDescription
POST/business-modules201Create a business module association
GET/business-modules200List active modules for a business (paginated)
GET/business-modules/:id200Get by ID
PATCH/business-modules/:id200Update (partial)
PATCH/business-modules/disable-module/:id200Soft-disable (sets isActive=false)
DELETE/business-modules/:id204Hard delete

All endpoints require a valid Firebase bearer token (Authorization: Bearer <token> or flowpos-id-token cookie).


Bruno API Collection

Requests are in api-client/flowpos/collections/business-modules/.

FileOperation
list business modules.ymlGET /business-modules
create business module.ymlPOST /business-modules
get business module by id.ymlGET /business-modules/:id
update business module.ymlPATCH /business-modules/:id
disable business module.ymlPATCH /business-modules/disable-module/:id
delete business module.ymlDELETE /business-modules/:id