Ir al contenido

Bancos

Ruta: /owner/settings/bank-connections Conecta bancos mediante Open Banking (TrueLayer) y gestiona cuentas para facturas.


El módulo de Bancos permite conectar cuentas bancarias reales mediante Open Banking o añadirlas manualmente para:

  • Importar transacciones automáticamente desde tu banco
  • Conciliar gastos con tus transacciones bancarias
  • Mostrar datos bancarios en facturas emitidas
  • Sincronizar saldos en tiempo real
TipoDescripciónFunciones
Open BankingConectada vía TrueLayerSincronización automática, transacciones, saldos
ManualAñadida por el usuarioSolo datos para facturas (IBAN, BIC, etc.)

┌──────────┐
│ Pending │ ← Esperando autenticación
└────┬─────┘
│ OAuth exitoso
┌────▼─────┐
│ Active │ ← Tokens válidos, sincronizando
└────┬─────┘
│ Token expira / revocado
┌────▼─────┐
│ Expired │ ← Requiere reconexión
└────┬─────┘
┌────▼─────┐
│ Revoked │ ← Usuario revocó acceso desde banco
└──────────┘
  1. Verificar límite: Comprueba plan del usuario (Free: 1, Basic: 3, Premium: Ilimitadas)
  2. Generar link OAuth: Edge function truelayer-auth genera URL autorización
  3. Autenticación: Usuario autoriza en popup del banco
  4. Intercambio código: Convierte código OAuth en access/refresh tokens
  5. Almacenar tokens: Tokens encriptados en PostgreSQL vía RPC update_bank_connection_tokens
  6. Configurar sync: Wizard post-conexión para rango histórico y frecuencia
  7. Primera sincronización: Importa cuentas, transacciones y saldos
FrecuenciaDescripciónRecomendado para
every_6hCada 6 horasNegocios activos con muchas transacciones
dailyUna vez al díaUso estándar, balance diario
weeklyCada lunesFreelancers, bajo volumen
manualSolo bajo demandaControl total, eventos puntuales

Las cuentas bancarias (tabla business_bank_accounts) se unifican para:

  • Seleccionarlas en facturas emitidas
  • Mostrar IBAN/BIC al cliente
  • Configurar cuenta predeterminada
CampoObligatorioDescripción
bank_nameNombre del banco (ej: Santander)
ibanCódigo IBAN formateado
bicNoCódigo BIC/SWIFT para transferencias internacionales
bank_addressNoDirección completa del banco
beneficiary_nameNoNombre del titular de la cuenta

Cuando conectas un banco vía Open Banking, las cuentas se importan automáticamente a business_bank_accounts:

  • Nuevas cuentas: Se crean con source: 'open_banking'
  • Cuentas existentes: Si existe IBAN manual, se convierte a Open Banking
  • Cuenta predeterminada: La primera cuenta se marca como predeterminada si no hay ninguna

FunciónDescripciónDocumentación
Conectar BancoConecta cuenta mediante TrueLayer OAuthVer
Añadir Cuenta ManualCrea cuenta con datos IBAN/BICVer
Sincronizar TransaccionesImporta transacciones desde bancoVer
Reconectar BancoRenueva tokens expirados/revocadosVer
Configurar CuentaEdita IBAN, BIC, dirección bancariaVer
Marcar PredeterminadaEstablece cuenta por defecto para facturasVer

PlanConexiones bancariasAuto-syncRetención transacciones
Free1 bancoDiaria90 días
Basic3 bancosCada 6h1 año
PremiumIlimitadasCada 6hIlimitado

Tablas de base de datos

Almacena conexiones OAuth con bancos vía TrueLayer.

ColumnaTipoDescripción
idUUIDID único de conexión
business_idUUIDFK a businesses
user_idUUIDFK a auth.users
connection_idTEXTID interno de TrueLayer
provider_nameTEXTNombre del banco (ej: “Santander”)
account_idsTEXT[]Array de IDs de cuentas TrueLayer
access_token_encryptedTEXTToken OAuth encriptado
refresh_token_encryptedTEXTRefresh token encriptado
token_expires_atTIMESTAMPTZExpiración del access token
statusTEXTpending, active, expired, revoked, disconnected
auto_sync_enabledBOOLEANSincronización automática habilitada
sync_frequencyTEXTevery_6h, daily, weekly, manual
last_sync_atTIMESTAMPTZÚltima sincronización exitosa
requires_reauthBOOLEANRequiere reconexión manual
consecutive_failuresINTEGERContador de fallos consecutivos
last_errorTEXTÚltimo mensaje de error

Constraints:

CHECK (status IN ('pending', 'active', 'expired', 'revoked', 'error', 'disconnected'))
CHECK ((status = 'active' AND access_token_encrypted IS NOT NULL) OR status != 'active')

Cuentas importadas desde Open Banking (solo para transacciones).

