Architecture
1. System Context (C4 L1)
2. Container View (C4 L2)
No Redis, BullMQ, or WebSocket components. The only async surface is the single CDC consumer.
3. Component View (C4 L3) — Internal Layering
| Layer | Responsibility |
|---|---|
| Controllers | Auth (jwt/basic) + permission gate + DTO mapping; CRUD via ControllerFactory |
ApplicationKafkaComponent | Subscribes CDCKafkaTopics.PRODUCT, deserializes Debezium payload, routes to worker |
TaxationWorkerService | Maps CDC op (c/u/r/d) + product state to provision/deprovision |
TaxProvisioningService | Idempotent TaxSet+Tax write from a TaxGroup template |
TaxGroupService | Merchant taxMethod ↔ group compatibility validation |
| Repositories | Drizzle queries over @nx/core schemas; soft-delete |
4. State Machines Index
| Entity | States | Diagram |
|---|---|---|
TaxSet | ACTIVATED, DEACTIVATED, ARCHIVED | → jump |
TaxSet (provisioning lifecycle)
| From | Event | To | Guards |
|---|---|---|---|
[*] | provisionForProduct | ACTIVATED | TaxGroup exists, has ≥1 item |
ACTIVATED | re-provision (different group) | DEACTIVATED (old) | only when sourceType = TaxGroup |
ACTIVATED | deprovisionForProduct | DEACTIVATED | only when sourceType = TaxGroup |
ACTIVATED | same group | ACTIVATED (no-op) | idempotent skip |
Only
TaxGroup-sourced TaxSets are touched. Manually-created TaxSets (variant overrides,sourceTypeunset) are never deactivated by provisioning.
5. Runtime Scenarios
5.1 CDC reconcile on product create/update
| Step | Detail |
|---|---|
| 2-3 | fallbackMode: latest — only live changes consumed; no historical backfill |
| 4 | Debezium snake_case row converted via toCamelCaseKeys |
| 5 | Soft-deleted product (deletedAt) deprovisions before taxGroup check |
else | Provision is idempotent: same (sourceType=TaxGroup, sourceId) → skip |
5.2 CDC delete
5.3 Manual provisioning via REST
6. Crosscutting Concerns
| Concern | How this service handles it |
|---|---|
| AuthN | JWT (Issuer = identity) + HTTP Basic; strategies ['jwt','basic'] on every route |
| AuthZ | Resource-based permissions seeded per controller; granted to OWNER/EMPLOYEE/CASHIER at bootstrap |
| i18n | i18n('name') jsonb columns ({ default, en, vi }) on TaxGroup, TaxGroupItem name, Tax name |
| Logging | Structured key-value (key: %s); logger.for(method) scoping in services |
| Tracing | No dedicated tracer |
| Idempotency | Provision skips when active TaxSet already sourced from the same TaxGroup (ADR-0001) |
| Soft-delete | SoftDeletableRepository (deletedAt); deprovision uses status DEACTIVATED, never hard-delete |
| IDs | Snowflake via IdGenerator, worker id 13 (hardcoded — collision risk if scaled) |