docs: update showcase guides to use relative links and check in walkthrough configuration
This commit is contained in:
parent
55ffce242f
commit
65edbd35c0
6 changed files with 434 additions and 34 deletions
|
|
@ -71,6 +71,34 @@ services:
|
|||
- default
|
||||
- proxy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Walkthrough/Demo Application Instance
|
||||
# -----------------------------------------------------------------------------
|
||||
app-walkthrough:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: special-hotel-walkthrough
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3002:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=${DATABASE_URL_WALKTHROUGH}
|
||||
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL_WALKTHROUGH}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET_WALKTHROUGH}
|
||||
- N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL_WALKTHROUGH}
|
||||
- N8N_WEBHOOK_SECRET=${N8N_WEBHOOK_SECRET_WALKTHROUGH}
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
tag: "app-walkthrough/{{.Name}}"
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
networks:
|
||||
- default
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
default:
|
||||
proxy:
|
||||
|
|
|
|||
|
|
@ -23,35 +23,35 @@ The system is built as a highly secure, multi-tenant enterprise app designed to
|
|||
> **I want** to see only my own sales, goals, and commission settlements,
|
||||
> **So that** I cannot view or tamper with other team members' financial information.
|
||||
* **The Solution**: PostgreSQL RLS policies enforce segregation directly in the database. When any query runs, the system dynamically sets the session variable `app.current_user_role` and `app.current_user_id`. Even if a user calls APIs directly or tries to bypass the UI, the database returns `0` rows for other users or hotels.
|
||||
* **Code Reference**: Check [rls_and_seed.sql](file:///home/gabogg/Proyects/semillero-special-hotel/prisma/rls_and_seed.sql) to see the SQL policies, and [auth.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/lib/auth.ts) to see how session contexts are injected.
|
||||
* **Code Reference**: Check [rls_and_seed.sql](../../prisma/rls_and_seed.sql) to see the SQL policies, and [auth.ts](../../src/lib/auth.ts) to see how session contexts are injected.
|
||||
|
||||
### User Story 2: Plan Versioning & Rules Configuration
|
||||
> **As an** administrator or commercial analyst,
|
||||
> **I want** to create commission plans and rules, and ensure that editing active plans creates a new version instead of changing history,
|
||||
> **So that** historical calculations remain auditable and unchanged.
|
||||
* **The Solution**: The system implements plan locking. When a plan is in `DRAFT`, rules can be edited. When it is marked `ACTIVE`, it becomes read-only. If an administrator tries to toggle or update an active plan, the system clones the plan, increments the `version` (e.g. `v1` -> `v2`), and sets the old plan's validity end date.
|
||||
* **Code Reference**: Check plan clone logic in [/api/plans/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/plans/route.ts).
|
||||
* **Code Reference**: Check plan clone logic in [/api/plans/route.ts](../../src/app/api/plans/route.ts).
|
||||
|
||||
### User Story 3: Atomic Bulk Sales Import
|
||||
> **As a** commercial leader,
|
||||
> **I want** to import Excel files containing monthly sales results,
|
||||
> **So that** sales are automatically processed and verified, returning clear validation errors if the file is inconsistent.
|
||||
* **The Solution**: Excel sheets are parsed and checked against strict validation constraints (non-existent users, negative amounts, invalid periods, incorrect hotel codes). The validation is **atomic**—if even one row fails, the entire transaction is rolled back. The UI presents an inconsistency panel highlighting exactly which cells failed.
|
||||
* **Code Reference**: View sheet parsing in [/api/sales/import/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/sales/import/route.ts).
|
||||
* **Code Reference**: View sheet parsing in [/api/sales/import/route.ts](../../src/app/api/sales/import/route.ts).
|
||||
|
||||
### User Story 4: Automated Calculations & Retroactive Clawbacks
|
||||
> **As a** finance analyst,
|
||||
> **I want** commission calculations to run automatically via n8n and adjust for returned or cancelled sales from previous months,
|
||||
> **So that** payouts are correct and negative adjustments are deducted.
|
||||
* **The Solution**: The n8n calculation workflow triggers the settlement engine. The engine queries previous months' sales. If a sale was returned/cancelled, it applies a retroactive delta adjustment (clawback) to the current month's payout.
|
||||
* **Code Reference**: See settlement computations in [/api/settlements/calculate/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/settlements/calculate/route.ts).
|
||||
* **Code Reference**: See settlement computations in [/api/settlements/calculate/route.ts](../../src/app/api/settlements/calculate/route.ts).
|
||||
|
||||
### User Story 5: Immutable Auditing Log
|
||||
> **As an** external auditor,
|
||||
> **I want** to see a complete history of all user logins, plan modifications, and approvals,
|
||||
> **So that** I can verify that logs cannot be edited or deleted by anyone.
|
||||
* **The Solution**: The `audit_logs` table has database triggers that intercept and block all `UPDATE` and `DELETE` queries. The Admin dashboard features an interactive Audit Logs Explorer displaying side-by-side JSON diff panels showing what changed.
|
||||
* **Code Reference**: View audit logs logic in [/api/audit-logs/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/audit-logs/route.ts).
|
||||
* **Code Reference**: View audit logs logic in [/api/audit-logs/route.ts](../../src/app/api/audit-logs/route.ts).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -23,35 +23,35 @@ El sistema está construido como una aplicación empresarial multi-inquilino alt
|
|||
> **Quiero** ver únicamente mis propias ventas, metas y liquidaciones de comisiones,
|
||||
> **Para que** no pueda ver ni alterar la información financiera de otros miembros del equipo.
|
||||
* **La Solución**: Las políticas de RLS de PostgreSQL imponen la segregación directamente en el motor de base de datos. Cuando se ejecuta cualquier consulta, el sistema establece dinámicamente la variable de sesión `app.current_user_role` y `app.current_user_id`. Incluso si un usuario realiza solicitudes directas a la API o intenta eludir la interfaz de usuario, la base de datos retorna `0` registros para otros usuarios u hoteles.
|
||||
* **Referencia de Código**: Revise [rls_and_seed.sql](file:///home/gabogg/Proyects/semillero-special-hotel/prisma/rls_and_seed.sql) para ver las políticas SQL, y [auth.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/lib/auth.ts) para ver cómo se inyectan los contextos de sesión.
|
||||
* **Referencia de Código**: Revise [rls_and_seed.sql](../../prisma/rls_and_seed.sql) para ver las políticas SQL, y [auth.ts](../../src/lib/auth.ts) para ver cómo se inyectan los contextos de sesión.
|
||||
|
||||
### Historia de Usuario 2: Control de Versiones de Planes y Configuración de Reglas
|
||||
> **Como** administrador o analista comercial,
|
||||
> **Quiero** crear planes de compensación y reglas, y asegurar que la edición de planes activos genere una nueva versión en lugar de modificar el historial,
|
||||
> **Para que** los cálculos históricos sigan siendo auditables e inalterados.
|
||||
* **La Solución**: El sistema implementa el bloqueo de planes. Cuando un plan está en estado `DRAFT` (Borrador), se pueden editar sus reglas. Cuando se marca como `ACTIVE` (Activo), pasa a ser de solo lectura. Si un administrador intenta alternar o actualizar un plan activo, el sistema clona el plan, incrementa el campo `version` (por ejemplo, `v1` -> `v2`) y establece la fecha de fin de validez del plan anterior.
|
||||
* **Referencia de Código**: Revise la lógica de clonación de planes en [/api/plans/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/plans/route.ts).
|
||||
* **Referencia de Código**: Revise la lógica de clonación de planes en [/api/plans/route.ts](../../src/app/api/plans/route.ts).
|
||||
|
||||
### Historia de Usuario 3: Importación Masiva Atómica de Ventas
|
||||
> **Como** líder comercial,
|
||||
> **Quiero** importar archivos Excel con los resultados de ventas mensuales,
|
||||
> **Para que** las ventas se procesen y validen automáticamente, retornando errores claros si el archivo presenta inconsistencias.
|
||||
* **La Solución**: Las hojas de Excel se procesan y validan contra restricciones estrictas (usuarios inexistentes, montos negativos, períodos inválidos, códigos de hotel incorrectos). La validación es **atómica**: si una sola fila falla, toda la transacción se revierte. La interfaz de usuario presenta un panel de inconsistencias que detalla con precisión qué celdas fallaron.
|
||||
* **Referencia de Código**: Vea el análisis de archivos en [/api/sales/import/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/sales/import/route.ts).
|
||||
* **Referencia de Código**: Vea el análisis de archivos en [/api/sales/import/route.ts](../../src/app/api/sales/import/route.ts).
|
||||
|
||||
### Historia de Usuario 4: Cálculos Automatizados y Deducciones Retroactivas (Clawbacks)
|
||||
> **Como** analista de finanzas,
|
||||
> **Quiero** que los cálculos de comisiones se ejecuten automáticamente mediante n8n y se ajusten por devoluciones o ventas canceladas de meses anteriores,
|
||||
> **Para que** los pagos sean correctos y se deduzcan los saldos negativos correspondientes.
|
||||
* **La Solución**: El flujo de cálculo de n8n activa el motor de liquidación. El motor consulta las ventas de los meses anteriores. Si una venta fue devuelta o cancelada, aplica un ajuste de delta retroactivo (clawback) en la liquidación del mes en curso, restándolo del pago final.
|
||||
* **Referencia de Código**: Vea los cálculos de liquidaciones en [/api/settlements/calculate/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/settlements/calculate/route.ts).
|
||||
* **Referencia de Código**: Vea los cálculos de liquidaciones en [/api/settlements/calculate/route.ts](../../src/app/api/settlements/calculate/route.ts).
|
||||
|
||||
### Historia de Usuario 5: Registro de Auditoría Inmutable
|
||||
> **Como** auditor externo,
|
||||
> **Quiero** ver un historial completo de todos los inicios de sesión de usuario, modificaciones de planes y aprobaciones,
|
||||
> **Para** verificar que nadie pueda editar ni eliminar los registros de auditoría.
|
||||
* **La Solución**: La tabla `audit_logs` cuenta con reglas y disparadores de base de datos que interceptan y bloquean cualquier consulta de tipo `UPDATE` o `DELETE`. El panel de administración incluye un Explorador de Registros de Auditoría interactivo con un panel de comparación JSON en paralelo que muestra exactamente qué cambió.
|
||||
* **Referencia de Código**: Vea la lógica de los registros de auditoría en [/api/audit-logs/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/audit-logs/route.ts).
|
||||
* **Referencia de Código**: Vea la lógica de los registros de auditoría en [/api/audit-logs/route.ts](../../src/app/api/audit-logs/route.ts).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
372
prisma/seed-walkthrough.js
Normal file
372
prisma/seed-walkthrough.js
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaPg } = require('@prisma/adapter-pg');
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const dbUrl = new URL(process.env.DATABASE_URL_WALKTHROUGH || "postgresql://special_hotel_user:SpecialHotel1235%2F%2A-%2B@pg.gaboggamer.online/special_hotel_walkthrough?schema=public");
|
||||
|
||||
const pool = new Pool({
|
||||
host: dbUrl.hostname,
|
||||
port: dbUrl.port ? parseInt(dbUrl.port) : 5432,
|
||||
user: decodeURIComponent(dbUrl.username),
|
||||
password: decodeURIComponent(dbUrl.password),
|
||||
database: dbUrl.pathname.substring(1).split('?')[0],
|
||||
ssl: false
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const PASSWORD_HASH = '$2b$10$lRn/GrwzaEGxWqqQGGz0f.44nBywObKkGWRajlxWeDGZ3o4f7ayjy'; // bcrypt of 'password123'
|
||||
|
||||
async function main() {
|
||||
console.log("=== STARTING WALKTHROUGH SEED SCRIPT ===");
|
||||
|
||||
// 1. Clean existing records in the walkthrough database to ensure clean seeding
|
||||
console.log("Cleaning database...");
|
||||
// Disable RLS temporarily for bulk seed operations
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "regions" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "hotels" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "users" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "goals" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "sales_results" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "settlements" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "audit_logs" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "compensation_plans" DISABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "calculation_rules" DISABLE ROW LEVEL SECURITY;`);
|
||||
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.notification.deleteMany();
|
||||
await prisma.settlement.deleteMany();
|
||||
await prisma.salesResult.deleteMany();
|
||||
await prisma.goal.deleteMany();
|
||||
await prisma.calculationRule.deleteMany();
|
||||
await prisma.compensationPlan.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
await prisma.hotel.deleteMany();
|
||||
await prisma.region.deleteMany();
|
||||
|
||||
// 2. Seed Regions
|
||||
console.log("Seeding regions...");
|
||||
const bogota = await prisma.region.create({ data: { name: 'Bogotá', code: 'BOG' } });
|
||||
const antioquia = await prisma.region.create({ data: { name: 'Antioquia', code: 'ANT' } });
|
||||
const caribe = await prisma.region.create({ data: { name: 'Caribe', code: 'CAR' } });
|
||||
const valle = await prisma.region.create({ data: { name: 'Valle', code: 'VAL' } });
|
||||
|
||||
// 3. Seed Hotels
|
||||
console.log("Seeding hotels...");
|
||||
const p93 = await prisma.hotel.create({ data: { name: 'Estelar Parque de la 93', code: 'EST-P93', regionId: bogota.id } });
|
||||
const mde = await prisma.hotel.create({ data: { name: 'Estelar Medellin', code: 'EST-MDE', regionId: antioquia.id } });
|
||||
const ctg = await prisma.hotel.create({ data: { name: 'Estelar Cartagena', code: 'EST-CTG', regionId: caribe.id } });
|
||||
const cali = await prisma.hotel.create({ data: { name: 'Estelar Cali', code: 'EST-CALI', regionId: valle.id } });
|
||||
|
||||
// 4. Seed Users
|
||||
console.log("Seeding users...");
|
||||
const usersToSeed = [
|
||||
{ username: 'admin', email: 'admin@estelar.com', role: 'admin', hotelId: p93.id, area: 'Sistemas' },
|
||||
{ username: 'director', email: 'director@estelar.com', role: 'director', hotelId: p93.id, area: 'Comercial' },
|
||||
{ username: 'analista', email: 'analista@estelar.com', role: 'analyst', hotelId: p93.id, area: 'Finanzas' },
|
||||
{ username: 'consulta', email: 'consulta@estelar.com', role: 'auditor', hotelId: p93.id, area: 'Auditoria' },
|
||||
|
||||
// Managers
|
||||
{ username: 'gerente_bog', email: 'gerente.bog@estelar.com', role: 'hotel_manager', hotelId: p93.id, area: 'Administracion' },
|
||||
{ username: 'gerente_mde', email: 'gerente.mde@estelar.com', role: 'hotel_manager', hotelId: mde.id, area: 'Administracion' },
|
||||
{ username: 'gerente_ctg', email: 'gerente.ctg@estelar.com', role: 'hotel_manager', hotelId: ctg.id, area: 'Administracion' },
|
||||
{ username: 'gerente_cali', email: 'gerente.cali@estelar.com', role: 'hotel_manager', hotelId: cali.id, area: 'Administracion' },
|
||||
|
||||
// Leaders
|
||||
{ username: 'lider_bog', email: 'lider.bog@estelar.com', role: 'commercial_leader', hotelId: p93.id, area: 'Ventas' },
|
||||
{ username: 'lider_mde', email: 'lider.mde@estelar.com', role: 'commercial_leader', hotelId: mde.id, area: 'Ventas' },
|
||||
{ username: 'lider_ctg', email: 'lider.ctg@estelar.com', role: 'commercial_leader', hotelId: ctg.id, area: 'Ventas' },
|
||||
{ username: 'lider_cali', email: 'lider.cali@estelar.com', role: 'commercial_leader', hotelId: cali.id, area: 'Ventas' },
|
||||
|
||||
// Collaborators
|
||||
{ username: 'colab_bog_1', email: 'colab.bog1@estelar.com', role: 'collaborator', hotelId: p93.id, area: 'Ventas' },
|
||||
{ username: 'colab_bog_2', email: 'colab.bog2@estelar.com', role: 'collaborator', hotelId: p93.id, area: 'Ventas' },
|
||||
{ username: 'colab_mde_1', email: 'colab.mde1@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' },
|
||||
{ username: 'colab_mde_2', email: 'colab.mde2@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' },
|
||||
{ username: 'colaborador_mde', email: 'colaborador.mde@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' },
|
||||
{ username: 'colab_ctg_1', email: 'colab.ctg1@estelar.com', role: 'collaborator', hotelId: ctg.id, area: 'Ventas' },
|
||||
{ username: 'colab_ctg_2', email: 'colab.ctg2@estelar.com', role: 'collaborator', hotelId: ctg.id, area: 'Ventas' },
|
||||
{ username: 'colab_cali_1', email: 'colab.cali1@estelar.com', role: 'collaborator', hotelId: cali.id, area: 'Ventas' },
|
||||
{ username: 'colab_cali_2', email: 'colab.cali2@estelar.com', role: 'collaborator', hotelId: cali.id, area: 'Ventas' }
|
||||
];
|
||||
|
||||
const seededUsers = {};
|
||||
for (const u of usersToSeed) {
|
||||
const created = await prisma.user.create({
|
||||
data: {
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
passwordHash: PASSWORD_HASH,
|
||||
role: u.role,
|
||||
hotelId: u.hotelId,
|
||||
area: u.area,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
});
|
||||
seededUsers[u.username] = created;
|
||||
}
|
||||
|
||||
// 5. Seed Compensation Plans & Rules
|
||||
console.log("Seeding compensation plans...");
|
||||
// Plan Q1 (Percentage, Inactive)
|
||||
const planQ1 = await prisma.compensationPlan.create({
|
||||
data: {
|
||||
name: 'Plan Comisiones Q1 - Porcentaje Fijo',
|
||||
code: 'PLAN-2026-Q1',
|
||||
validityStart: new Date('2026-01-01'),
|
||||
validityEnd: new Date('2026-03-31'),
|
||||
type: 'PERCENTAGE',
|
||||
percentageRate: 0.025, // 2.5%
|
||||
maxCap: 15000.00,
|
||||
status: 'INACTIVE',
|
||||
version: 1,
|
||||
createdBy: seededUsers['admin'].id
|
||||
}
|
||||
});
|
||||
|
||||
// Plan Q2 (Scale, Active)
|
||||
const planQ2 = await prisma.compensationPlan.create({
|
||||
data: {
|
||||
name: 'Plan Comisiones Q2 - Escala Escalonada',
|
||||
code: 'PLAN-2026-Q2',
|
||||
validityStart: new Date('2026-04-01'),
|
||||
validityEnd: new Date('2026-06-30'),
|
||||
type: 'SCALE',
|
||||
status: 'ACTIVE',
|
||||
version: 1,
|
||||
createdBy: seededUsers['admin'].id
|
||||
}
|
||||
});
|
||||
|
||||
// Calculation rules for Q2 Plan
|
||||
console.log("Seeding calculation rules...");
|
||||
await prisma.calculationRule.createMany({
|
||||
data: [
|
||||
{ planId: planQ2.id, type: 'TIER', minAchievement: 0.0, maxAchievement: 0.8, rate: 0.0, payoutAmount: 0.0 },
|
||||
{ planId: planQ2.id, type: 'TIER', minAchievement: 0.8, maxAchievement: 1.0, rate: 0.02, payoutAmount: 100.0 },
|
||||
{ planId: planQ2.id, type: 'TIER', minAchievement: 1.0, maxAchievement: 9.9, rate: 0.03, payoutAmount: 250.0 }
|
||||
]
|
||||
});
|
||||
|
||||
// 6. Seed Goals, Sales Results and Settlements
|
||||
console.log("Seeding goals, sales, and settlements...");
|
||||
const collaborators = [
|
||||
'colab_bog_1', 'colab_bog_2',
|
||||
'colab_mde_1', 'colab_mde_2', 'colaborador_mde',
|
||||
'colab_ctg_1', 'colab_ctg_2',
|
||||
'colab_cali_1', 'colab_cali_2'
|
||||
];
|
||||
|
||||
const periods = ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06'];
|
||||
|
||||
// Seed structured sales, goals and settlements month-by-month
|
||||
for (const period of periods) {
|
||||
const isQ2 = period >= '2026-04';
|
||||
const plan = isQ2 ? planQ2 : planQ1;
|
||||
|
||||
for (const username of collaborators) {
|
||||
const user = seededUsers[username];
|
||||
|
||||
// Goal Setup
|
||||
let goalAmount = 70000.00;
|
||||
if (username.endsWith('2')) goalAmount = 60000.00;
|
||||
if (username === 'colaborador_mde') goalAmount = 80000.00;
|
||||
|
||||
// Random variance for each user and month
|
||||
const hashVal = (username.length + period.charCodeAt(6)) % 10;
|
||||
const achievementMultiplier = 0.75 + (hashVal * 0.04); // ranges from 0.75 to 1.15
|
||||
const salesAmount = goalAmount * achievementMultiplier;
|
||||
|
||||
// Create Goal
|
||||
const goal = await prisma.goal.create({
|
||||
data: {
|
||||
targetType: 'INDIVIDUAL',
|
||||
targetId: user.id,
|
||||
period,
|
||||
amount: goalAmount
|
||||
}
|
||||
});
|
||||
|
||||
// Create Sales Entries
|
||||
const entryCount = 4;
|
||||
const amountPerEntry = salesAmount / entryCount;
|
||||
const salesCountPerEntry = Math.round(5 + hashVal / 2);
|
||||
|
||||
for (let i = 0; i < entryCount; i++) {
|
||||
// Build unique idempotency key
|
||||
const idempotencyKey = `sale-${user.id}-${period}-${i}`;
|
||||
|
||||
// Seed normal sale
|
||||
await prisma.salesResult.create({
|
||||
data: {
|
||||
source: 'EXCEL',
|
||||
hotelId: user.hotelId,
|
||||
userId: user.id,
|
||||
period,
|
||||
amount: amountPerEntry,
|
||||
salesCount: salesCountPerEntry,
|
||||
status: 'PROCESSED',
|
||||
idempotencyKey,
|
||||
uploadedBy: seededUsers['gerente_mde'].id, // Seed uploaded by MDE manager for convenience
|
||||
isAnomaly: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add a flagged outlier anomaly for colab_bog_1 in March
|
||||
if (username === 'colab_bog_1' && period === '2026-03') {
|
||||
await prisma.salesResult.create({
|
||||
data: {
|
||||
source: 'EXCEL',
|
||||
hotelId: user.hotelId,
|
||||
userId: user.id,
|
||||
period,
|
||||
amount: 85000.00,
|
||||
salesCount: 45,
|
||||
status: 'PROCESSED',
|
||||
idempotencyKey: `anomaly-outlier-bog1-mar`,
|
||||
uploadedBy: seededUsers['gerente_bog'].id,
|
||||
isAnomaly: true,
|
||||
flaggedReason: {
|
||||
en: "Outlier transaction: Sale amount exceeds standard region limits.",
|
||||
es: "Transacción atípica: El monto de venta supera los límites regionales estándar."
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Compute Commission & Bonus
|
||||
let commission = 0;
|
||||
let bonus = 0;
|
||||
const achievement = salesAmount / goalAmount;
|
||||
|
||||
if (!isQ2) {
|
||||
// Percentage plan: 2.5% of total sales
|
||||
commission = salesAmount * 0.025;
|
||||
bonus = 0;
|
||||
} else {
|
||||
// Scale plan rules
|
||||
if (achievement >= 0.8 && achievement < 1.0) {
|
||||
commission = salesAmount * 0.02;
|
||||
bonus = 100.00;
|
||||
} else if (achievement >= 1.0) {
|
||||
commission = salesAmount * 0.03;
|
||||
bonus = 250.00;
|
||||
}
|
||||
}
|
||||
|
||||
// Retroactive Clawback deduction for colab_mde_1 in May 2026
|
||||
let adjustmentAmount = 0.00;
|
||||
let adjustmentNotes = null;
|
||||
let originalSettlementId = null;
|
||||
|
||||
if (username === 'colab_mde_1' && period === '2026-05') {
|
||||
// Clawback applied for returned sale of $15000 in March 2026 (PLAN Q1 rate: 2.5%)
|
||||
adjustmentAmount = -375.00;
|
||||
adjustmentNotes = "Deducción de retroactivo: Devolución de reserva de ventas de marzo (PLAN-2026-Q1).";
|
||||
|
||||
// Find their March settlement to link as original
|
||||
const marchSettlement = await prisma.settlement.findFirst({
|
||||
where: { userId: user.id, period: '2026-03' }
|
||||
});
|
||||
if (marchSettlement) {
|
||||
originalSettlementId = marchSettlement.id;
|
||||
}
|
||||
}
|
||||
|
||||
const totalPayout = commission + bonus + adjustmentAmount;
|
||||
|
||||
// Settlement Status: APPROVED for Jan-May, PENDING for June
|
||||
const isJune = period === '2026-06';
|
||||
const settlementStatus = isJune ? 'PENDING' : 'APPROVED';
|
||||
|
||||
const managerMap = {
|
||||
[p93.id]: seededUsers['gerente_bog'].id,
|
||||
[mde.id]: seededUsers['gerente_mde'].id,
|
||||
[ctg.id]: seededUsers['gerente_ctg'].id,
|
||||
[cali.id]: seededUsers['gerente_cali'].id
|
||||
};
|
||||
|
||||
await prisma.settlement.create({
|
||||
data: {
|
||||
period,
|
||||
planId: plan.id,
|
||||
userId: user.id,
|
||||
salesAmount,
|
||||
goalAmount,
|
||||
achievementPercentage: achievement,
|
||||
calculatedCommission: commission,
|
||||
calculatedBonus: bonus,
|
||||
adjustmentAmount,
|
||||
totalPayout,
|
||||
status: settlementStatus,
|
||||
approvedBy: isJune ? null : managerMap[user.hotelId],
|
||||
approvedAt: isJune ? null : new Date(`${period}-05T14:30:00Z`),
|
||||
originalSettlementId,
|
||||
adjustmentNotes,
|
||||
aiAudited: true,
|
||||
aiAuditNotes: {
|
||||
en: isJune
|
||||
? "Calculated under PLAN-2026-Q2. Validation checks successfully completed."
|
||||
: "Compliant commission settlement. Payout successfully audited.",
|
||||
es: isJune
|
||||
? "Calculado bajo el PLAN-2026-Q2. Revisiones de validación completadas exitosamente."
|
||||
: "Liquidación de comisiones conforme. Pago auditado correctamente."
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Seed Audit Logs Trail
|
||||
console.log("Seeding audit logs...");
|
||||
const logs = [
|
||||
{ userId: seededUsers['admin'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['admin'].id, prev: null, next: null, ip: '::1', date: '2026-01-02T09:00:00Z' },
|
||||
{ userId: seededUsers['admin'].id, action: 'CREATE', targetTable: 'compensation_plans', targetId: planQ1.id, prev: null, next: { id: planQ1.id, code: planQ1.code, status: 'DRAFT' }, ip: '127.0.0.1', date: '2026-01-02T09:15:00Z' },
|
||||
{ userId: seededUsers['admin'].id, action: 'UPDATE', targetTable: 'compensation_plans', targetId: planQ1.id, prev: { status: 'DRAFT' }, next: { status: 'ACTIVE' }, ip: '127.0.0.1', date: '2026-01-05T10:00:00Z' },
|
||||
{ userId: seededUsers['gerente_mde'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['gerente_mde'].id, prev: null, next: null, ip: '10.8.0.5', date: '2026-02-05T08:30:00Z' },
|
||||
{ userId: seededUsers['gerente_mde'].id, action: 'APPROVE', targetTable: 'settlements', targetId: 1, prev: { status: 'PENDING' }, next: { status: 'APPROVED' }, ip: '10.8.0.5', date: '2026-02-05T08:45:00Z' },
|
||||
{ userId: seededUsers['gerente_bog'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['gerente_bog'].id, prev: null, next: null, ip: '10.8.0.6', date: '2026-02-05T09:12:00Z' },
|
||||
{ userId: seededUsers['gerente_bog'].id, action: 'APPROVE', targetTable: 'settlements', targetId: 2, prev: { status: 'PENDING' }, next: { status: 'APPROVED' }, ip: '10.8.0.6', date: '2026-02-05T09:18:00Z' }
|
||||
];
|
||||
|
||||
for (const log of logs) {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: log.userId,
|
||||
action: log.action,
|
||||
targetTable: log.targetTable,
|
||||
targetId: log.targetId,
|
||||
previousValue: log.prev,
|
||||
newValue: log.next,
|
||||
ipAddress: log.ip,
|
||||
createdAt: new Date(log.date)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 8. Re-enable Row-Level Security Policies via SQL
|
||||
console.log("Configuring database RLS boundaries...");
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "regions" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "hotels" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "goals" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "sales_results" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "settlements" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "compensation_plans" ENABLE ROW LEVEL SECURITY;`);
|
||||
await prisma.$executeRawUnsafe(`ALTER TABLE "calculation_rules" ENABLE ROW LEVEL SECURITY;`);
|
||||
|
||||
// Disconnect temporary prisma connection before running manual execute
|
||||
await prisma.$disconnect();
|
||||
|
||||
console.log("Walkthrough seeding completed successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(async (e) => {
|
||||
console.error("Error during walkthrough seeding:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -32,18 +32,6 @@ export default function AuditLogsPage() {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && role !== 'admin') {
|
||||
router.push('/unauthorized');
|
||||
}
|
||||
}, [role, authLoading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (role === 'admin') {
|
||||
fetchLogs();
|
||||
}
|
||||
}, [role]);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
|
@ -59,6 +47,18 @@ export default function AuditLogsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && role !== 'admin') {
|
||||
router.push('/unauthorized');
|
||||
}
|
||||
}, [role, authLoading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (role === 'admin') {
|
||||
fetchLogs();
|
||||
}
|
||||
}, [role]);
|
||||
|
||||
const handleRowClick = (log: AuditLog) => {
|
||||
setSelectedLog(log);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,18 +40,6 @@ export default function DashboardPage() {
|
|||
|
||||
const isRoleAuthorized = role === 'admin' || role === 'director' || role === 'analyst';
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isRoleAuthorized) {
|
||||
router.push('/unauthorized');
|
||||
}
|
||||
}, [role, authLoading, router, isRoleAuthorized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRoleAuthorized) {
|
||||
fetchDashboardData();
|
||||
}
|
||||
}, [role, isRoleAuthorized]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
|
@ -69,6 +57,18 @@ export default function DashboardPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !isRoleAuthorized) {
|
||||
router.push('/unauthorized');
|
||||
}
|
||||
}, [role, authLoading, router, isRoleAuthorized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRoleAuthorized) {
|
||||
fetchDashboardData();
|
||||
}
|
||||
}, [role, isRoleAuthorized]);
|
||||
|
||||
const formatCurrency = (val: number) => {
|
||||
return new Intl.NumberFormat(locale === 'es' ? 'es-CO' : 'en-US', {
|
||||
style: 'currency',
|
||||
|
|
|
|||
Loading…
Reference in a new issue