16 KiB
Architecture and System Design
Variable Remuneration, Compensation, and Commissions System - Hoteles Estelar
1. Technical Stack
- Frontend: Next.js App Router (React + TypeScript).
- Styling: Vanilla CSS utilizing CSS Modules (
*.module.css) for component-level style isolation, paired with a global variables system (globals.css) defining the design tokens. - Backend: Next.js API Routes (Node.js runtime).
- ORM: Prisma ORM (providing type-safe database queries and automated schema migrations).
- Database: PostgreSQL (decoupled, configured via environment variables to run on any host/network).
- Workflow Engine: n8n (external service triggered via API Webhooks). Runs the actual calculation steps, anomaly checks, AI-assisted audits, and notifications. This allows live monitoring of tasks and flexible AI model switching (e.g., swapping OpenAI/Anthropic/Ollama models within n8n nodes without rebuilding the Next.js codebase).
- Auditing: Audit logs are explicitly recorded within transactions inside the Next.js API endpoints (
tx.auditLog.create) utilizing the authenticated user session details. To enforce financial integrity and SOC 2/GDPR compliance, the database layer implements strict Row-Level Security (RLS) policies on theaudit_logstable that permit onlyINSERToperations (making the logs strictly append-only) and block allUPDATEorDELETEmutations for all users, including system administrators. Sensitive inputs (like password hashes or detailed bulk data records) are redacted/masked at the application layer before serialization.
2. Decoupled Network & Environment Independence
The project is structured to run in any isolated Docker environment. All parameters are fed via environment variables to cleanly partition production and development configurations:
2.1. Environment Variables (.env Layout)
- Production Stack Variables (app-prod):
DATABASE_URL_PROD: Connection string for the production PostgreSQL database.NEXT_PUBLIC_APP_URL: The production public domain (https://hotels.gaboggamer.online).NEXTAUTH_SECRET: Secret token used to sign and verify session JWTs in production.N8N_WEBHOOK_URL: Webhook endpoint for live production calculation workflows (https://n8n.gaboggamer.online/webhook/calculate-commissions).N8N_WEBHOOK_SECRET: Signature token to verify n8n webhook payload authenticity.APP_PROD_INTERNAL_URL: Internal container URL for prod mapping (http://special-hotel-prod:3000).
- Development & Test Stack Variables (app-dev):
DATABASE_URL_DEV: Connection string for the development PostgreSQL database.TEST_DATABASE_URL: Dedicated connection string for the isolated test/sandbox database.NEXT_PUBLIC_APP_URL_DEV: Development URL (http://localhost:3001).NEXTAUTH_SECRET_DEV: JWT signature token for development sessions.N8N_TEST_WEBHOOK_URL: Sandbox webhook URL for development calculation testing (https://n8n.gaboggamer.online/webhook-test/calculate-commissions).N8N_WEBHOOK_SECRET_DEV: Signature token to authorize development webhook payloads.APP_DEV_INTERNAL_URL: Internal container URL for dev mapping (http://special-hotel-dev:3000).
2.2. Component Interaction:
graph TD
User([User Client]) -->|HTTPS| WebApp[Next.js App Router]
subgraph Isolated Network
WebApp -->|Prisma Client| DB[(PostgreSQL Main)]
WebApp -.->|Prisma Client - Test Env| DBTest[(PostgreSQL Test)]
WebApp -->|HTTP POST Webhook /webhook-test| n8n[n8n Workflow Engine]
n8n -->|IF Webhook Path Match| DBTest
n8n -->|Else| DB
n8n -->|HTTP POST Callback| WebApp
end
3. Database Schema Design (Entity-Relationship Diagram)
erDiagram
REGIONS ||--o{ HOTELS : contains
HOTELS ||--o{ USERS : houses
USERS ||--o{ GOALS : achieves
USERS ||--o{ SALES_RESULTS : generates
USERS ||--o{ SETTLEMENTS : receives
COMPENSATION_PLANS ||--o{ CALCULATION_RULES : dictates
COMPENSATION_PLANS ||--o{ SETTLEMENTS : calculates
USERS ||--o{ AUDIT_LOGS : executes
USERS ||--o{ NOTIFICATIONS : receives
REGIONS {
int id PK
string name
string code
}
HOTELS {
int id PK
string name
string code
int region_id FK
string status
}
USERS {
int id PK
string username
string email
string password_hash
string role "admin | director | hotel_manager | commercial_leader | analyst | auditor | collaborator"
int hotel_id FK
string area
string status "ACTIVE | INACTIVE"
datetime created_at
}
COMPENSATION_PLANS {
int id PK
string name
string code
datetime validity_start
datetime validity_end
string type "PERCENTAGE | SCALE | CONDITIONAL | FIXED"
string formula "JSON or string representation"
decimal meta_amount
decimal percentage_rate
decimal max_cap
string status "DRAFT | ACTIVE | INACTIVE"
int version
int created_by FK
datetime created_at
}
CALCULATION_RULES {
int id PK
int plan_id FK
string type "TIER | BONUS"
decimal min_achievement "percentage"
decimal max_achievement "percentage"
decimal rate "multiplier or percentage"
decimal payout_amount "fixed payment"
}
GOALS {
int id PK
string target_type "INDIVIDUAL | TEAM | HOTEL"
int target_id "user_id, team_id, or hotel_id"
string period "YYYY-MM"
decimal amount
datetime created_at
}
SALES_RESULTS {
int id PK
string source "EXCEL | API"
int hotel_id FK
int user_id FK "colaborador"
string period "YYYY-MM"
decimal amount
int sales_count
string status "PENDING | PROCESSED"
string idempotency_key UK
string transaction_id
int uploaded_by FK
boolean is_anomaly
json flagged_reason
datetime created_at
}
SETTLEMENTS {
int id PK
string period "YYYY-MM"
int plan_id FK "References specific version of the plan"
int user_id FK
decimal sales_amount
decimal goal_amount
decimal achievement_percentage
decimal calculated_commission
decimal calculated_bonus
decimal adjustment_amount "Clawback or adjustment delta"
decimal total_payout "calculated_commission + calculated_bonus + adjustment_amount"
string status "SIMULATED | PENDING | APPROVED | REJECTED"
int approved_by FK
datetime approved_at
string rejection_reason
int original_settlement_id FK "Self-references the settlement adjusted, if any"
string adjustment_notes
boolean ai_audited
json ai_audit_notes
datetime created_at
}
AUDIT_LOGS {
int id PK
int user_id FK
string action "CREATE | UPDATE | DELETE | APPROVE | REJECT | LOGIN"
string target_table
int target_id
json previous_value
json new_value
string ip_address
datetime created_at
}
NOTIFICATIONS {
int id PK
int user_id FK
string title
string message
string status "UNREAD | READ"
string type "EMAIL | PUSH"
datetime sent_at
}
4. Workflows & n8n Integration Model
To achieve high modularity, testability, and adaptability, the system implements a Thick Client (Next.js), Thin Coordinator (n8n) architecture:
- Zero Database Connections in n8n: n8n must not connect directly to PostgreSQL. All data read/write mutations are handled by Next.js API endpoints.
- Minimal Code Blocks in n8n: Business logic, math calculations, and transaction management are kept on the Next.js side where they are type-safe and fully covered by unit tests.
- All AI/LLM inside n8n: All LLM queries and AI-assisted audits are processed using native n8n LangChain and AI Agent nodes, allowing prompt/model hot-swapping without redeploying the Next.js codebase.
- Stalwart Email Notifications: Email notifications are dispatched directly by n8n using native SMTP nodes configured to use the company's Stalwart mail server.
4.1. Sales Data Import Workflow
- Trigger: Next.js calls
POST /webhook/calculate-commissions(or/webhook-test/...in dev) forwarding parsed Excel rows, uploader ID, and an idempotency key. - n8n Processing:
- Queries
POST /api/n8n/validate-salesto resolve database entities (collaborators, hotels) and retrieve historical standard deviation baselines. - Passes data to a native LangChain LLM Chain node (
@n8n/n8n-nodes-langchain.chainLlm) connected to a Primary Chat Model (DeepSeek,lmChatDeepSeek) and a Fallback Chat Model (Google Gemini,lmChatGoogleGemini) to perform semantic anomaly validation and record anomaly flags. If the primary model fails or is rate-limited, n8n automatically falls back to the Google Gemini model. - Dynamic Credential Mapping: To support isolated project spaces, the bootstrap and test scripts query the n8n API (
GET /api/v1/credentials) at deploy time, resolve the correct credential IDs for the DeepSeek and Gemini accounts in the workspace, and inject them into the workflow definition before creating or updating the workflow.
- Queries
- Response: n8n calls
POST /api/sales/batch-save(authenticated viax-n8n-signature) to persist finalized results in the PostgreSQL database under admin privileges.
4.2. Settlement Calculation Workflow
- Trigger: Next.js calls n8n to execute the calculation for period
YYYY-MM. - n8n Processing:
- Gathers plans, goals, and sales results from
/api/n8n/fetch-calculation-data?period=YYYY-MM. - Offloads calculation processing to
/api/n8n/process-formulato run the type-safe mathematical rules engine (tiers, bonuses, clawbacks). - Runs a native AI Agent node to audit calculated payouts for compliance (e.g. capping rules, negative payouts).
- Dispatches email alerts using the Stalwart mail server if any extreme payouts require analyst reviews.
- Gathers plans, goals, and sales results from
- Response: Persists settlements via
POST /api/n8n/save-settlementswith static signature checks.
4.3. Test Webhook Branching & Environment Isolation in n8n
To ensure complete isolation of production data, all n8n workflows follow a strict testing branch architecture:
- Webhook Entry Node: n8n listens on two webhook path variants:
- Production calls hit:
/webhook/calculate-commissionsor/webhook/calculate-settlements - Test suite calls hit:
/webhook-test/calculate-commissionsor/webhook-test/calculate-settlements
- Production calls hit:
- Conditional Path Routing:
- An
IFnode immediately checks if the webhook request path containswebhook-test. - True (Test Mode): The workflow sets its Next.js target host variables to the development instance (
https://special-hotel-dev.gaboggamer.online), which usesTEST_DATABASE_URL(the secondary test sandbox database) and signing secretN8N_WEBHOOK_SECRET_DEV. - False (Prod Mode): The workflow executes against the production instance, validating calls using
N8N_WEBHOOK_SECRET.
- An
5. Security & Access Control Model (RBAC & RLS)
We define role-based access restrictions as follows:
| Role | Access Level | Restrictions |
|---|---|---|
| Administrador | Full system write & read. | None. |
| Director Comercial | Reads all dashboards & reports. Can configure metadata. | Cannot calculate or approve. |
| Gerente Hotel | Reads data, sales, and settlements for their specific Hotel. | Restricted to hotel_id. |
| Líder Comercial | Triggers simulations, views dashboards. Approves/Rejects settlements. | Restricted to their region/team. |
| Analista Financiero | Reviews calculations. Exports consolidated PDF/Excel reports. | Cannot approve. |
| Consulta / Auditor | Read-only. | No mutations allowed. |
| Colaborador | Consults own history & dashboard. | Restricted to user_id. |
5.1. PostgreSQL Row-Level Security (RLS) & Data Isolation
To ensure absolute segregation of sensitive compensation data, the database implements Row-Level Security (RLS). RLS is enforced at the database layer (or via Prisma client middleware setting transaction context parameters), guaranteeing security even if application queries omit filters.
- Tenant Isolation Rules:
- Colaboradores: Can only select rows from
SETTLEMENTS,SALES_RESULTS, andGOALSwhereuser_id = current_setting('app.current_user_id'). - Gerentes: Can only select rows where
hotel_id = current_setting('app.current_hotel_id'). - Líderes: Can select rows within their assigned regions or teams (
region_id = current_setting('app.current_region_id')). - Administradores / Analistas: RLS is bypassed to allow system-wide computations and consolidated reporting.
- Colaboradores: Can only select rows from
- Audit Trail Immutability:
- The
audit_logstable has database RLS policies (audit_logs_insert_policyandaudit_logs_select_policy) that only permitINSERToperations and explicitly blockUPDATEandDELETEactions for all users, including administrators. It is strictly write-only.
- The
6. Dual-Environment Docker Deployment Model
We design the containerized architecture to run two distinct instances of the application concurrently from the same codebase, ensuring clean division.
6.1. Service Configurations
- Production Container (
app-prod/special-hotel-prod):- Network Port: Exposed on port
3000(mapped via Caddy tohotels.gaboggamer.online). - Database: Bound to the production
DATABASE_URL_PROD. - Logs: Prefixed with
[PROD]inside the container engine.
- Network Port: Exposed on port
- Development Container (
app-dev/special-hotel-dev):- Network Port: Exposed on port
3001(mapped via Caddy tolocalhost:3001or developmental paths). - Database: Bound to
DATABASE_URL_DEV(with integration testing executing queries onTEST_DATABASE_URL). - Logs: Prefixed with
[DEV]for easy debugging contrast.
- Network Port: Exposed on port
6.2. Graceful Dev Failure Model
The development instance utilizes a validation startup hook. If any development-exclusive environment variables (like TEST_DATABASE_URL) are omitted:
- The
app-devcontainer logs a clear notification:[DEV] Required development-exclusive variables (TEST_DATABASE_URL, N8N_TEST_WEBHOOK_URL) are missing. - The entrypoint script exits with exit code 0.
- Docker or the compose orchestrator registers the container as cleanly stopped (not crashed). The production stack is completely unaffected, avoiding restart-loop penalties or deployment failures.
7. Internationalization (i18n) Architecture
The internationalization architecture provides bilingual support (English and Spanish) across the application, separating client-side UI translations from AI audit translation flows.
7.1. Client-Side Translation Context
- Scaffolding: Static JSON dictionaries map UI strings under
src/lib/i18n/dictionaries/. - State Management: A React Context provider (
LocaleProvider) coordinates the selected locale across client components, utilizing local storage for persistence. - Component Translation: UI components call a lightweight
t(key)translation hook, dynamically rendering headers, tables, validation warnings, and labels. - Keyword Localization: Core calculation states and types (such as
ACTIVE/ACTIVO,CLOSED/CERRADO,REVENUE/INGRESOS,NIGHTS/NOCHES) are mapped at the dictionary level so views translate database enums to localized strings seamlessly.
7.2. Bilingual AI Audit Translation Flow
To maintain semantic accuracy and structured parsing within the n8n pipelines, AI audits execute in English, followed by a dedicated translation stage:
graph LR
Engine[Next.js API] -->|Calculated Data| n8n[n8n Workflow]
n8n -->|Step 1: Audit in EN| LLM[LLM Node]
LLM -->|Audit Notes in EN| Trans[LLM Translation Node]
Trans -->|Translate to EN & ES| Output[Bilingual JSON Object]
Output -->|Callback Save| NextDb[Next.js Database API]
- Storage: Localized AI outputs are transmitted as JSON objects:
These are stored in the database as PostgreSQL{ "en": "Audit warning: High commission payout.", "es": "Advertencia de auditoría: Pago de comisión alto." }JSONBfields (flaggedReasononSalesResult, andaiAuditNotesonSettlement). - Rendering: The frontend page renders the string corresponding to the user's active locale:
item.aiAuditNotes[locale].