ImagLend — Modelo de Datos
Base de Datos
- Nombre:
imagy_lending - Usuario:
imagy_lending_app(NOBYPASSRLS) - RLS: Habilitado en todas las tablas (tenant_id)
Diagrama ER
Tablas
credit_products
Metadata del producto de credito (no contiene la configuracion — esa vive en product_versions).
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
tenant_id | UUID | Tenant propietario (RLS) |
code | VARCHAR(50) | Codigo unico dentro del tenant |
name | VARCHAR(255) | Nombre del producto |
description | TEXT | Descripcion |
category | VARCHAR(100) | Categoria (microcredito, consumo, linea_rotativa) |
is_active | BOOLEAN | Si esta disponible para nuevas solicitudes |
row_version | INT | Concurrencia optimista |
created_at | TIMESTAMPTZ | Fecha de creacion |
updated_at | TIMESTAMPTZ | Ultima modificacion |
created_by | UUID | Actor que lo creo |
Constraint: UNIQUE(tenant_id, code)
product_versions
Versiones inmutables del producto. Cada cambio aprobado crea una nueva version. Las solicitudes referencian la version especifica.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
product_id | UUID FK | Producto padre |
version_number | INT | Numero secuencial (1, 2, 3...) |
status | VARCHAR(20) | active, archived |
amount_config | JSONB | Configuracion de montos |
term_config | JSONB | Configuracion de plazos |
rate_config | JSONB | Configuracion de tasas |
fee_config | JSONB | Configuracion de fees (array flexible) |
requirements | JSONB | Requisitos del solicitante |
validation_flow_id | UUID | Flujo de ImagFlow para validar (nullable) |
signature_required | BOOLEAN | Si requiere firma |
signature_type | VARCHAR(20) | digital o electronic |
config_snapshot | JSONB | Snapshot completo de toda la configuracion (redundante para auditoria) |
change_request_id | UUID FK | Change request que origino esta version |
change_summary | TEXT | Resumen del cambio respecto a la version anterior |
created_by | UUID | Quien solicito el cambio |
approved_by | UUID | Quien aprobo el cambio |
created_at | TIMESTAMPTZ | Fecha de creacion |
published_at | TIMESTAMPTZ | Cuando se activo |
archived_at | TIMESTAMPTZ | Cuando se archivo (nullable) |
Reglas:
- Solo 1 version
activepor producto - Al publicar nueva version, la anterior pasa a
archived - Las solicitudes referencian
product_version_id, noproduct_id - Una version archivada nunca se modifica
credit_applications
Solicitudes de credito (pre-aprobacion). Cada solicitud referencia la version exacta del producto con la que fue creada.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
tenant_id | UUID | Tenant (RLS) |
organization_id | UUID | Organizacion (nullable) |
product_id | UUID FK | Producto solicitado |
product_version_id | UUID FK | Version exacta del producto al momento de crear la solicitud |
product_config_snapshot | JSONB | Copia de la configuracion del producto (inmutable) |
status | VARCHAR(30) | draft, submitted, under_review, approved, rejected, expired |
subject_data | JSONB | Datos del solicitante (nombre, documento, contacto) |
financial_data | JSONB | Datos financieros (ingresos, egresos, actividad) |
simulation_result | JSONB | Resultado de la simulacion (monto, plazo, desglose) |
origin | JSONB | Origen de la solicitud (type, channel, flow_id, etc.) |
flow_execution_id | UUID | ID de la ejecucion en ImagFlow (nullable) |
signature_id | UUID | ID de la firma en ImagSign (nullable) |
access_token | VARCHAR(255) | Token para wizard publico (nullable) |
row_version | INT | Concurrencia optimista |
created_at | TIMESTAMPTZ | Fecha de creacion |
expires_at | TIMESTAMPTZ | Expiracion de la solicitud |
decided_at | TIMESTAMPTZ | Cuando se tomo la decision |
decided_by | UUID | Quien decidio (nullable si automatico) |
credits
Creditos activos (post-aprobacion y desembolso).
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
tenant_id | UUID | Tenant (RLS) |
application_id | UUID FK | Solicitud que lo origino |
product_id | UUID FK | Producto de credito |
status | VARCHAR(30) | approved, disbursed, active, past_due, defaulted, paid_off |
amount | DECIMAL(15,2) | Monto desembolsado |
currency | VARCHAR(3) | Moneda (USD, COP, etc.) |
term_days | INT | Plazo en dias |
interest_rate | DECIMAL(8,6) | Tasa de interes aplicada |
fee_breakdown | JSONB | Desglose de costos aplicados |
total_to_pay | DECIMAL(15,2) | Total a pagar (capital + intereses + costos) |
origin | JSONB | Origen (type, channel, flow_request_id) |
row_version | INT | Concurrencia optimista |
created_at | TIMESTAMPTZ | Fecha de creacion |
disbursed_at | TIMESTAMPTZ | Fecha de desembolso |
paid_off_at | TIMESTAMPTZ | Fecha de pago total (nullable) |
disbursements
Registro de desembolsos.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
credit_id | UUID FK | Credito asociado |
tenant_id | UUID | Tenant (RLS) |
amount | DECIMAL(15,2) | Monto desembolsado |
method | VARCHAR(50) | bank_transfer, wallet, check |
reference | VARCHAR(255) | Referencia de la transaccion |
status | VARCHAR(20) | pending, completed, failed |
disbursed_at | TIMESTAMPTZ | Fecha efectiva del desembolso |
created_at | TIMESTAMPTZ | Fecha de registro |
payments
Registro de pagos recibidos.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
credit_id | UUID FK | Credito asociado |
tenant_id | UUID | Tenant (RLS) |
amount | DECIMAL(15,2) | Monto pagado |
payment_type | VARCHAR(30) | installment, prepayment, partial, penalty |
reference | VARCHAR(255) | Referencia del pago |
status | VARCHAR(20) | pending, confirmed, reversed |
installment_number | INT | Numero de cuota (nullable si prepago) |
payment_date | TIMESTAMPTZ | Fecha del pago |
created_at | TIMESTAMPTZ | Fecha de registro |
amortization_schedule
Tabla de amortizacion generada al crear el credito.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
credit_id | UUID FK | Credito asociado |
installment_number | INT | Numero de cuota |
principal | DECIMAL(15,2) | Capital de la cuota |
interest | DECIMAL(15,2) | Interes de la cuota |
fees | DECIMAL(15,2) | Costos adicionales |
total | DECIMAL(15,2) | Total de la cuota |
balance_after | DECIMAL(15,2) | Saldo despues del pago |
due_date | DATE | Fecha de vencimiento |
status | VARCHAR(20) | pending, paid, past_due |
change_requests
Solicitudes de cambio (maker-checker) para el dominio de lending.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico |
tenant_id | UUID | Tenant (RLS) |
entity_type | VARCHAR(50) | credit_product, credit, configuration |
entity_id | UUID | ID de la entidad a modificar |
action | VARCHAR(20) | create, update, delete, activate, deactivate |
payload_before | JSONB | Estado actual (snapshot) |
payload_after | JSONB | Cambio propuesto |
status | VARCHAR(20) | pending, approved, rejected, applied, expired |
requested_by | UUID | Quien solicito |
reviewed_by | UUID | Quien reviso (nullable) |
review_notes | TEXT | Notas del revisor (nullable) |
created_at | TIMESTAMPTZ | Fecha de solicitud |
reviewed_at | TIMESTAMPTZ | Fecha de revision (nullable) |
applied_at | TIMESTAMPTZ | Fecha de aplicacion (nullable) |
Tablas de Soporte
idempotency_keys
Misma estructura que en ImagFlow (ver data-strategy).
processed_events
Misma estructura que en ImagFlow.
RLS Policies
ALTER TABLE credit_products ENABLE ROW LEVEL SECURITY;
ALTER TABLE credit_applications ENABLE ROW LEVEL SECURITY;
ALTER TABLE credits ENABLE ROW LEVEL SECURITY;
ALTER TABLE disbursements ENABLE ROW LEVEL SECURITY;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE amortization_schedule ENABLE ROW LEVEL SECURITY;
ALTER TABLE change_requests ENABLE ROW LEVEL SECURITY;
-- Todas las tablas usan la misma politica
CREATE POLICY tenant_isolation ON credit_products
FOR ALL USING (tenant_id = get_current_tenant_id());
-- Control plane para platform admin
CREATE POLICY control_plane ON credit_products
FOR ALL USING (
current_setting('app.current_tenant_id', true) IS NULL
OR current_setting('app.current_tenant_id', true) = ''
);Indices
-- Productos
CREATE UNIQUE INDEX idx_credit_products_tenant_code ON credit_products(tenant_id, code);
CREATE INDEX idx_credit_products_active ON credit_products(tenant_id, is_active);
-- Solicitudes
CREATE INDEX idx_applications_tenant_status ON credit_applications(tenant_id, status);
CREATE INDEX idx_applications_token ON credit_applications(access_token);
CREATE INDEX idx_applications_subject ON credit_applications(tenant_id, (subject_data->>'identifier_value'));
-- Creditos
CREATE INDEX idx_credits_tenant_status ON credits(tenant_id, status);
CREATE INDEX idx_credits_application ON credits(application_id);
-- Pagos
CREATE INDEX idx_payments_credit ON payments(credit_id);
CREATE INDEX idx_payments_date ON payments(tenant_id, payment_date DESC);
-- Amortizacion
CREATE INDEX idx_amortization_credit ON amortization_schedule(credit_id, installment_number);
CREATE INDEX idx_amortization_due ON amortization_schedule(due_date) WHERE status = 'pending';
-- Change requests
CREATE INDEX idx_change_requests_tenant_status ON change_requests(tenant_id, status);
CREATE INDEX idx_change_requests_entity ON change_requests(entity_type, entity_id);Calculos Financieros
Simulacion de Credito
// Funcion pura — sin efectos secundarios
function calculateCredit(
amount: number,
termDays: number,
product: CreditProductConfig
): SimulationResult {
const dailyRate = product.rate / 360;
const interest = amount * dailyRate * termDays;
// Calcular fees (array flexible)
const feeBreakdown = product.feeConfig.items.map(fee => ({
code: fee.code,
label: fee.label,
amount: fee.type === 'percentage'
? round(amount * fee.value)
: fee.value,
taxable: fee.taxable,
}));
const totalFees = feeBreakdown.reduce((sum, f) => sum + f.amount, 0);
// Calcular impuesto segun tax_applies_to
let taxableBase = 0;
if (product.feeConfig.tax_applies_to === 'interest') {
taxableBase = interest;
} else if (product.feeConfig.tax_applies_to === 'fees') {
taxableBase = feeBreakdown.filter(f => f.taxable).reduce((sum, f) => sum + f.amount, 0);
} else { // 'all'
taxableBase = interest + feeBreakdown.filter(f => f.taxable).reduce((sum, f) => sum + f.amount, 0);
}
const tax = round(taxableBase * product.feeConfig.tax_rate);
const totalToPay = amount + round(interest) + totalFees + tax;
return {
amount,
termDays,
interest: round(interest),
feeBreakdown,
totalFees,
tax,
totalToPay,
monthlyPayment: round(totalToPay / (termDays / 30)),
};
}Tabla de Amortizacion
Se genera al momento de crear el credito (post-aprobacion). Metodo: cuota fija (sistema frances) o capital constante (sistema aleman), configurable por producto.