ColumnaTipoDescripción
idUUIDID único
connection_idUUIDFK a bank_connections
business_idUUIDFK a businesses
truelayer_account_idTEXTID TrueLayer de la cuenta
account_typeTEXTTipo de cuenta (ej: “TRANSACTION”)
display_nameTEXTNombre mostrado al usuario
ibanTEXTCódigo IBAN
current_balanceNUMERIC(12,2)Saldo actual
available_balanceNUMERIC(12,2)Saldo disponible
currencyTEXTMoneda (default: EUR)
is_activeBOOLEANCuenta activa
last_balance_updateTIMESTAMPTZÚltima actualización de saldo

Cuentas unificadas para facturas (manuales + Open Banking).

ColumnaTipoDescripción
idUUIDID único
business_idUUIDFK a businesses
bank_connection_idUUIDFK opcional a bank_connections
truelayer_account_idTEXTID TrueLayer si es Open Banking
sourceTEXTmanual o open_banking
bank_nameTEXTNombre del banco
bank_addressTEXTDirección del banco
ibanTEXTCódigo IBAN
bicTEXTCódigo BIC/SWIFT
beneficiary_nameTEXTNombre del beneficiario
is_defaultBOOLEANCuenta predeterminada para facturas
show_in_invoicesBOOLEANMostrar en selector de facturas
is_activeBOOLEANCuenta activa

Constraints:

UNIQUE (business_id, iban)
CHECK (source IN ('manual', 'open_banking'))

Transacciones importadas desde bancos.

ColumnaTipoDescripción
idUUIDID único
bank_account_idUUIDFK a bank_accounts
business_idUUIDFK a businesses
truelayer_transaction_idTEXTID TrueLayer
transaction_dateTIMESTAMPTZFecha de transacción
descriptionTEXTDescripción bancaria
amountNUMERIC(12,2)Importe absoluto
currencyTEXTMoneda
transaction_typeTEXTDEBIT o CREDIT
merchant_nameTEXTNombre del comercio
reconciliation_statusTEXTpending, matched, ignored, manual
matched_expense_idUUIDFK opcional a expenses

Constraints:

CHECK (transaction_type IN ('DEBIT', 'CREDIT'))
CHECK (reconciliation_status IN ('pending', 'matched', 'ignored', 'manual'))
UNIQUE (truelayer_transaction_id, bank_account_id)

Histórico de sincronizaciones.

ColumnaTipoDescripción
idUUIDID único
connection_idUUIDFK a bank_connections
user_idUUIDFK a auth.users
sync_typeTEXTaccounts, transactions, balances
statusTEXTpending, success, failed
records_syncedINTEGERNúmero de registros sincronizados
error_messageTEXTMensaje de error si falló
started_atTIMESTAMPTZInicio de sincronización
completed_atTIMESTAMPTZFin de sincronización
Políticas RLS
-- SELECT
POLICY "Users can view their business bank connections"
USING (business_id IN (
SELECT business_id FROM user_businesses
WHERE user_id = auth.uid()
));
-- INSERT
POLICY "Users can insert their business bank connections"
WITH CHECK (business_id IN (
SELECT business_id FROM user_businesses
WHERE user_id = auth.uid()
));
-- SELECT
POLICY "Users can view their business bank accounts"
USING (business_id IN (
SELECT business_id FROM user_businesses
WHERE user_id = auth.uid()
));
-- INSERT, UPDATE, DELETE
-- Similar isolation por business_id
Edge Functions

Ruta: supabase/functions/truelayer-auth

Acciones:

  • generate_auth_link: Genera URL OAuth de TrueLayer
  • exchange_code: Intercambia código OAuth por tokens

Parámetros (generate_auth_link):

{
action: 'generate_auth_link',
redirect_uri: string,
state?: string,
scopes?: string[],
business_id: string
}

Respuesta:

{
auth_url: string // URL para popup OAuth
}

Parámetros (exchange_code):

{
action: 'exchange_code',
code: string,
redirect_uri: string,
business_id: string
}

Respuesta:

{
success: boolean,
provider_name: string,
connection_id: string,
accounts_count: number,
is_reconnection?: boolean
}

Ruta: supabase/functions/truelayer-sync-transactions

Función: Sincroniza cuentas, saldos y transacciones desde TrueLayer.

Parámetros:

{
connection_id: string,
business_id: string,
from_date?: string, // YYYY-MM-DD
to_date?: string,
account_id?: string // Para sincronizar cuenta específica
}

Proceso:

  1. Desencripta tokens con RPC get_bank_connection_tokens
  2. Verifica expiración y refresca token si es necesario
  3. Fetch cuentas desde TrueLayer API /data/v1/accounts
  4. Fetch saldos desde /data/v1/accounts/{id}/balance
  5. Fetch transacciones desde /data/v1/accounts/{id}/transactions
  6. Upsert en bank_accounts y bank_transactions
  7. Preserva estado de reconciliación en transacciones existentes
  8. Actualiza bank_sync_history

Respuesta:

{
success: boolean,
accounts_synced: number,
transactions_imported: number,
transactions_updated: number,
accounts_total: number,
accounts_failed: number
}

Ruta: supabase/functions/refresh-bank-tokens

Función: Cron job que refresca tokens próximos a expirar.

Trigger: Cada 6 horas vía pg_cron

Componentes React principales

