semillero-special-hotel/docs/PHASE_5_IMPLEMENTATION.md

6.2 KiB

Phase 5 Implementation & Settlement Design Document

Variable Remuneration, Compensation, and Commissions System - Hoteles Estelar


This document outlines the detailed architectural decisions, logical workflows, API specifications, UI mockups, and E2E verification plans for Phase 5: Settlement Engine & Approvals.

1. Architectural Strategy & Logic Flows

1.1. Core Settlement Calculation Engine (US-COM-006)

The system calculates commissions automatically based on defined rules. For a target period (YYYY-MM), the calculation steps are:

  1. Fetch Active Plans: Resolve the active version of each CompensationPlan for target collaborators.
  2. Fetch Goals: Resolve the goal amount for target users from the Goal table for the matching period.
  3. Fetch Sales Results: Retrieve and sum the total amount and salesCount of SalesResult records generated by each collaborator in the period.
  4. Determine Achievement:
    \text{Achievement \%} = \frac{\text{Sum of Sales Amount}}{\text{Goal Amount}}
  5. Apply Rules:
    • Compare the Achievement % against boundaries in the plan's CalculationRule list.
    • For TIER rules: Payout is calculated as:
      \text{Commission} = \text{Sum of Sales Amount} \times \text{Rule Rate}
    • For BONUS rules: Payout is the fixed payoutAmount configured.
  6. Enforce Cap Limit: If the sum of calculated payouts exceeds the plan's maxCap, the payout is capped at maxCap.

1.2. Retroactive Adjustments & PMS Clawbacks

Hotel booking modifications, refunds, or cancellations frequently occur after past commissions have been paid. The engine processes retroactive adjustments as follows:

graph TD
    A[Start Calculation for Month M] --> B[Retrieve Month M-1 and M-2 Sales Results]
    B --> C[Fetch Original Settlements for Month M-1 and M-2]
    C --> D{Any sales discrepancy found?}
    D -->|No| E[Continue with normal month M calculation]
    D -->|Yes| F[Recalculate past commission with live sales data]
    F --> G[Calculate Delta: Corrected Payout - Previously Paid Payout]
    G --> H[Create Pending Adjustment Settlement record in DB linked to Original ID]
    H --> I[Add Delta to Month M Total Payout: Total = Commission + Bonus + Sum(Deltas)]
  • If \Delta \text{Payout} < 0, it is treated as a Clawback (reduction).
  • If \Delta \text{Payout} > 0, it is treated as an Adjustment Credit (addition).
  • A self-referencing relationship is stored in the Settlement table via originalSettlementId to preserve the historical audit trail.

1.3. Webhook Branching & Environment Isolation

To prevent test calculation suites from polluting production records, the n8n workflows implement a strict routing architecture:

  1. Conditional Branching: The n8n webhook endpoint evaluates the request URI path.
  2. Test/Staging calls: Webhooks routed via /webhook-test/calculate-settlements route to http://app-dev:3001 (running development stack, connected to TEST_DATABASE_URL) and validate signatures using N8N_WEBHOOK_SECRET_DEV.
  3. Production calls: Webhooks routed via /webhook/calculate-settlements route to http://app-prod:3000 and validate signatures using N8N_WEBHOOK_SECRET.

1.4. SOC 2 Log Redaction

All Next.js API routes run console outputs through a filtering proxy that redacts base salaries, passwords, and custom compensation parameters before writing to standard streams:

  • Intercepts logs on settlement mutations.
  • Replaces values for keys: password, passwordHash, baseSalary, bankAccount, and salary with [REDACTED].

2. API Specifications

2.1. POST /api/settlements/calculate (Process Settlements)

  • Role Restriction: admin | analyst
  • Request Body:
    {
      "period": "2026-06",
      "simulateOnly": false
    }
    
  • Response:
    • 201 Created (Direct commit mode):
      {
        "success": true,
        "code": "SETTLEMENTS_CALCULATED",
        "metadata": {
          "count": 14,
          "totalCommission": 85000.00,
          "totalAdjustment": -3500.00
        }
      }
      

2.2. POST /api/settlements/[id]/approve (Approve Payout)

  • Role Restriction: commercial_leader
  • Logic: Enforces RLS. A leader can only approve settlements belonging to collaborators of hotels in their region. Sets status to APPROVED, logs approvedBy, and triggers a callback notification.
  • Response: 200 OK.

2.3. POST /api/settlements/[id]/reject (Reject Payout)

  • Role Restriction: commercial_leader
  • Request Body:
    {
      "reason": "Venta reportada del colaborador no coincide con el cierre del hotel"
    }
    
  • Logic: Sets status to REJECTED, stores the mandatory reason in the database, and sets the state to allow correction and recalculation.
  • Response: 200 OK.

3. UI Design Specifications

3.1. Simulation Dashboard (/sales/simulation)

Provides analysts with a dry-run environment:

  1. Period Selector: Dropdown to select the target payout month.
  2. Comparison Matrix: Grid displaying:
    • Collaborator | Plan Version | Goal | Confirmed Sales | Achievement % | Proposed Commission | Retroactive Adjustments | Total Payout (Simulated)
  3. Dry-Run Mode Toggle: Execute calculations without writing data or triggering final callbacks.

3.2. Approvals Panel (/settlements/approvals)

Allows leaders to sign off on payouts:

  1. Segregated List: Displays pending settlements, restricted to the leader's region.
  2. Reason Modal: Appears when selecting "Rechazar", requiring a text entry.
  3. Status Badges: Shows simulated vs finalized payout progress.

4. Headless Puppeteer Verification Plan

We will create a verification script prisma/test-phase5-ui.js that tests:

  1. Engine Accuracy: Seed sales/goals, run calculation, verify output against expected mathematical tiers.
  2. Clawback Delta Logic: Update a past month's sale amount, run the calculation again, and verify that the current month's proposed settlement contains a negative adjustment equal to the commission delta.
  3. Leader Isolation: Confirm lider_ctg is blocked from approving a settlement for a collaborator in Antioquia (EST-MDE).
  4. Rejection Constraint: Ensure a rejection request fails (400) if the reason is omitted.