feat: add Prisma schema with multi-tenant support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
64
apps/api/prisma/schema.prisma
Normal file
64
apps/api/prisma/schema.prisma
Normal file
@@ -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
|
||||||
|
}
|
||||||
13
apps/api/src/config/database.ts
Normal file
13
apps/api/src/config/database.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
104
apps/api/src/utils/schema-manager.ts
Normal file
104
apps/api/src/utils/schema-manager.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { prisma } from '../config/database.js';
|
||||||
|
|
||||||
|
export async function createTenantSchema(schemaName: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
await prisma.$executeRawUnsafe(`SET search_path TO "${schemaName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTenantSchema(schemaName: string): Promise<void> {
|
||||||
|
await prisma.$executeRawUnsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user