- Create TASK-2026-01-25-NOTIFICACIONES-COMPLETAS with full CAPVED docs - Update DATABASE_INVENTORY with auth.notifications, auth.user_push_tokens, investment.distribution_history, investment.distribution_runs tables - Update BACKEND_INVENTORY with push-token endpoints, firebase.client, and unit tests - Update FRONTEND_INVENTORY with notification components, store, service - Update MASTER_INVENTORY with updated totals - Update _INDEX.yml with new task entry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.0 KiB
5.0 KiB
05-EJECUCION - Sistema de Notificaciones Completo
Resumen de Ejecucion
Fecha: 2026-01-25 Agente: claude-opus-4.5 Duracion: ~45 minutos
1. DDL Creados
auth/tables/11-notifications.sql
CREATE TABLE auth.notifications (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
priority VARCHAR(20) DEFAULT 'normal',
data JSONB,
action_url VARCHAR(500),
icon_type VARCHAR(20) DEFAULT 'info',
channels TEXT[] DEFAULT '{}',
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
auth/tables/12-user_push_tokens.sql
CREATE TABLE auth.user_push_tokens (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
token TEXT NOT NULL UNIQUE,
platform VARCHAR(20) NOT NULL, -- web, ios, android
device_info JSONB,
is_active BOOLEAN DEFAULT TRUE,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
investment/tables/08-distribution_history.sql
CREATE TABLE investment.distribution_history (
id UUID PRIMARY KEY,
account_id UUID NOT NULL REFERENCES investment.accounts(id),
product_id UUID NOT NULL REFERENCES investment.products(id),
distribution_date DATE NOT NULL,
gross_amount DECIMAL(18,2) NOT NULL,
fee_amount DECIMAL(18,2) DEFAULT 0,
net_amount DECIMAL(18,2) NOT NULL,
balance_before DECIMAL(18,2) NOT NULL,
balance_after DECIMAL(18,2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT unique_daily_distribution UNIQUE (account_id, distribution_date)
);
investment/tables/09-distribution_runs.sql
CREATE TABLE investment.distribution_runs (
id UUID PRIMARY KEY,
run_date DATE NOT NULL UNIQUE,
total_accounts INTEGER DEFAULT 0,
successful_count INTEGER DEFAULT 0,
failed_count INTEGER DEFAULT 0,
total_gross_amount DECIMAL(18,2) DEFAULT 0,
total_fee_amount DECIMAL(18,2) DEFAULT 0,
total_net_amount DECIMAL(18,2) DEFAULT 0,
started_at TIMESTAMPTZ NOT NULL,
completed_at TIMESTAMPTZ,
duration_ms INTEGER,
error_details JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
2. Backend - Firebase Client
firebase.client.ts (~300 lineas)
class FirebaseClient {
private app: admin.app.App | null = null;
initialize(): void // Init con service account
async sendToDevice(token: string, payload: NotificationPayload): Promise<boolean>
async sendToMultiple(tokens: string[], payload: NotificationPayload): Promise<SendResult>
async deactivateInvalidTokens(tokens: string[]): Promise<void>
}
export const firebaseClient = new FirebaseClient();
config/index.ts (modificado)
firebase: {
serviceAccountKey: process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '',
projectId: process.env.FIREBASE_PROJECT_ID || '',
},
webPush: {
publicKey: process.env.VAPID_PUBLIC_KEY || '',
privateKey: process.env.VAPID_PRIVATE_KEY || '',
subject: process.env.VAPID_SUBJECT || 'mailto:admin@orbiquant.io',
},
notification.controller.ts (modificado)
// Nuevos endpoints
export async function registerPushToken(req: AuthenticatedRequest, res: Response)
export async function removePushToken(req: AuthenticatedRequest, res: Response)
notification.routes.ts (modificado)
router.post('/push-token', authHandler(registerPushToken));
router.delete('/push-token', authHandler(removePushToken));
3. Tests Unitarios
notification.service.spec.ts (~450 lineas)
- Mocks: db, wsManager, nodemailer, firebaseClient
- Tests: sendNotification, getUserNotifications, markAsRead, etc.
distribution.job.spec.ts (~380 lineas)
- Mocks: db, notificationService
- Tests: run, getActiveAccounts, distributeReturns, etc.
4. Frontend
notificationStore.ts
interface NotificationState {
notifications: Notification[];
unreadCount: number;
preferences: NotificationPreferences | null;
loading: boolean;
error: string | null;
fetchNotifications: (unreadOnly?: boolean) => Promise<void>;
fetchUnreadCount: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
addNotification: (notification: Notification) => void;
initializeWebSocket: () => () => void;
}
NotificationBell.tsx
- Badge con unreadCount
- Click toggle dropdown
- useEffect para fetch inicial y WebSocket
NotificationDropdown.tsx
- Lista de ultimas 10 notificaciones
- Mark all as read button
- View all link
NotificationItem.tsx
- Iconos por tipo (success, warning, error, info)
- Tiempo relativo (hace X minutos)
- Click marca como leida y navega
NotificationsPage.tsx
- Lista completa con paginacion
- Filtros por tipo y estado
- Seccion de preferencias
MainLayout.tsx (modificado)
// Antes: Bell icon estatico
// Despues: <NotificationBell />
Dependencias Instaladas
npm install firebase-admin web-push
npm install -D @types/web-push
Estado Final
Todos los archivos creados, tests estructurados, commits pusheados.