Business Tax Config
Overview
The business-tax-config module manages the association between a Business and its configured TaxDefinition records. A single business may have multiple active tax configurations (e.g., one general VAT and one withholding tax).
Default tax configurations are automatically seeded when a new business is created, based on the business's country.
Domain Concepts
| Concept | Description |
|---|---|
BusinessTaxConfig | A join record linking a business to a tax_definition. Carries isActive and audit fields. |
TaxDefinition | A globally-defined tax (e.g. "IVA Guatemala 12%"). Managed by the tax-definition module. |
| Default seeding | On business.create event, one tax config is auto-created based on countryId. |
Country → Default Tax mapping
| Country | Tax Definition |
|---|---|
| Guatemala (GT) | IVA GT |
| El Salvador (SV) | IVA SV |
| Honduras (HN) | IVA HN |
| Mexico (MX) | IVA MX |
| United States (US) | IVA US |
| (any other) | IVA GT (fallback) |
Architecture
This module follows Hexagonal Architecture:
domain/
business-tax-config-repository.domain.ts ← IBusinessTaxConfigRepository port
application/
business-tax-config.service.ts ← Use cases + OnCreateBusinessEvent handler
infrastructure/
business-tax-config.repository.ts ← Kysely adapter implementing the port
interfaces/
business-tax-config.controller.ts ← HTTP routes (thin controller)
dtos/
create-business-tax-config.dto.ts
update-business-tax-config.dto.ts
query/
paginate-business-tax-config.query.ts
Dependency direction: Controller → Service → Repository interface ← Repository implementation
API Endpoints
All endpoints require Bearer token authentication. The controller applies RolesGuard with PermissionResource(PolicyResource.BusinessTaxConfig) at class level, enforcing RBAC for all routes.
| Method | Path | Description |
|---|---|---|
POST | /business-tax-config | Create a new tax config |
GET | /business-tax-config | List active configs (paginated) |
GET | /business-tax-config/:id | Get config by ID |
PATCH | /business-tax-config/:id | Update a config |
DELETE | /business-tax-config/:id | Delete a config |
All read/delete endpoints require a businessId query parameter for multi-tenant scoping.
GET /business-tax-config
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
businessId | UUID | No | Filter by business |
search | string | No | Search across tax definition name and code |
page | number | No | Page number (default 1) |
size | number | No | Page size (default 20) |
Only records where isActive = true are returned.
POST /business-tax-config
{
"businessId": "uuid",
"taxDefinitionId": "uuid",
"createdBy": "uuid",
"isActive": true
}
PATCH /business-tax-config/:id
{
"businessId": "uuid",
"taxDefinitionId": "uuid",
"updatedBy": "uuid",
"isActive": false
}
All fields except businessId and updatedBy are optional.
Event Integration
The service listens to the business.create event emitted by the businesses module:
@OnEvent(OnCreateBusinessEvent.eventName, { async: true })
private async handleBusinessCreateEvent({ business }) { ... }
This auto-seeds the appropriate default tax configuration without any HTTP call.
Bruno API Collection
Located at api-client/flowpos/collections/business-tax-config/. Request files follow the naming convention <verb> business tax config[s].yml. Uses environment variables:
| Variable | Description |
|---|---|
{{BASE_URL}} | API base URL (e.g. http://localhost:4000) |
{{ID_TOKEN}} | Firebase Bearer token |
{{businessId}} | Target business UUID |
{{taxDefinitionId}} | Tax definition UUID |
{{dbUserId}} | Authenticated user's DB UUID |
{{businessTaxConfigId}} | Auto-set after POST by the after-response script |
Design Decisions
isActivefilter in base query — both the count and results queries share the same base query so pagination counts are always consistent with the returned data.- No transaction wrapper in controller — create and update are single-operation statements and are inherently atomic; explicit transaction wrappers at the HTTP layer added complexity without benefit.
- Country→tax mapping as a constant — the
DEFAULT_TAX_BY_COUNTRYmap in the service is a pure data structure, easy to extend for new countries without touching control flow. deletereturnsSelectableBusinessTaxConfig— returning only the deleted record's flat fields is sufficient; loading full relations after deletion was a type-safety lie (the mixin was never applied).- Domain port is framework-agnostic —
IBusinessTaxConfigRepositorydoes not import from the interfaces layer; sortable key types are expressed askeyof SelectableBusinessTaxConfigto keep the dependency direction inward. - RBAC enforced at controller level —
@UseGuards(RolesGuard)+@PermissionResource(PolicyResource.BusinessTaxConfig)applied to the class, consistent with other modules (e.g. products).