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 {
|
||||
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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -23,20 +23,20 @@ export async function register(data: RegisterRequest): Promise<LoginResponse> {
|
||||
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<LoginResponse> {
|
||||
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<LoginResponse> {
|
||||
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);
|
||||
|
||||
@@ -58,13 +58,17 @@ 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface Tenant {
|
||||
nombre: string;
|
||||
rfc: string;
|
||||
plan: string;
|
||||
schemaName: string;
|
||||
databaseName: string;
|
||||
createdAt: string;
|
||||
_count?: {
|
||||
users: number;
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface JWTPayload {
|
||||
email: string;
|
||||
role: Role;
|
||||
tenantId: string;
|
||||
schemaName: string;
|
||||
databaseName: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user