diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..449dc92 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -0,0 +1,64 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Tenant { + id String @id @default(uuid()) + nombre String + rfc String @unique + plan Plan @default(starter) + schemaName String @unique @map("schema_name") + cfdiLimit Int @map("cfdi_limit") + usersLimit Int @map("users_limit") + active Boolean @default(true) + createdAt DateTime @default(now()) @map("created_at") + expiresAt DateTime? @map("expires_at") + + users User[] + + @@map("tenants") +} + +model User { + id String @id @default(uuid()) + tenantId String @map("tenant_id") + email String @unique + passwordHash String @map("password_hash") + nombre String + role Role @default(visor) + active Boolean @default(true) + lastLogin DateTime? @map("last_login") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + + @@map("users") +} + +model RefreshToken { + id String @id @default(uuid()) + userId String @map("user_id") + token String @unique + expiresAt DateTime @map("expires_at") + createdAt DateTime @default(now()) @map("created_at") + + @@map("refresh_tokens") +} + +enum Plan { + starter + business + professional + enterprise +} + +enum Role { + admin + contador + visor +} diff --git a/apps/api/src/config/database.ts b/apps/api/src/config/database.ts new file mode 100644 index 0000000..df46150 --- /dev/null +++ b/apps/api/src/config/database.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + var prisma: PrismaClient | undefined; +} + +export const prisma = globalThis.prisma || new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +if (process.env.NODE_ENV !== 'production') { + globalThis.prisma = prisma; +} diff --git a/apps/api/src/utils/schema-manager.ts b/apps/api/src/utils/schema-manager.ts new file mode 100644 index 0000000..7fe6e4f --- /dev/null +++ b/apps/api/src/utils/schema-manager.ts @@ -0,0 +1,104 @@ +import { prisma } from '../config/database.js'; + +export async function createTenantSchema(schemaName: string): Promise { + await prisma.$executeRawUnsafe(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`); + + 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 + ) + `); + + await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "${schemaName}"."iva_mensual" ( + id SERIAL PRIMARY KEY, + año INTEGER NOT NULL, + mes INTEGER 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) + ) + `); + + await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS "${schemaName}"."isr_mensual" ( + id SERIAL PRIMARY KEY, + año INTEGER NOT NULL, + mes INTEGER NOT NULL, + ingresos_acumulados DECIMAL(18,2) NOT NULL, + deducciones DECIMAL(18,2) NOT NULL, + base_gravable DECIMAL(18,2) NOT NULL, + isr_causado DECIMAL(18,2) NOT NULL, + isr_retenido DECIMAL(18,2) NOT NULL, + isr_a_pagar DECIMAL(18,2) NOT NULL, + estado VARCHAR(20) DEFAULT 'pendiente', + fecha_declaracion TIMESTAMP, + UNIQUE(año, mes) + ) + `); + + 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(` + CREATE TABLE IF NOT EXISTS "${schemaName}"."calendario_fiscal" ( + id SERIAL PRIMARY KEY, + titulo VARCHAR(200) NOT NULL, + descripcion TEXT, + tipo VARCHAR(50) NOT NULL, + fecha_limite TIMESTAMP NOT NULL, + recurrencia VARCHAR(20) DEFAULT 'unica', + completado BOOLEAN DEFAULT FALSE, + notas TEXT + ) + `); +} + +export async function setTenantSchema(schemaName: string): Promise { + await prisma.$executeRawUnsafe(`SET search_path TO "${schemaName}"`); +} + +export async function deleteTenantSchema(schemaName: string): Promise { + await prisma.$executeRawUnsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`); +}