Archivo: src/features/bank/components/BankConnectionWizard.tsx

Función: Wizard post-conexión para configurar preferencias de sincronización.

Pasos:

  1. Seleccionar rango de importación (30_days, 90_days, 1_year, all)
  2. Seleccionar frecuencia de sync (every_6h, daily, weekly, manual)
  3. Confirmación y inicio de primera sincronización

Props:

interface BankConnectionWizardProps {
open: boolean;
onOpenChange: (open: boolean) => void;
providerName: string;
connectionId: string;
accountsCount: number;
onComplete: (preferences: SyncPreferences) => Promise<void>;
}

Archivo: src/features/bank/components/BankAccountFormDialog.tsx

Función: Formulario para crear/editar cuentas bancarias manuales.

Validaciones:

  • IBAN: Formato internacional con checksum
  • BIC/SWIFT: 8 o 11 caracteres
  • Auto-formateo con espacios para IBAN

Archivo: src/features/bank/hooks/useBankConnection.ts

Función: Gestiona flujo OAuth completo con TrueLayer.

Estados:

type ConnectionStatus =
| 'idle'
| 'checking_limit'
| 'generating_link'
| 'waiting_auth'
| 'exchanging_code'
| 'success'
| 'error';

API:

interface UseBankConnectionReturn {
isConnecting: boolean;
connectionStatus: ConnectionStatus;
error: TrueLayerError | null;
initiateConnection: () => Promise<void>;
handleCallback: (code: string) => Promise<void>;
cleanup: () => void;
syncAfterConnection: (connectionId: string, options?: SyncOptions) => Promise<any>;
lastConnection: { provider_name: string; connection_id: string; accounts_count: number } | null;
showWizard: boolean;
setShowWizard: (show: boolean) => void;
}
Schemas Zod / TypeScript
interface BusinessBankAccount {
id: string;
business_id: string;
bank_connection_id: string | null;
truelayer_account_id: string | null;
source: 'manual' | 'open_banking';
bank_name: string;
bank_address: string | null;
iban: string;
bic: string | null;
beneficiary_name: string | null;
is_default: boolean;
show_in_invoices: boolean;
is_active: boolean;
created_at: string;
updated_at: string;
}
interface TrueLayerAuthResponse {
success: boolean;
provider_name: string;
connection_id: string;
accounts_count: number;
}
interface SyncOptions {
fromDate?: string;
toDate?: string;
importRange?: '30_days' | '90_days' | '1_year' | 'all';
syncFrequency?: 'every_6h' | 'daily' | 'weekly' | 'manual';
}
Queries de diagnóstico

Conexiones activas de un negocio:

SELECT
bc.id,
bc.provider_name,
bc.status,
bc.auto_sync_enabled,
bc.last_sync_at,
bc.requires_reauth,
bc.consecutive_failures,
ARRAY_LENGTH(bc.account_ids, 1) as accounts_count
FROM bank_connections bc
WHERE bc.business_id = '<business_id>'
AND bc.status = 'active'
ORDER BY bc.created_at DESC;

Cuentas importadas desde conexión:

SELECT
ba.display_name,
ba.iban,
ba.current_balance,
ba.available_balance,
ba.currency,
ba.is_active,
ba.last_balance_update
FROM bank_accounts ba
WHERE ba.connection_id = '<connection_id>'
AND ba.business_id = '<business_id>'
ORDER BY ba.display_name;

Transacciones pendientes de conciliación:

SELECT
bt.transaction_date,
bt.description,
bt.merchant_name,
bt.amount,
bt.transaction_type,
bt.reconciliation_status,
ba.display_name as account_name
FROM bank_transactions bt
JOIN bank_accounts ba ON bt.bank_account_id = ba.id
WHERE bt.business_id = '<business_id>'
AND bt.reconciliation_status = 'pending'
ORDER BY bt.transaction_date DESC
LIMIT 50;

Histórico de sincronizaciones con errores:

SELECT
bsh.started_at,
bsh.completed_at,
bsh.sync_type,
bsh.status,
bsh.records_synced,
bsh.error_message,
bc.provider_name
FROM bank_sync_history bsh
JOIN bank_connections bc ON bsh.connection_id = bc.id
WHERE bsh.status = 'failed'
AND bsh.started_at > NOW() - INTERVAL '7 days'
ORDER BY bsh.started_at DESC;

Verificar tokens próximos a expirar:

SELECT
id,
provider_name,
token_expires_at,
token_expires_at - NOW() as time_until_expiry,
status,
requires_reauth
FROM bank_connections
WHERE status = 'active'
AND token_expires_at < NOW() + INTERVAL '1 day'
ORDER BY token_expires_at;

ErrorCausaSolución
invalid_tokenToken OAuth expirado o revocadoReconectar banco desde UI
LIMIT_REACHEDPlan no permite más conexionesActualizar plan o eliminar conexión existente
TOKEN_STORAGE_FAILEDError al encriptar tokensVerificar RPC update_bank_connection_tokens
refresh_failedRefresh token inválidoRequiere reautenticación manual
TIMEOUTPopup OAuth tardó >5 minutosReintentar conexión