diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts new file mode 100644 index 0000000..4db93b0 --- /dev/null +++ b/apps/api/prisma/seed.ts @@ -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(); + }); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5d07efa --- /dev/null +++ b/docker-compose.yml @@ -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: