Skip to main content

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​

ConceptDescription
BusinessTaxConfigA join record linking a business to a tax_definition. Carries isActive and audit fields.
TaxDefinitionA globally-defined tax (e.g. "IVA Guatemala 12%"). Managed by the tax-definition module.
Default seedingOn business.create event, one tax config is auto-created based on countryId.

Country → Default Tax mapping​

CountryTax 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.

MethodPathDescription
POST/business-tax-configCreate a new tax config
GET/business-tax-configList active configs (paginated)
GET/business-tax-config/:idGet config by ID
PATCH/business-tax-config/:idUpdate a config
DELETE/business-tax-config/:idDelete a config

All read/delete endpoints require a businessId query parameter for multi-tenant scoping.

GET /business-tax-config​

Query parameters:

ParameterTypeRequiredDescription
businessIdUUIDNoFilter by business
searchstringNoSearch across tax definition name and code
pagenumberNoPage number (default 1)
sizenumberNoPage 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:

VariableDescription
{{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​

  • isActive filter 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_COUNTRY map in the service is a pure data structure, easy to extend for new countries without touching control flow.
  • delete returns SelectableBusinessTaxConfig — 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 — IBusinessTaxConfigRepository does not import from the interfaces layer; sortable key types are expressed as keyof SelectableBusinessTaxConfig to 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).