feat: add database seed with demo data and Docker Compose setup
- Seed creates demo tenant, users (admin, contador, visor) - Creates tenant schema with cfdis, iva_mensual, alertas tables - Inserts 50 demo CFDIs and 6 months of IVA records - Docker Compose with PostgreSQL, API, and Web services - Demo credentials: admin/contador/visor @demo.com / demo123 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
202
apps/api/prisma/seed.ts
Normal file
202
apps/api/prisma/seed.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🌱 Seeding database...');
|
||||||
|
|
||||||
|
// Create demo tenant
|
||||||
|
const schemaName = 'tenant_ede123456ab1';
|
||||||
|
|
||||||
|
const tenant = await prisma.tenant.upsert({
|
||||||
|
where: { rfc: 'EDE123456AB1' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
nombre: 'Empresa Demo SA de CV',
|
||||||
|
rfc: 'EDE123456AB1',
|
||||||
|
plan: 'professional',
|
||||||
|
schemaName,
|
||||||
|
cfdiLimit: 2000,
|
||||||
|
usersLimit: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Tenant created:', tenant.nombre);
|
||||||
|
|
||||||
|
// Create demo users
|
||||||
|
const passwordHash = await bcrypt.hash('demo123', 12);
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ email: 'admin@demo.com', nombre: 'Admin Demo', role: 'admin' as const },
|
||||||
|
{ email: 'contador@demo.com', nombre: 'Contador Demo', role: 'contador' as const },
|
||||||
|
{ email: 'visor@demo.com', nombre: 'Visor Demo', role: 'visor' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const userData of users) {
|
||||||
|
const user = await prisma.user.upsert({
|
||||||
|
where: { email: userData.email },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
email: userData.email,
|
||||||
|
passwordHash,
|
||||||
|
nombre: userData.nombre,
|
||||||
|
role: userData.role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✅ User created: ${user.email} (${user.role})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tenant schema
|
||||||
|
await prisma.$executeRawUnsafe(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
||||||
|
|
||||||
|
// Create tables in tenant schema
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${schemaName}"."cfdis" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
uuid_fiscal VARCHAR(36) UNIQUE NOT NULL,
|
||||||
|
tipo VARCHAR(20) NOT NULL,
|
||||||
|
serie VARCHAR(25),
|
||||||
|
folio VARCHAR(40),
|
||||||
|
fecha_emision TIMESTAMP NOT NULL,
|
||||||
|
fecha_timbrado TIMESTAMP NOT NULL,
|
||||||
|
rfc_emisor VARCHAR(13) NOT NULL,
|
||||||
|
nombre_emisor VARCHAR(300) NOT NULL,
|
||||||
|
rfc_receptor VARCHAR(13) NOT NULL,
|
||||||
|
nombre_receptor VARCHAR(300) NOT NULL,
|
||||||
|
subtotal DECIMAL(18,2) NOT NULL,
|
||||||
|
descuento DECIMAL(18,2) DEFAULT 0,
|
||||||
|
iva DECIMAL(18,2) DEFAULT 0,
|
||||||
|
isr_retenido DECIMAL(18,2) DEFAULT 0,
|
||||||
|
iva_retenido DECIMAL(18,2) DEFAULT 0,
|
||||||
|
total DECIMAL(18,2) NOT NULL,
|
||||||
|
moneda VARCHAR(3) DEFAULT 'MXN',
|
||||||
|
tipo_cambio DECIMAL(10,4) DEFAULT 1,
|
||||||
|
metodo_pago VARCHAR(3),
|
||||||
|
forma_pago VARCHAR(2),
|
||||||
|
uso_cfdi VARCHAR(4),
|
||||||
|
estado VARCHAR(20) DEFAULT 'vigente',
|
||||||
|
xml_url TEXT,
|
||||||
|
pdf_url TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create IVA monthly table
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${schemaName}"."iva_mensual" (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
año INT NOT NULL,
|
||||||
|
mes INT NOT NULL,
|
||||||
|
iva_trasladado DECIMAL(18,2) NOT NULL,
|
||||||
|
iva_acreditable DECIMAL(18,2) NOT NULL,
|
||||||
|
iva_retenido DECIMAL(18,2) DEFAULT 0,
|
||||||
|
resultado DECIMAL(18,2) NOT NULL,
|
||||||
|
acumulado DECIMAL(18,2) NOT NULL,
|
||||||
|
estado VARCHAR(20) DEFAULT 'pendiente',
|
||||||
|
fecha_declaracion TIMESTAMP,
|
||||||
|
UNIQUE(año, mes)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Insert demo CFDIs
|
||||||
|
const cfdiTypes = ['ingreso', 'egreso'];
|
||||||
|
const rfcs = ['XAXX010101000', 'MEXX020202000', 'AAXX030303000', 'BBXX040404000'];
|
||||||
|
const nombres = ['Cliente Demo SA', 'Proveedor ABC', 'Servicios XYZ', 'Materiales 123'];
|
||||||
|
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const tipo = cfdiTypes[i % 2];
|
||||||
|
const rfcIndex = i % 4;
|
||||||
|
const subtotal = Math.floor(Math.random() * 50000) + 1000;
|
||||||
|
const iva = subtotal * 0.16;
|
||||||
|
const total = subtotal + iva;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
INSERT INTO "${schemaName}"."cfdis"
|
||||||
|
(uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
|
||||||
|
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
|
||||||
|
subtotal, iva, total, estado)
|
||||||
|
VALUES (
|
||||||
|
'${crypto.randomUUID()}',
|
||||||
|
'${tipo}',
|
||||||
|
'A',
|
||||||
|
'${1000 + i}',
|
||||||
|
NOW() - INTERVAL '${Math.floor(Math.random() * 180)} days',
|
||||||
|
NOW() - INTERVAL '${Math.floor(Math.random() * 180)} days',
|
||||||
|
'${tipo === 'ingreso' ? 'EDE123456AB1' : rfcs[rfcIndex]}',
|
||||||
|
'${tipo === 'ingreso' ? 'Empresa Demo SA de CV' : nombres[rfcIndex]}',
|
||||||
|
'${tipo === 'egreso' ? 'EDE123456AB1' : rfcs[rfcIndex]}',
|
||||||
|
'${tipo === 'egreso' ? 'Empresa Demo SA de CV' : nombres[rfcIndex]}',
|
||||||
|
${subtotal},
|
||||||
|
${iva},
|
||||||
|
${total},
|
||||||
|
'vigente'
|
||||||
|
)
|
||||||
|
ON CONFLICT (uuid_fiscal) DO NOTHING
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Demo CFDIs created');
|
||||||
|
|
||||||
|
// Create IVA monthly records
|
||||||
|
let acumulado = 0;
|
||||||
|
|
||||||
|
for (let mes = 1; mes <= 6; mes++) {
|
||||||
|
const trasladado = Math.floor(Math.random() * 100000) + 150000;
|
||||||
|
const acreditable = Math.floor(Math.random() * 80000) + 120000;
|
||||||
|
const resultado = trasladado - acreditable;
|
||||||
|
acumulado += resultado;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
INSERT INTO "${schemaName}"."iva_mensual"
|
||||||
|
(año, mes, iva_trasladado, iva_acreditable, resultado, acumulado, estado)
|
||||||
|
VALUES (2024, ${mes}, ${trasladado}, ${acreditable}, ${resultado}, ${acumulado},
|
||||||
|
'${mes <= 4 ? 'declarado' : 'pendiente'}')
|
||||||
|
ON CONFLICT (año, mes) DO NOTHING
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ IVA monthly records created');
|
||||||
|
|
||||||
|
// Create alerts table
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${schemaName}"."alertas" (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
tipo VARCHAR(50) NOT NULL,
|
||||||
|
titulo VARCHAR(200) NOT NULL,
|
||||||
|
mensaje TEXT NOT NULL,
|
||||||
|
prioridad VARCHAR(20) DEFAULT 'media',
|
||||||
|
fecha_vencimiento TIMESTAMP,
|
||||||
|
leida BOOLEAN DEFAULT FALSE,
|
||||||
|
resuelta BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
INSERT INTO "${schemaName}"."alertas" (tipo, titulo, mensaje, prioridad, fecha_vencimiento)
|
||||||
|
VALUES
|
||||||
|
('iva_favor', 'IVA a Favor Disponible', 'Tienes $43,582.40 de IVA a favor acumulado', 'media', NULL),
|
||||||
|
('declaracion', 'Declaración Mensual', 'La declaración mensual de IVA/ISR vence el 17 de febrero', 'alta', NOW() + INTERVAL '5 days'),
|
||||||
|
('discrepancia', 'CFDI con Discrepancia', 'Se encontraron 12 facturas con discrepancias', 'alta', NULL)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ Alerts created');
|
||||||
|
|
||||||
|
console.log('🎉 Seed completed successfully!');
|
||||||
|
console.log('\n📝 Demo credentials:');
|
||||||
|
console.log(' Admin: admin@demo.com / demo123');
|
||||||
|
console.log(' Contador: contador@demo.com / demo123');
|
||||||
|
console.log(' Visor: visor@demo.com / demo123');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Error seeding database:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
53
docker-compose.yml
Normal file
53
docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: horux360-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: horux360
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/api/Dockerfile
|
||||||
|
container_name: horux360-api
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
PORT: 4000
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/horux360?schema=public
|
||||||
|
JWT_SECRET: your-super-secret-jwt-key-min-32-chars-long-for-development
|
||||||
|
JWT_EXPIRES_IN: 15m
|
||||||
|
JWT_REFRESH_EXPIRES_IN: 7d
|
||||||
|
CORS_ORIGIN: http://localhost:3000
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/web/Dockerfile
|
||||||
|
container_name: horux360-web
|
||||||
|
environment:
|
||||||
|
NEXT_PUBLIC_API_URL: http://localhost:4000/api
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
Reference in New Issue
Block a user