feat(saas): update schema for db-per-tenant and per-component FIEL encryption
- Rename Tenant.schemaName to databaseName across all services - Add Subscription and Payment models to Prisma schema - Update FielCredential to per-component IV/tag encryption columns - Switch FIEL encryption key from JWT_SECRET to FIEL_ENCRYPTION_KEY - Add Subscription and Payment shared types - Update JWTPayload to use databaseName Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,20 +8,22 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Tenant {
|
model Tenant {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
nombre String
|
nombre String
|
||||||
rfc String @unique
|
rfc String @unique
|
||||||
plan Plan @default(starter)
|
plan Plan @default(starter)
|
||||||
schemaName String @unique @map("schema_name")
|
databaseName String @unique @map("database_name")
|
||||||
cfdiLimit Int @map("cfdi_limit")
|
cfdiLimit Int @default(100) @map("cfdi_limit")
|
||||||
usersLimit Int @map("users_limit")
|
usersLimit Int @default(1) @map("users_limit")
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
expiresAt DateTime? @map("expires_at")
|
expiresAt DateTime? @map("expires_at")
|
||||||
|
|
||||||
users User[]
|
users User[]
|
||||||
fielCredential FielCredential?
|
fielCredential FielCredential?
|
||||||
satSyncJobs SatSyncJob[]
|
satSyncJobs SatSyncJob[]
|
||||||
|
subscriptions Subscription[]
|
||||||
|
payments Payment[]
|
||||||
|
|
||||||
@@map("tenants")
|
@@map("tenants")
|
||||||
}
|
}
|
||||||
@@ -76,8 +78,12 @@ model FielCredential {
|
|||||||
cerData Bytes @map("cer_data")
|
cerData Bytes @map("cer_data")
|
||||||
keyData Bytes @map("key_data")
|
keyData Bytes @map("key_data")
|
||||||
keyPasswordEncrypted Bytes @map("key_password_encrypted")
|
keyPasswordEncrypted Bytes @map("key_password_encrypted")
|
||||||
encryptionIv Bytes @map("encryption_iv")
|
cerIv Bytes @map("cer_iv")
|
||||||
encryptionTag Bytes @map("encryption_tag")
|
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)
|
serialNumber String? @map("serial_number") @db.VarChar(50)
|
||||||
validFrom DateTime @map("valid_from")
|
validFrom DateTime @map("valid_from")
|
||||||
validUntil DateTime @map("valid_until")
|
validUntil DateTime @map("valid_until")
|
||||||
@@ -90,6 +96,46 @@ model FielCredential {
|
|||||||
@@map("fiel_credentials")
|
@@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 {
|
model SatSyncJob {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
tenantId String @map("tenant_id")
|
tenantId String @map("tenant_id")
|
||||||
|
|||||||
@@ -29,17 +29,17 @@ export async function tenantMiddleware(req: Request, res: Response, next: NextFu
|
|||||||
|
|
||||||
const tenant = await prisma.tenant.findUnique({
|
const tenant = await prisma.tenant.findUnique({
|
||||||
where: { id: tenantId },
|
where: { id: tenantId },
|
||||||
select: { schemaName: true, active: true },
|
select: { databaseName: true, active: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!tenant || !tenant.active) {
|
if (!tenant || !tenant.active) {
|
||||||
return next(new AppError(403, 'Tenant no encontrado o inactivo'));
|
return next(new AppError(403, 'Tenant no encontrado o inactivo'));
|
||||||
}
|
}
|
||||||
|
|
||||||
req.tenantSchema = tenant.schemaName;
|
req.tenantSchema = tenant.databaseName;
|
||||||
|
|
||||||
// Set search_path for this request
|
// Set search_path for this request (will be replaced by pool-based approach)
|
||||||
await prisma.$executeRawUnsafe(`SET search_path TO "${tenant.schemaName}", public`);
|
await prisma.$executeRawUnsafe(`SET search_path TO "${tenant.databaseName}", public`);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -23,20 +23,20 @@ export async function register(data: RegisterRequest): Promise<LoginResponse> {
|
|||||||
throw new AppError(400, 'El RFC ya está registrado');
|
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({
|
const tenant = await prisma.tenant.create({
|
||||||
data: {
|
data: {
|
||||||
nombre: data.empresa.nombre,
|
nombre: data.empresa.nombre,
|
||||||
rfc: data.empresa.rfc.toUpperCase(),
|
rfc: data.empresa.rfc.toUpperCase(),
|
||||||
plan: 'starter',
|
plan: 'starter',
|
||||||
schemaName,
|
databaseName,
|
||||||
cfdiLimit: PLANS.starter.cfdiLimit,
|
cfdiLimit: PLANS.starter.cfdiLimit,
|
||||||
usersLimit: PLANS.starter.usersLimit,
|
usersLimit: PLANS.starter.usersLimit,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTenantSchema(schemaName);
|
await createTenantSchema(databaseName);
|
||||||
|
|
||||||
const passwordHash = await hashPassword(data.usuario.password);
|
const passwordHash = await hashPassword(data.usuario.password);
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
@@ -54,7 +54,7 @@ export async function register(data: RegisterRequest): Promise<LoginResponse> {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
schemaName: tenant.schemaName,
|
databaseName: tenant.databaseName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessToken = generateAccessToken(tokenPayload);
|
const accessToken = generateAccessToken(tokenPayload);
|
||||||
@@ -117,7 +117,7 @@ export async function login(data: LoginRequest): Promise<LoginResponse> {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
tenantId: user.tenantId,
|
tenantId: user.tenantId,
|
||||||
schemaName: user.tenant.schemaName,
|
databaseName: user.tenant.databaseName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessToken = generateAccessToken(tokenPayload);
|
const accessToken = generateAccessToken(tokenPayload);
|
||||||
@@ -181,7 +181,7 @@ export async function refreshTokens(token: string): Promise<{ accessToken: strin
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
tenantId: user.tenantId,
|
tenantId: user.tenantId,
|
||||||
schemaName: user.tenant.schemaName,
|
databaseName: user.tenant.databaseName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessToken = generateAccessToken(newTokenPayload);
|
const accessToken = generateAccessToken(newTokenPayload);
|
||||||
|
|||||||
@@ -58,15 +58,19 @@ export async function uploadFiel(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encriptar credenciales (todas juntas con el mismo IV/tag)
|
// Encriptar credenciales (per-component IV/tag)
|
||||||
const {
|
const {
|
||||||
encryptedCer,
|
encryptedCer,
|
||||||
encryptedKey,
|
encryptedKey,
|
||||||
encryptedPassword,
|
encryptedPassword,
|
||||||
iv,
|
cerIv,
|
||||||
tag,
|
cerTag,
|
||||||
|
keyIv,
|
||||||
|
keyTag,
|
||||||
|
passwordIv,
|
||||||
|
passwordTag,
|
||||||
} = encryptFielCredentials(cerData, keyData, password);
|
} = encryptFielCredentials(cerData, keyData, password);
|
||||||
|
|
||||||
// Guardar o actualizar en BD
|
// Guardar o actualizar en BD
|
||||||
await prisma.fielCredential.upsert({
|
await prisma.fielCredential.upsert({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
@@ -76,8 +80,12 @@ export async function uploadFiel(
|
|||||||
cerData: encryptedCer,
|
cerData: encryptedCer,
|
||||||
keyData: encryptedKey,
|
keyData: encryptedKey,
|
||||||
keyPasswordEncrypted: encryptedPassword,
|
keyPasswordEncrypted: encryptedPassword,
|
||||||
encryptionIv: iv,
|
cerIv,
|
||||||
encryptionTag: tag,
|
cerTag,
|
||||||
|
keyIv,
|
||||||
|
keyTag,
|
||||||
|
passwordIv,
|
||||||
|
passwordTag,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
validFrom,
|
validFrom,
|
||||||
validUntil,
|
validUntil,
|
||||||
@@ -88,8 +96,12 @@ export async function uploadFiel(
|
|||||||
cerData: encryptedCer,
|
cerData: encryptedCer,
|
||||||
keyData: encryptedKey,
|
keyData: encryptedKey,
|
||||||
keyPasswordEncrypted: encryptedPassword,
|
keyPasswordEncrypted: encryptedPassword,
|
||||||
encryptionIv: iv,
|
cerIv,
|
||||||
encryptionTag: tag,
|
cerTag,
|
||||||
|
keyIv,
|
||||||
|
keyTag,
|
||||||
|
passwordIv,
|
||||||
|
passwordTag,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
validFrom,
|
validFrom,
|
||||||
validUntil,
|
validUntil,
|
||||||
@@ -198,13 +210,17 @@ export async function getDecryptedFiel(tenantId: string): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Desencriptar todas las credenciales juntas
|
// Desencriptar credenciales (per-component IV/tag)
|
||||||
const { cerData, keyData, password } = decryptFielCredentials(
|
const { cerData, keyData, password } = decryptFielCredentials(
|
||||||
Buffer.from(fiel.cerData),
|
Buffer.from(fiel.cerData),
|
||||||
Buffer.from(fiel.keyData),
|
Buffer.from(fiel.keyData),
|
||||||
Buffer.from(fiel.keyPasswordEncrypted),
|
Buffer.from(fiel.keyPasswordEncrypted),
|
||||||
Buffer.from(fiel.encryptionIv),
|
Buffer.from(fiel.cerIv),
|
||||||
Buffer.from(fiel.encryptionTag)
|
Buffer.from(fiel.cerTag),
|
||||||
|
Buffer.from(fiel.keyIv),
|
||||||
|
Buffer.from(fiel.keyTag),
|
||||||
|
Buffer.from(fiel.passwordIv),
|
||||||
|
Buffer.from(fiel.passwordTag)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ const IV_LENGTH = 16;
|
|||||||
const TAG_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 {
|
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(
|
export function encryptFielCredentials(
|
||||||
cerData: Buffer,
|
cerData: Buffer,
|
||||||
@@ -62,61 +62,51 @@ export function encryptFielCredentials(
|
|||||||
encryptedCer: Buffer;
|
encryptedCer: Buffer;
|
||||||
encryptedKey: Buffer;
|
encryptedKey: Buffer;
|
||||||
encryptedPassword: Buffer;
|
encryptedPassword: Buffer;
|
||||||
iv: Buffer;
|
cerIv: Buffer;
|
||||||
tag: Buffer;
|
cerTag: Buffer;
|
||||||
|
keyIv: Buffer;
|
||||||
|
keyTag: Buffer;
|
||||||
|
passwordIv: Buffer;
|
||||||
|
passwordTag: Buffer;
|
||||||
} {
|
} {
|
||||||
// Usamos el mismo IV y tag para simplificar, concatenando los datos
|
const cer = encrypt(cerData);
|
||||||
const combined = Buffer.concat([
|
const key = encrypt(keyData);
|
||||||
Buffer.from(cerData.length.toString().padStart(10, '0')),
|
const pwd = encrypt(Buffer.from(password, 'utf-8'));
|
||||||
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;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptedCer: encrypted.subarray(0, 10 + cerLength),
|
encryptedCer: cer.encrypted,
|
||||||
encryptedKey: encrypted.subarray(10 + cerLength, 20 + cerLength + keyLength),
|
encryptedKey: key.encrypted,
|
||||||
encryptedPassword: encrypted.subarray(20 + cerLength + keyLength),
|
encryptedPassword: pwd.encrypted,
|
||||||
iv,
|
cerIv: cer.iv,
|
||||||
tag,
|
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(
|
export function decryptFielCredentials(
|
||||||
encryptedCer: Buffer,
|
encryptedCer: Buffer,
|
||||||
encryptedKey: Buffer,
|
encryptedKey: Buffer,
|
||||||
encryptedPassword: Buffer,
|
encryptedPassword: Buffer,
|
||||||
iv: Buffer,
|
cerIv: Buffer,
|
||||||
tag: Buffer
|
cerTag: Buffer,
|
||||||
|
keyIv: Buffer,
|
||||||
|
keyTag: Buffer,
|
||||||
|
passwordIv: Buffer,
|
||||||
|
passwordTag: Buffer
|
||||||
): {
|
): {
|
||||||
cerData: Buffer;
|
cerData: Buffer;
|
||||||
keyData: Buffer;
|
keyData: Buffer;
|
||||||
password: string;
|
password: string;
|
||||||
} {
|
} {
|
||||||
const combined = Buffer.concat([encryptedCer, encryptedKey, encryptedPassword]);
|
const cerData = decrypt(encryptedCer, cerIv, cerTag);
|
||||||
const decrypted = decrypt(combined, iv, tag);
|
const keyData = decrypt(encryptedKey, keyIv, keyTag);
|
||||||
|
const password = decrypt(encryptedPassword, passwordIv, passwordTag).toString('utf-8');
|
||||||
// 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');
|
|
||||||
|
|
||||||
return { cerData, keyData, password };
|
return { cerData, keyData, password };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface SyncContext {
|
|||||||
service: Service;
|
service: Service;
|
||||||
rfc: string;
|
rfc: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
schemaName: string;
|
databaseName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +54,7 @@ async function updateJobProgress(
|
|||||||
* Guarda los CFDIs en la base de datos del tenant
|
* Guarda los CFDIs en la base de datos del tenant
|
||||||
*/
|
*/
|
||||||
async function saveCfdis(
|
async function saveCfdis(
|
||||||
schemaName: string,
|
databaseName: string,
|
||||||
cfdis: CfdiParsed[],
|
cfdis: CfdiParsed[],
|
||||||
jobId: string
|
jobId: string
|
||||||
): Promise<{ inserted: number; updated: number }> {
|
): Promise<{ inserted: number; updated: number }> {
|
||||||
@@ -65,14 +65,14 @@ async function saveCfdis(
|
|||||||
try {
|
try {
|
||||||
// Usar raw query para el esquema del tenant
|
// Usar raw query para el esquema del tenant
|
||||||
const existing = await prisma.$queryRawUnsafe<{ id: string }[]>(
|
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
|
cfdi.uuidFiscal
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
// Actualizar CFDI existente
|
// Actualizar CFDI existente
|
||||||
await prisma.$executeRawUnsafe(
|
await prisma.$executeRawUnsafe(
|
||||||
`UPDATE "${schemaName}".cfdis SET
|
`UPDATE "${databaseName}".cfdis SET
|
||||||
tipo = $2,
|
tipo = $2,
|
||||||
serie = $3,
|
serie = $3,
|
||||||
folio = $4,
|
folio = $4,
|
||||||
@@ -128,7 +128,7 @@ async function saveCfdis(
|
|||||||
} else {
|
} else {
|
||||||
// Insertar nuevo CFDI
|
// Insertar nuevo CFDI
|
||||||
await prisma.$executeRawUnsafe(
|
await prisma.$executeRawUnsafe(
|
||||||
`INSERT INTO "${schemaName}".cfdis (
|
`INSERT INTO "${databaseName}".cfdis (
|
||||||
id, uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
|
id, uuid_fiscal, tipo, serie, folio, fecha_emision, fecha_timbrado,
|
||||||
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
|
rfc_emisor, nombre_emisor, rfc_receptor, nombre_receptor,
|
||||||
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
|
subtotal, descuento, iva, isr_retenido, iva_retenido, total,
|
||||||
@@ -255,7 +255,7 @@ async function processDateRange(
|
|||||||
|
|
||||||
console.log(`[SAT] Procesando ${cfdis.length} CFDIs del paquete`);
|
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;
|
totalInserted += inserted;
|
||||||
totalUpdated += updated;
|
totalUpdated += updated;
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ export async function startSync(
|
|||||||
// Obtener datos del tenant
|
// Obtener datos del tenant
|
||||||
const tenant = await prisma.tenant.findUnique({
|
const tenant = await prisma.tenant.findUnique({
|
||||||
where: { id: tenantId },
|
where: { id: tenantId },
|
||||||
select: { schemaName: true },
|
select: { databaseName: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
@@ -446,7 +446,7 @@ export async function startSync(
|
|||||||
service,
|
service,
|
||||||
rfc: decryptedFiel.rfc,
|
rfc: decryptedFiel.rfc,
|
||||||
tenantId,
|
tenantId,
|
||||||
schemaName: tenant.schemaName,
|
databaseName: tenant.databaseName,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ejecutar sincronización en background
|
// Ejecutar sincronización en background
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function getAllTenants() {
|
|||||||
nombre: true,
|
nombre: true,
|
||||||
rfc: true,
|
rfc: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
schemaName: true,
|
databaseName: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: { users: true }
|
select: { users: true }
|
||||||
@@ -26,7 +26,7 @@ export async function getTenantById(id: string) {
|
|||||||
nombre: true,
|
nombre: true,
|
||||||
rfc: true,
|
rfc: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
schemaName: true,
|
databaseName: true,
|
||||||
cfdiLimit: true,
|
cfdiLimit: true,
|
||||||
usersLimit: true,
|
usersLimit: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
@@ -41,7 +41,7 @@ export async function createTenant(data: {
|
|||||||
cfdiLimit?: number;
|
cfdiLimit?: number;
|
||||||
usersLimit?: 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
|
// Create tenant record
|
||||||
const tenant = await prisma.tenant.create({
|
const tenant = await prisma.tenant.create({
|
||||||
@@ -49,18 +49,18 @@ export async function createTenant(data: {
|
|||||||
nombre: data.nombre,
|
nombre: data.nombre,
|
||||||
rfc: data.rfc.toUpperCase(),
|
rfc: data.rfc.toUpperCase(),
|
||||||
plan: data.plan || 'starter',
|
plan: data.plan || 'starter',
|
||||||
schemaName,
|
databaseName,
|
||||||
cfdiLimit: data.cfdiLimit || 500,
|
cfdiLimit: data.cfdiLimit || 500,
|
||||||
usersLimit: data.usersLimit || 3,
|
usersLimit: data.usersLimit || 3,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create schema and tables for the tenant
|
// 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
|
// Create CFDIs table
|
||||||
await prisma.$executeRawUnsafe(`
|
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(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
uuid_fiscal VARCHAR(36) UNIQUE NOT NULL,
|
uuid_fiscal VARCHAR(36) UNIQUE NOT NULL,
|
||||||
tipo VARCHAR(20) NOT NULL,
|
tipo VARCHAR(20) NOT NULL,
|
||||||
@@ -92,7 +92,7 @@ export async function createTenant(data: {
|
|||||||
|
|
||||||
// Create IVA monthly table
|
// Create IVA monthly table
|
||||||
await prisma.$executeRawUnsafe(`
|
await prisma.$executeRawUnsafe(`
|
||||||
CREATE TABLE IF NOT EXISTS "${schemaName}"."iva_mensual" (
|
CREATE TABLE IF NOT EXISTS "${databaseName}"."iva_mensual" (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
año INT NOT NULL,
|
año INT NOT NULL,
|
||||||
mes INT NOT NULL,
|
mes INT NOT NULL,
|
||||||
@@ -109,7 +109,7 @@ export async function createTenant(data: {
|
|||||||
|
|
||||||
// Create alerts table
|
// Create alerts table
|
||||||
await prisma.$executeRawUnsafe(`
|
await prisma.$executeRawUnsafe(`
|
||||||
CREATE TABLE IF NOT EXISTS "${schemaName}"."alertas" (
|
CREATE TABLE IF NOT EXISTS "${databaseName}"."alertas" (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
tipo VARCHAR(50) NOT NULL,
|
tipo VARCHAR(50) NOT NULL,
|
||||||
titulo VARCHAR(200) NOT NULL,
|
titulo VARCHAR(200) NOT NULL,
|
||||||
@@ -124,7 +124,7 @@ export async function createTenant(data: {
|
|||||||
|
|
||||||
// Create calendario_fiscal table
|
// Create calendario_fiscal table
|
||||||
await prisma.$executeRawUnsafe(`
|
await prisma.$executeRawUnsafe(`
|
||||||
CREATE TABLE IF NOT EXISTS "${schemaName}"."calendario_fiscal" (
|
CREATE TABLE IF NOT EXISTS "${databaseName}"."calendario_fiscal" (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
titulo VARCHAR(200) NOT NULL,
|
titulo VARCHAR(200) NOT NULL,
|
||||||
descripcion TEXT,
|
descripcion TEXT,
|
||||||
@@ -163,7 +163,7 @@ export async function updateTenant(id: string, data: {
|
|||||||
nombre: true,
|
nombre: true,
|
||||||
rfc: true,
|
rfc: true,
|
||||||
plan: true,
|
plan: true,
|
||||||
schemaName: true,
|
databaseName: true,
|
||||||
cfdiLimit: true,
|
cfdiLimit: true,
|
||||||
usersLimit: true,
|
usersLimit: true,
|
||||||
active: true,
|
active: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface Tenant {
|
|||||||
nombre: string;
|
nombre: string;
|
||||||
rfc: string;
|
rfc: string;
|
||||||
plan: string;
|
plan: string;
|
||||||
schemaName: string;
|
databaseName: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_count?: {
|
_count?: {
|
||||||
users: number;
|
users: number;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface JWTPayload {
|
|||||||
email: string;
|
email: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
schemaName: string;
|
databaseName: string;
|
||||||
iat?: number;
|
iat?: number;
|
||||||
exp?: number;
|
exp?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface Tenant {
|
|||||||
nombre: string;
|
nombre: string;
|
||||||
rfc: string;
|
rfc: string;
|
||||||
plan: Plan;
|
plan: Plan;
|
||||||
schemaName: string;
|
databaseName: string;
|
||||||
cfdiLimit: number;
|
cfdiLimit: number;
|
||||||
usersLimit: number;
|
usersLimit: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@@ -20,3 +20,29 @@ export interface TenantUsage {
|
|||||||
usersLimit: number;
|
usersLimit: number;
|
||||||
plan: Plan;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user