diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 5772958..ccd8877 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -8,20 +8,22 @@ datasource db { } 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") + id String @id @default(uuid()) + nombre String + rfc String @unique + plan Plan @default(starter) + databaseName String @unique @map("database_name") + cfdiLimit Int @default(100) @map("cfdi_limit") + usersLimit Int @default(1) @map("users_limit") + active Boolean @default(true) + createdAt DateTime @default(now()) @map("created_at") + expiresAt DateTime? @map("expires_at") users User[] fielCredential FielCredential? satSyncJobs SatSyncJob[] + subscriptions Subscription[] + payments Payment[] @@map("tenants") } @@ -76,8 +78,12 @@ model FielCredential { cerData Bytes @map("cer_data") keyData Bytes @map("key_data") keyPasswordEncrypted Bytes @map("key_password_encrypted") - encryptionIv Bytes @map("encryption_iv") - encryptionTag Bytes @map("encryption_tag") + cerIv Bytes @map("cer_iv") + cerTag Bytes @map("cer_tag") + keyIv Bytes @map("key_iv") + keyTag Bytes @map("key_tag") + passwordIv Bytes @map("password_iv") + passwordTag Bytes @map("password_tag") serialNumber String? @map("serial_number") @db.VarChar(50) validFrom DateTime @map("valid_from") validUntil DateTime @map("valid_until") @@ -90,6 +96,46 @@ model FielCredential { @@map("fiel_credentials") } +model Subscription { + id String @id @default(uuid()) + tenantId String @map("tenant_id") + plan Plan + mpPreapprovalId String? @map("mp_preapproval_id") + status String @default("pending") + amount Decimal @db.Decimal(10, 2) + frequency String @default("monthly") + currentPeriodStart DateTime? @map("current_period_start") + currentPeriodEnd DateTime? @map("current_period_end") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + payments Payment[] + + @@index([tenantId]) + @@index([status]) + @@map("subscriptions") +} + +model Payment { + id String @id @default(uuid()) + tenantId String @map("tenant_id") + subscriptionId String? @map("subscription_id") + mpPaymentId String? @map("mp_payment_id") + amount Decimal @db.Decimal(10, 2) + status String @default("pending") + paymentMethod String? @map("payment_method") + paidAt DateTime? @map("paid_at") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + subscription Subscription? @relation(fields: [subscriptionId], references: [id]) + + @@index([tenantId]) + @@index([subscriptionId]) + @@map("payments") +} + model SatSyncJob { id String @id @default(uuid()) tenantId String @map("tenant_id") diff --git a/apps/api/src/middlewares/tenant.middleware.ts b/apps/api/src/middlewares/tenant.middleware.ts index b7674c5..07f67ed 100644 --- a/apps/api/src/middlewares/tenant.middleware.ts +++ b/apps/api/src/middlewares/tenant.middleware.ts @@ -29,17 +29,17 @@ export async function tenantMiddleware(req: Request, res: Response, next: NextFu const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, - select: { schemaName: true, active: true }, + select: { databaseName: true, active: true }, }); if (!tenant || !tenant.active) { return next(new AppError(403, 'Tenant no encontrado o inactivo')); } - req.tenantSchema = tenant.schemaName; + req.tenantSchema = tenant.databaseName; - // Set search_path for this request - await prisma.$executeRawUnsafe(`SET search_path TO "${tenant.schemaName}", public`); + // Set search_path for this request (will be replaced by pool-based approach) + await prisma.$executeRawUnsafe(`SET search_path TO "${tenant.databaseName}", public`); next(); } catch (error) { diff --git a/apps/api/src/services/auth.service.ts b/apps/api/src/services/auth.service.ts index 6150da7..205d9db 100644 --- a/apps/api/src/services/auth.service.ts +++ b/apps/api/src/services/auth.service.ts @@ -23,20 +23,20 @@ export async function register(data: RegisterRequest): Promise { throw new AppError(400, 'El RFC ya está registrado'); } - const schemaName = `tenant_${data.empresa.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`; + const databaseName = `horux_${data.empresa.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`; const tenant = await prisma.tenant.create({ data: { nombre: data.empresa.nombre, rfc: data.empresa.rfc.toUpperCase(), plan: 'starter', - schemaName, + databaseName, cfdiLimit: PLANS.starter.cfdiLimit, usersLimit: PLANS.starter.usersLimit, }, }); - await createTenantSchema(schemaName); + await createTenantSchema(databaseName); const passwordHash = await hashPassword(data.usuario.password); const user = await prisma.user.create({ @@ -54,7 +54,7 @@ export async function register(data: RegisterRequest): Promise { email: user.email, role: user.role, tenantId: tenant.id, - schemaName: tenant.schemaName, + databaseName: tenant.databaseName, }; const accessToken = generateAccessToken(tokenPayload); @@ -117,7 +117,7 @@ export async function login(data: LoginRequest): Promise { email: user.email, role: user.role, tenantId: user.tenantId, - schemaName: user.tenant.schemaName, + databaseName: user.tenant.databaseName, }; const accessToken = generateAccessToken(tokenPayload); @@ -181,7 +181,7 @@ export async function refreshTokens(token: string): Promise<{ accessToken: strin email: user.email, role: user.role, tenantId: user.tenantId, - schemaName: user.tenant.schemaName, + databaseName: user.tenant.databaseName, }; const accessToken = generateAccessToken(newTokenPayload); diff --git a/apps/api/src/services/fiel.service.ts b/apps/api/src/services/fiel.service.ts index 3c917ad..1142d27 100644 --- a/apps/api/src/services/fiel.service.ts +++ b/apps/api/src/services/fiel.service.ts @@ -58,15 +58,19 @@ export async function uploadFiel( }; } - // Encriptar credenciales (todas juntas con el mismo IV/tag) + // Encriptar credenciales (per-component IV/tag) const { encryptedCer, encryptedKey, encryptedPassword, - iv, - tag, + cerIv, + cerTag, + keyIv, + keyTag, + passwordIv, + passwordTag, } = encryptFielCredentials(cerData, keyData, password); - + // Guardar o actualizar en BD await prisma.fielCredential.upsert({ where: { tenantId }, @@ -76,8 +80,12 @@ export async function uploadFiel( cerData: encryptedCer, keyData: encryptedKey, keyPasswordEncrypted: encryptedPassword, - encryptionIv: iv, - encryptionTag: tag, + cerIv, + cerTag, + keyIv, + keyTag, + passwordIv, + passwordTag, serialNumber, validFrom, validUntil, @@ -88,8 +96,12 @@ export async function uploadFiel( cerData: encryptedCer, keyData: encryptedKey, keyPasswordEncrypted: encryptedPassword, - encryptionIv: iv, - encryptionTag: tag, + cerIv, + cerTag, + keyIv, + keyTag, + passwordIv, + passwordTag, serialNumber, validFrom, validUntil, @@ -198,13 +210,17 @@ export async function getDecryptedFiel(tenantId: string): Promise<{ } try { - // Desencriptar todas las credenciales juntas + // Desencriptar credenciales (per-component IV/tag) const { cerData, keyData, password } = decryptFielCredentials( Buffer.from(fiel.cerData), Buffer.from(fiel.keyData), Buffer.from(fiel.keyPasswordEncrypted), - Buffer.from(fiel.encryptionIv), - Buffer.from(fiel.encryptionTag) + Buffer.from(fiel.cerIv), + Buffer.from(fiel.cerTag), + Buffer.from(fiel.keyIv), + Buffer.from(fiel.keyTag), + Buffer.from(fiel.passwordIv), + Buffer.from(fiel.passwordTag) ); return { diff --git a/apps/api/src/services/sat/sat-crypto.service.ts b/apps/api/src/services/sat/sat-crypto.service.ts index 3d16868..4228d50 100644 --- a/apps/api/src/services/sat/sat-crypto.service.ts +++ b/apps/api/src/services/sat/sat-crypto.service.ts @@ -6,10 +6,10 @@ const IV_LENGTH = 16; const TAG_LENGTH = 16; /** - * Deriva una clave de 256 bits del JWT_SECRET + * Deriva una clave de 256 bits del FIEL_ENCRYPTION_KEY */ function deriveKey(): Buffer { - return createHash('sha256').update(env.JWT_SECRET).digest(); + return createHash('sha256').update(env.FIEL_ENCRYPTION_KEY).digest(); } /** @@ -52,7 +52,7 @@ export function decryptToString(encrypted: Buffer, iv: Buffer, tag: Buffer): str } /** - * Encripta credenciales FIEL (cer, key, password) + * Encripta credenciales FIEL con IV/tag independiente por componente */ export function encryptFielCredentials( cerData: Buffer, @@ -62,61 +62,51 @@ export function encryptFielCredentials( encryptedCer: Buffer; encryptedKey: Buffer; encryptedPassword: Buffer; - iv: Buffer; - tag: Buffer; + cerIv: Buffer; + cerTag: Buffer; + keyIv: Buffer; + keyTag: Buffer; + passwordIv: Buffer; + passwordTag: Buffer; } { - // Usamos el mismo IV y tag para simplificar, concatenando los datos - const combined = Buffer.concat([ - Buffer.from(cerData.length.toString().padStart(10, '0')), - cerData, - Buffer.from(keyData.length.toString().padStart(10, '0')), - keyData, - Buffer.from(password, 'utf-8'), - ]); - - const { encrypted, iv, tag } = encrypt(combined); - - // Extraemos las partes encriptadas - const cerLength = cerData.length; - const keyLength = keyData.length; - const passwordLength = Buffer.from(password, 'utf-8').length; - + const cer = encrypt(cerData); + const key = encrypt(keyData); + const pwd = encrypt(Buffer.from(password, 'utf-8')); + return { - encryptedCer: encrypted.subarray(0, 10 + cerLength), - encryptedKey: encrypted.subarray(10 + cerLength, 20 + cerLength + keyLength), - encryptedPassword: encrypted.subarray(20 + cerLength + keyLength), - iv, - tag, + encryptedCer: cer.encrypted, + encryptedKey: key.encrypted, + encryptedPassword: pwd.encrypted, + cerIv: cer.iv, + cerTag: cer.tag, + keyIv: key.iv, + keyTag: key.tag, + passwordIv: pwd.iv, + passwordTag: pwd.tag, }; } /** - * Desencripta credenciales FIEL + * Desencripta credenciales FIEL (per-component IV/tag) */ export function decryptFielCredentials( encryptedCer: Buffer, encryptedKey: Buffer, encryptedPassword: Buffer, - iv: Buffer, - tag: Buffer + cerIv: Buffer, + cerTag: Buffer, + keyIv: Buffer, + keyTag: Buffer, + passwordIv: Buffer, + passwordTag: Buffer ): { cerData: Buffer; keyData: Buffer; password: string; } { - const combined = Buffer.concat([encryptedCer, encryptedKey, encryptedPassword]); - const decrypted = decrypt(combined, iv, tag); - - // Parseamos las partes - const cerLengthStr = decrypted.subarray(0, 10).toString(); - const cerLength = parseInt(cerLengthStr, 10); - const cerData = decrypted.subarray(10, 10 + cerLength); - - const keyLengthStr = decrypted.subarray(10 + cerLength, 20 + cerLength).toString(); - const keyLength = parseInt(keyLengthStr, 10); - const keyData = decrypted.subarray(20 + cerLength, 20 + cerLength + keyLength); - - const password = decrypted.subarray(20 + cerLength + keyLength).toString('utf-8'); - + const cerData = decrypt(encryptedCer, cerIv, cerTag); + const keyData = decrypt(encryptedKey, keyIv, keyTag); + const password = decrypt(encryptedPassword, passwordIv, passwordTag).toString('utf-8'); + return { cerData, keyData, password }; } diff --git a/apps/api/src/services/sat/sat.service.ts b/apps/api/src/services/sat/sat.service.ts index 1de7f59..c5869c9 100644 --- a/apps/api/src/services/sat/sat.service.ts +++ b/apps/api/src/services/sat/sat.service.ts @@ -20,7 +20,7 @@ interface SyncContext { service: Service; rfc: string; tenantId: string; - schemaName: string; + databaseName: string; } /** @@ -54,7 +54,7 @@ async function updateJobProgress( * Guarda los CFDIs en la base de datos del tenant */ async function saveCfdis( - schemaName: string, + databaseName: string, cfdis: CfdiParsed[], jobId: string ): Promise<{ inserted: number; updated: number }> { @@ -65,14 +65,14 @@ async function saveCfdis( try { // Usar raw query para el esquema del tenant const existing = await prisma.$queryRawUnsafe<{ id: string }[]>( - `SELECT id FROM "${schemaName}".cfdis WHERE uuid_fiscal = $1`, + `SELECT id FROM "${databaseName}".cfdis WHERE uuid_fiscal = $1`, cfdi.uuidFiscal ); if (existing.length > 0) { // Actualizar CFDI existente await prisma.$executeRawUnsafe( - `UPDATE "${schemaName}".cfdis SET + `UPDATE "${databaseName}".cfdis SET tipo = $2, serie = $3, folio = $4, @@ -128,7 +128,7 @@ async function saveCfdis( } else { // Insertar nuevo CFDI await prisma.$executeRawUnsafe( - `INSERT INTO "${schemaName}".cfdis ( + `INSERT INTO "${databaseName}".cfdis ( id, uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado, rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor, subtotal, descuento, iva, isr_retenido, iva_retenido, total, @@ -255,7 +255,7 @@ async function processDateRange( console.log(`[SAT] Procesando ${cfdis.length} CFDIs del paquete`); - const { inserted, updated } = await saveCfdis(ctx.schemaName, cfdis, jobId); + const { inserted, updated } = await saveCfdis(ctx.databaseName, cfdis, jobId); totalInserted += inserted; totalUpdated += updated; @@ -409,7 +409,7 @@ export async function startSync( // Obtener datos del tenant const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, - select: { schemaName: true }, + select: { databaseName: true }, }); if (!tenant) { @@ -446,7 +446,7 @@ export async function startSync( service, rfc: decryptedFiel.rfc, tenantId, - schemaName: tenant.schemaName, + databaseName: tenant.databaseName, }; // Ejecutar sincronización en background diff --git a/apps/api/src/services/tenants.service.ts b/apps/api/src/services/tenants.service.ts index 10e302b..806943e 100644 --- a/apps/api/src/services/tenants.service.ts +++ b/apps/api/src/services/tenants.service.ts @@ -8,7 +8,7 @@ export async function getAllTenants() { nombre: true, rfc: true, plan: true, - schemaName: true, + databaseName: true, createdAt: true, _count: { select: { users: true } @@ -26,7 +26,7 @@ export async function getTenantById(id: string) { nombre: true, rfc: true, plan: true, - schemaName: true, + databaseName: true, cfdiLimit: true, usersLimit: true, createdAt: true, @@ -41,7 +41,7 @@ export async function createTenant(data: { cfdiLimit?: number; usersLimit?: number; }) { - const schemaName = `tenant_${data.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`; + const databaseName = `horux_${data.rfc.toLowerCase().replace(/[^a-z0-9]/g, '')}`; // Create tenant record const tenant = await prisma.tenant.create({ @@ -49,18 +49,18 @@ export async function createTenant(data: { nombre: data.nombre, rfc: data.rfc.toUpperCase(), plan: data.plan || 'starter', - schemaName, + databaseName, cfdiLimit: data.cfdiLimit || 500, usersLimit: data.usersLimit || 3, } }); // Create schema and tables for the tenant - await prisma.$executeRawUnsafe(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`); + await prisma.$executeRawUnsafe(`CREATE SCHEMA IF NOT EXISTS "${databaseName}"`); // Create CFDIs table await prisma.$executeRawUnsafe(` - CREATE TABLE IF NOT EXISTS "${schemaName}"."cfdis" ( + CREATE TABLE IF NOT EXISTS "${databaseName}"."cfdis" ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), uuid_fiscal VARCHAR(36) UNIQUE NOT NULL, tipo VARCHAR(20) NOT NULL, @@ -92,7 +92,7 @@ export async function createTenant(data: { // Create IVA monthly table await prisma.$executeRawUnsafe(` - CREATE TABLE IF NOT EXISTS "${schemaName}"."iva_mensual" ( + CREATE TABLE IF NOT EXISTS "${databaseName}"."iva_mensual" ( id SERIAL PRIMARY KEY, año INT NOT NULL, mes INT NOT NULL, @@ -109,7 +109,7 @@ export async function createTenant(data: { // Create alerts table await prisma.$executeRawUnsafe(` - CREATE TABLE IF NOT EXISTS "${schemaName}"."alertas" ( + CREATE TABLE IF NOT EXISTS "${databaseName}"."alertas" ( id SERIAL PRIMARY KEY, tipo VARCHAR(50) NOT NULL, titulo VARCHAR(200) NOT NULL, @@ -124,7 +124,7 @@ export async function createTenant(data: { // Create calendario_fiscal table await prisma.$executeRawUnsafe(` - CREATE TABLE IF NOT EXISTS "${schemaName}"."calendario_fiscal" ( + CREATE TABLE IF NOT EXISTS "${databaseName}"."calendario_fiscal" ( id SERIAL PRIMARY KEY, titulo VARCHAR(200) NOT NULL, descripcion TEXT, @@ -163,7 +163,7 @@ export async function updateTenant(id: string, data: { nombre: true, rfc: true, plan: true, - schemaName: true, + databaseName: true, cfdiLimit: true, usersLimit: true, active: true, diff --git a/apps/web/lib/api/tenants.ts b/apps/web/lib/api/tenants.ts index 6cd57f1..f8ecf44 100644 --- a/apps/web/lib/api/tenants.ts +++ b/apps/web/lib/api/tenants.ts @@ -5,7 +5,7 @@ export interface Tenant { nombre: string; rfc: string; plan: string; - schemaName: string; + databaseName: string; createdAt: string; _count?: { users: number; diff --git a/packages/shared/src/types/auth.ts b/packages/shared/src/types/auth.ts index 2fdeac5..40c7e28 100644 --- a/packages/shared/src/types/auth.ts +++ b/packages/shared/src/types/auth.ts @@ -36,7 +36,7 @@ export interface JWTPayload { email: string; role: Role; tenantId: string; - schemaName: string; + databaseName: string; iat?: number; exp?: number; } diff --git a/packages/shared/src/types/tenant.ts b/packages/shared/src/types/tenant.ts index 79758e3..f9bdb94 100644 --- a/packages/shared/src/types/tenant.ts +++ b/packages/shared/src/types/tenant.ts @@ -5,7 +5,7 @@ export interface Tenant { nombre: string; rfc: string; plan: Plan; - schemaName: string; + databaseName: string; cfdiLimit: number; usersLimit: number; active: boolean; @@ -20,3 +20,29 @@ export interface TenantUsage { usersLimit: number; plan: Plan; } + +export interface Subscription { + id: string; + tenantId: string; + plan: Plan; + mpPreapprovalId?: string; + status: 'pending' | 'authorized' | 'paused' | 'cancelled'; + amount: number; + frequency: 'monthly' | 'yearly'; + currentPeriodStart?: string; + currentPeriodEnd?: string; + createdAt: string; + updatedAt: string; +} + +export interface Payment { + id: string; + tenantId: string; + subscriptionId?: string; + mpPaymentId?: string; + amount: number; + status: 'approved' | 'pending' | 'rejected' | 'refunded'; + paymentMethod?: string; + paidAt?: string; + createdAt: string; +}