From 3bc1cbf7a314f95784f9e968ee6aaaa41ff68b07 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Wed, 4 Feb 2026 00:16:18 -0600 Subject: [PATCH] [TASK-MASTER] feat: BE-001 biometrics service + BE-002 invoices deprecated BE-001: Biometrics - Servicio minimo de solo lectura - biometrics.service.ts con metodos de consulta - biometrics.controller.ts con 10 endpoints GET - DTOs para filtros y respuestas - README actualizado BE-002: Invoices - Marcado como deprecated - DEPRECATED.md con plan de migracion - Entities marcadas con @deprecated - Mapeo a modulo financial documentado Co-Authored-By: Claude Opus 4.5 --- src/modules/biometrics/README.md | 90 +++- .../biometrics/biometrics.controller.ts | 327 ++++++++++++++ src/modules/biometrics/biometrics.module.ts | 70 +++ src/modules/biometrics/biometrics.routes.ts | 41 ++ src/modules/biometrics/dto/biometrics.dto.ts | 116 +++++ src/modules/biometrics/dto/index.ts | 12 + src/modules/biometrics/index.ts | 46 ++ .../biometrics/services/biometrics.service.ts | 410 ++++++++++++++++++ src/modules/biometrics/services/index.ts | 9 + src/modules/invoices/DEPRECATED.md | 67 +++ .../invoices/entities/invoice-item.entity.ts | 6 + .../invoices/entities/invoice.entity.ts | 3 + .../entities/payment-allocation.entity.ts | 8 + .../invoices/entities/payment.entity.ts | 6 + 14 files changed, 1205 insertions(+), 6 deletions(-) create mode 100644 src/modules/biometrics/biometrics.controller.ts create mode 100644 src/modules/biometrics/biometrics.module.ts create mode 100644 src/modules/biometrics/biometrics.routes.ts create mode 100644 src/modules/biometrics/dto/biometrics.dto.ts create mode 100644 src/modules/biometrics/dto/index.ts create mode 100644 src/modules/biometrics/index.ts create mode 100644 src/modules/biometrics/services/biometrics.service.ts create mode 100644 src/modules/biometrics/services/index.ts create mode 100644 src/modules/invoices/DEPRECATED.md diff --git a/src/modules/biometrics/README.md b/src/modules/biometrics/README.md index d66f6fe..3c96067 100644 --- a/src/modules/biometrics/README.md +++ b/src/modules/biometrics/README.md @@ -4,6 +4,8 @@ Modulo de autenticacion biometrica para dispositivos moviles y web. Soporta fingerprint, Face ID, reconocimiento facial y escaneo de iris. Gestiona dispositivos registrados, credenciales biometricas (WebAuthn/FIDO2), sesiones y log de actividad. +**Arquitectura:** Este modulo implementa un **servicio de solo lectura**. La funcionalidad de escritura (crear, modificar, eliminar dispositivos y credenciales) se maneja desde el modulo `auth`. + ## Entidades | Entidad | Schema | Descripcion | @@ -15,16 +17,49 @@ Modulo de autenticacion biometrica para dispositivos moviles y web. Soporta fing ## Servicios -Este modulo actualmente solo define entidades. Los servicios de autenticacion biometrica deben implementarse en el modulo `auth` utilizando estas entidades. +### BiometricsService -## Endpoints +Servicio de solo lectura que proporciona metodos para consultar: -Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`: +- **Dispositivos:** `findDevicesByUserId`, `findDeviceById`, `findDeviceByUuid` +- **Credenciales:** `findCredentialsByUserId`, `findCredentialsByDeviceId`, `findCredentialById` +- **Enrollment:** `getEnrollmentStatus` +- **Sesiones:** `findSessionsByUserId`, `findActiveSessionsByUserId`, `countActiveSessions` +- **Actividad:** `findActivityByUserId`, `findActivityByDeviceId`, `getVerificationStats` + +## Endpoints (Solo lectura) + +Los endpoints exponen funcionalidad GET para consultar datos biometricos: + +| Method | Path | Descripcion | +|--------|------|-------------| +| GET | `/api/v1/biometrics/devices` | Lista dispositivos del usuario | +| GET | `/api/v1/biometrics/devices/:deviceId` | Obtiene dispositivo por ID | +| GET | `/api/v1/biometrics/credentials` | Lista credenciales biometricas | +| GET | `/api/v1/biometrics/credentials/:credentialId` | Obtiene credencial por ID | +| GET | `/api/v1/biometrics/enrollment/status` | Estado de enrollment biometrico | +| GET | `/api/v1/biometrics/sessions` | Lista sesiones del usuario | +| GET | `/api/v1/biometrics/sessions/active` | Sesiones activas | +| GET | `/api/v1/biometrics/activity` | Log de actividad | +| GET | `/api/v1/biometrics/activity/stats` | Estadisticas de verificacion | + +### Query Parameters + +| Endpoint | Parametros | +|----------|------------| +| `/devices` | `platform`, `isActive`, `biometricEnabled`, `page`, `limit` | +| `/credentials` | `biometricType`, `isActive`, `page`, `limit` | +| `/sessions` | `isActive`, `page`, `limit` | +| `/activity` | `activityType`, `activityStatus`, `startDate`, `endDate`, `page`, `limit` | +| `/activity/stats` | `startDate`, `endDate` | + +## Endpoints de Escritura (en modulo `auth`) + +La funcionalidad de escritura se expone a traves del modulo `auth`: | Method | Path | Descripcion | |--------|------|-------------| | POST | `/auth/devices/register` | Registra nuevo dispositivo | -| GET | `/auth/devices` | Lista dispositivos del usuario | | DELETE | `/auth/devices/:id` | Elimina dispositivo | | POST | `/auth/biometric/register` | Registra credencial biometrica | | POST | `/auth/biometric/authenticate` | Autentica con biometrico | @@ -32,8 +67,9 @@ Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`: ## Dependencias -- `auth` - Modulo de autenticacion principal -- WebAuthn/FIDO2 library +- `auth` - Modulo de autenticacion principal (escritura) +- `typeorm` - ORM para acceso a datos +- WebAuthn/FIDO2 library (configurado en auth) ## Configuracion @@ -76,3 +112,45 @@ Los endpoints de autenticacion biometrica se exponen a traves del modulo `auth`: - Lock temporal despues de N intentos fallidos - Registro de todas las actividades para auditoria - Soporte para multiples credenciales por dispositivo + +## Uso + +```typescript +import { BiometricsModule } from './modules/biometrics'; +import { DataSource } from 'typeorm'; + +// Inicializar modulo +const biometricsModule = new BiometricsModule({ + dataSource: myDataSource, + basePath: '/biometrics', +}); + +// Usar en Express app +app.use('/api/v1', authMiddleware, biometricsModule.router); + +// Acceder al servicio directamente +const enrollmentStatus = await biometricsModule.biometricsService.getEnrollmentStatus(userId); +``` + +## Estructura del Modulo + +``` +biometrics/ +├── biometrics.controller.ts # Controller con endpoints GET +├── biometrics.module.ts # Configuracion del modulo +├── biometrics.routes.ts # Definicion de rutas +├── index.ts # Exports publicos +├── README.md # Esta documentacion +├── dto/ +│ ├── biometrics.dto.ts # DTOs de query y response +│ └── index.ts +├── entities/ +│ ├── device.entity.ts +│ ├── biometric-credential.entity.ts +│ ├── device-session.entity.ts +│ ├── device-activity-log.entity.ts +│ └── index.ts +└── services/ + ├── biometrics.service.ts # Servicio de solo lectura + └── index.ts +``` diff --git a/src/modules/biometrics/biometrics.controller.ts b/src/modules/biometrics/biometrics.controller.ts new file mode 100644 index 0000000..0a8e185 --- /dev/null +++ b/src/modules/biometrics/biometrics.controller.ts @@ -0,0 +1,327 @@ +import { Response, NextFunction } from 'express'; +import { AuthenticatedRequest, ApiResponse, NotFoundError } from '../../shared/types/index.js'; +import { BiometricsService } from './services/biometrics.service.js'; + +/** + * BiometricsController - Controller de solo lectura para biometricos + * + * Endpoints GET only: + * - GET /api/v1/biometrics/devices - Listar dispositivos del usuario + * - GET /api/v1/biometrics/devices/:deviceId - Obtener dispositivo por ID + * - GET /api/v1/biometrics/credentials - Listar credenciales del usuario + * - GET /api/v1/biometrics/credentials/:credentialId - Obtener credencial por ID + * - GET /api/v1/biometrics/enrollment/status - Estado de enrollment + * - GET /api/v1/biometrics/sessions - Listar sesiones del usuario + * - GET /api/v1/biometrics/sessions/active - Sesiones activas + * - GET /api/v1/biometrics/activity - Log de actividad + * - GET /api/v1/biometrics/activity/stats - Estadisticas de verificacion + */ +export class BiometricsController { + constructor(private readonly biometricsService: BiometricsService) {} + + // ============================================ + // DEVICES + // ============================================ + + /** + * GET /api/v1/biometrics/devices + * Lista dispositivos del usuario autenticado + */ + async getDevices(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const userId = req.user!.userId; + const { platform, isActive, biometricEnabled, page = 1, limit = 20 } = req.query; + + const filters = { + platform: platform as any, + isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined, + biometricEnabled: + biometricEnabled === 'true' ? true : biometricEnabled === 'false' ? false : undefined, + }; + + const result = await this.biometricsService.findDevicesByUserId(userId, filters, { + page: Number(page), + limit: Number(limit), + }); + + const response: ApiResponse = { + success: true, + data: result.data, + meta: { + total: result.total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(result.total / Number(limit)), + }, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + /** + * GET /api/v1/biometrics/devices/:deviceId + * Obtiene un dispositivo por ID + */ + async getDeviceById( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + const { deviceId } = req.params; + + const device = await this.biometricsService.findDeviceById(deviceId, userId); + + if (!device) { + throw new NotFoundError('Dispositivo no encontrado'); + } + + const response: ApiResponse = { + success: true, + data: device, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + // ============================================ + // CREDENTIALS + // ============================================ + + /** + * GET /api/v1/biometrics/credentials + * Lista credenciales biometricas del usuario + */ + async getCredentials( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + const { biometricType, isActive, page = 1, limit = 20 } = req.query; + + const filters = { + biometricType: biometricType as any, + isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined, + }; + + const result = await this.biometricsService.findCredentialsByUserId(userId, filters, { + page: Number(page), + limit: Number(limit), + }); + + const response: ApiResponse = { + success: true, + data: result.data, + meta: { + total: result.total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(result.total / Number(limit)), + }, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + /** + * GET /api/v1/biometrics/credentials/:credentialId + * Obtiene una credencial por ID + */ + async getCredentialById( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + const { credentialId } = req.params; + + const credential = await this.biometricsService.findCredentialById(credentialId, userId); + + if (!credential) { + throw new NotFoundError('Credencial biometrica no encontrada'); + } + + const response: ApiResponse = { + success: true, + data: credential, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + // ============================================ + // ENROLLMENT STATUS + // ============================================ + + /** + * GET /api/v1/biometrics/enrollment/status + * Obtiene el estado de enrollment biometrico del usuario + */ + async getEnrollmentStatus( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + + const status = await this.biometricsService.getEnrollmentStatus(userId); + + const response: ApiResponse = { + success: true, + data: status, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + // ============================================ + // SESSIONS + // ============================================ + + /** + * GET /api/v1/biometrics/sessions + * Lista sesiones del usuario + */ + async getSessions(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const userId = req.user!.userId; + const { isActive, page = 1, limit = 20 } = req.query; + + const filters = { + isActive: isActive === 'true' ? true : isActive === 'false' ? false : undefined, + }; + + const result = await this.biometricsService.findSessionsByUserId(userId, filters, { + page: Number(page), + limit: Number(limit), + }); + + const response: ApiResponse = { + success: true, + data: result.data, + meta: { + total: result.total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(result.total / Number(limit)), + }, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + /** + * GET /api/v1/biometrics/sessions/active + * Lista sesiones activas del usuario + */ + async getActiveSessions( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + + const sessions = await this.biometricsService.findActiveSessionsByUserId(userId); + const count = await this.biometricsService.countActiveSessions(userId); + + const response: ApiResponse = { + success: true, + data: { + sessions, + count, + }, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + // ============================================ + // ACTIVITY LOG + // ============================================ + + /** + * GET /api/v1/biometrics/activity + * Lista actividad del usuario + */ + async getActivity(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const userId = req.user!.userId; + const { activityType, activityStatus, startDate, endDate, page = 1, limit = 50 } = req.query; + + const filters = { + activityType: activityType as any, + activityStatus: activityStatus as any, + startDate: startDate ? new Date(startDate as string) : undefined, + endDate: endDate ? new Date(endDate as string) : undefined, + }; + + const result = await this.biometricsService.findActivityByUserId(userId, filters, { + page: Number(page), + limit: Number(limit), + }); + + const response: ApiResponse = { + success: true, + data: result.data, + meta: { + total: result.total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(result.total / Number(limit)), + }, + }; + res.json(response); + } catch (error) { + next(error); + } + } + + /** + * GET /api/v1/biometrics/activity/stats + * Estadisticas de verificaciones biometricas + */ + async getVerificationStats( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const userId = req.user!.userId; + const { startDate, endDate } = req.query; + + const stats = await this.biometricsService.getVerificationStats( + userId, + startDate ? new Date(startDate as string) : undefined, + endDate ? new Date(endDate as string) : undefined + ); + + const response: ApiResponse = { + success: true, + data: stats, + }; + res.json(response); + } catch (error) { + next(error); + } + } +} diff --git a/src/modules/biometrics/biometrics.module.ts b/src/modules/biometrics/biometrics.module.ts new file mode 100644 index 0000000..d23b724 --- /dev/null +++ b/src/modules/biometrics/biometrics.module.ts @@ -0,0 +1,70 @@ +import { Router } from 'express'; +import { DataSource } from 'typeorm'; +import { BiometricsService } from './services/biometrics.service.js'; +import { BiometricsController } from './biometrics.controller.js'; +import { createBiometricsRoutes } from './biometrics.routes.js'; +import { + Device, + BiometricCredential, + DeviceSession, + DeviceActivityLog, +} from './entities/index.js'; + +export interface BiometricsModuleOptions { + dataSource: DataSource; + basePath?: string; +} + +/** + * BiometricsModule - Modulo de solo lectura para datos biometricos + * + * Este modulo proporciona endpoints GET para consultar: + * - Dispositivos registrados + * - Credenciales biometricas + * - Estado de enrollment + * - Sesiones activas + * - Log de actividad + * + * La funcionalidad de escritura se maneja desde el modulo `auth`. + */ +export class BiometricsModule { + public router: Router; + public biometricsService: BiometricsService; + private dataSource: DataSource; + private basePath: string; + + constructor(options: BiometricsModuleOptions) { + this.dataSource = options.dataSource; + this.basePath = options.basePath || '/biometrics'; + this.router = Router(); + this.initializeServices(); + this.initializeRoutes(); + } + + private initializeServices(): void { + const deviceRepository = this.dataSource.getRepository(Device); + const credentialRepository = this.dataSource.getRepository(BiometricCredential); + const sessionRepository = this.dataSource.getRepository(DeviceSession); + const activityLogRepository = this.dataSource.getRepository(DeviceActivityLog); + + this.biometricsService = new BiometricsService( + deviceRepository, + credentialRepository, + sessionRepository, + activityLogRepository + ); + } + + private initializeRoutes(): void { + const biometricsController = new BiometricsController(this.biometricsService); + const routes = createBiometricsRoutes(biometricsController); + this.router.use(this.basePath, routes); + } + + /** + * Retorna las entidades del modulo para registrar en TypeORM + */ + static getEntities(): Function[] { + return [Device, BiometricCredential, DeviceSession, DeviceActivityLog]; + } +} diff --git a/src/modules/biometrics/biometrics.routes.ts b/src/modules/biometrics/biometrics.routes.ts new file mode 100644 index 0000000..d48e4d2 --- /dev/null +++ b/src/modules/biometrics/biometrics.routes.ts @@ -0,0 +1,41 @@ +import { Router } from 'express'; +import { BiometricsController } from './biometrics.controller.js'; + +/** + * Crea las rutas del modulo biometrics + * + * Todas las rutas son GET (solo lectura): + * - GET /devices - Listar dispositivos + * - GET /devices/:deviceId - Obtener dispositivo + * - GET /credentials - Listar credenciales + * - GET /credentials/:credentialId - Obtener credencial + * - GET /enrollment/status - Estado de enrollment + * - GET /sessions - Listar sesiones + * - GET /sessions/active - Sesiones activas + * - GET /activity - Log de actividad + * - GET /activity/stats - Estadisticas + */ +export function createBiometricsRoutes(controller: BiometricsController): Router { + const router = Router(); + + // Devices + router.get('/devices', controller.getDevices.bind(controller)); + router.get('/devices/:deviceId', controller.getDeviceById.bind(controller)); + + // Credentials + router.get('/credentials', controller.getCredentials.bind(controller)); + router.get('/credentials/:credentialId', controller.getCredentialById.bind(controller)); + + // Enrollment + router.get('/enrollment/status', controller.getEnrollmentStatus.bind(controller)); + + // Sessions + router.get('/sessions', controller.getSessions.bind(controller)); + router.get('/sessions/active', controller.getActiveSessions.bind(controller)); + + // Activity + router.get('/activity', controller.getActivity.bind(controller)); + router.get('/activity/stats', controller.getVerificationStats.bind(controller)); + + return router; +} diff --git a/src/modules/biometrics/dto/biometrics.dto.ts b/src/modules/biometrics/dto/biometrics.dto.ts new file mode 100644 index 0000000..79e99d7 --- /dev/null +++ b/src/modules/biometrics/dto/biometrics.dto.ts @@ -0,0 +1,116 @@ +import { BiometricType, DevicePlatform, ActivityStatus, ActivityType } from '../entities'; + +// ============================================ +// QUERY INTERFACES (Solo lectura) +// ============================================ + +export interface GetDeviceQueryDto { + tenantId?: string; +} + +export interface GetDevicesQueryDto { + platform?: DevicePlatform; + isActive?: string; + biometricEnabled?: string; + page?: number; + limit?: number; +} + +export interface GetCredentialsQueryDto { + biometricType?: BiometricType; + isActive?: string; + page?: number; + limit?: number; +} + +export interface GetActivityLogQueryDto { + activityType?: ActivityType; + activityStatus?: ActivityStatus; + startDate?: string; + endDate?: string; + page?: number; + limit?: number; +} + +export interface GetSessionsQueryDto { + isActive?: string; + page?: number; + limit?: number; +} + +// ============================================ +// RESPONSE INTERFACES +// ============================================ + +export interface DeviceResponseDto { + id: string; + userId: string; + tenantId: string; + deviceUuid: string; + deviceName: string; + deviceModel: string; + deviceBrand: string; + platform: DevicePlatform; + platformVersion: string; + appVersion: string; + isActive: boolean; + isTrusted: boolean; + trustLevel: number; + biometricEnabled: boolean; + biometricType: BiometricType; + lastSeenAt: Date; + createdAt: Date; +} + +export interface BiometricCredentialResponseDto { + id: string; + deviceId: string; + userId: string; + biometricType: BiometricType; + credentialName: string; + isPrimary: boolean; + isActive: boolean; + lastUsedAt: Date; + useCount: number; + createdAt: Date; +} + +export interface DeviceSessionResponseDto { + id: string; + deviceId: string; + userId: string; + tenantId: string; + authMethod: string; + issuedAt: Date; + expiresAt: Date; + isActive: boolean; + ipAddress: string; + createdAt: Date; +} + +export interface DeviceActivityLogResponseDto { + id: string; + deviceId: string; + userId: string; + activityType: ActivityType; + activityStatus: ActivityStatus; + details: Record; + ipAddress: string; + latitude: number; + longitude: number; + createdAt: Date; +} + +export interface EnrollmentStatusResponseDto { + userId: string; + hasEnrollment: boolean; + totalCredentials: number; + activeCredentials: number; + primaryCredential: BiometricCredentialResponseDto | null; + biometricTypes: BiometricType[]; + devices: { + deviceId: string; + deviceName: string; + credentialsCount: number; + }[]; +} diff --git a/src/modules/biometrics/dto/index.ts b/src/modules/biometrics/dto/index.ts new file mode 100644 index 0000000..a37d673 --- /dev/null +++ b/src/modules/biometrics/dto/index.ts @@ -0,0 +1,12 @@ +export { + GetDeviceQueryDto, + GetDevicesQueryDto, + GetCredentialsQueryDto, + GetActivityLogQueryDto, + GetSessionsQueryDto, + DeviceResponseDto, + BiometricCredentialResponseDto, + DeviceSessionResponseDto, + DeviceActivityLogResponseDto, + EnrollmentStatusResponseDto, +} from './biometrics.dto'; diff --git a/src/modules/biometrics/index.ts b/src/modules/biometrics/index.ts new file mode 100644 index 0000000..6854391 --- /dev/null +++ b/src/modules/biometrics/index.ts @@ -0,0 +1,46 @@ +// Module +export { BiometricsModule, BiometricsModuleOptions } from './biometrics.module'; + +// Entities +export { + Device, + DevicePlatform, + BiometricType, + BiometricCredential, + DeviceSession, + AuthMethod, + DeviceActivityLog, + ActivityType, + ActivityStatus, +} from './entities'; + +// Services +export { + BiometricsService, + DeviceFilters, + CredentialFilters, + ActivityLogFilters, + SessionFilters, + PaginationOptions, + EnrollmentStatus, +} from './services'; + +// DTOs +export { + GetDeviceQueryDto, + GetDevicesQueryDto, + GetCredentialsQueryDto, + GetActivityLogQueryDto, + GetSessionsQueryDto, + DeviceResponseDto, + BiometricCredentialResponseDto, + DeviceSessionResponseDto, + DeviceActivityLogResponseDto, + EnrollmentStatusResponseDto, +} from './dto'; + +// Controller +export { BiometricsController } from './biometrics.controller'; + +// Routes +export { createBiometricsRoutes } from './biometrics.routes'; diff --git a/src/modules/biometrics/services/biometrics.service.ts b/src/modules/biometrics/services/biometrics.service.ts new file mode 100644 index 0000000..3481b31 --- /dev/null +++ b/src/modules/biometrics/services/biometrics.service.ts @@ -0,0 +1,410 @@ +import { Repository, FindOptionsWhere, Between, LessThanOrEqual, MoreThanOrEqual, In } from 'typeorm'; +import { + Device, + BiometricCredential, + DeviceSession, + DeviceActivityLog, + BiometricType, + DevicePlatform, + ActivityType, + ActivityStatus, +} from '../entities'; + +export interface DeviceFilters { + platform?: DevicePlatform; + isActive?: boolean; + biometricEnabled?: boolean; +} + +export interface CredentialFilters { + biometricType?: BiometricType; + isActive?: boolean; +} + +export interface ActivityLogFilters { + activityType?: ActivityType; + activityStatus?: ActivityStatus; + startDate?: Date; + endDate?: Date; +} + +export interface SessionFilters { + isActive?: boolean; +} + +export interface PaginationOptions { + page?: number; + limit?: number; +} + +export interface EnrollmentStatus { + userId: string; + hasEnrollment: boolean; + totalCredentials: number; + activeCredentials: number; + primaryCredential: BiometricCredential | null; + biometricTypes: BiometricType[]; + devices: { + deviceId: string; + deviceName: string; + credentialsCount: number; + }[]; +} + +/** + * BiometricsService - Servicio de solo lectura para datos biometricos + * + * Este servicio proporciona metodos de consulta para dispositivos, + * credenciales biometricas, sesiones y logs de actividad. + * + * La funcionalidad de escritura (crear, modificar, eliminar) se maneja + * desde los modulos `auth` y `devices`. + */ +export class BiometricsService { + constructor( + private readonly deviceRepository: Repository, + private readonly credentialRepository: Repository, + private readonly sessionRepository: Repository, + private readonly activityLogRepository: Repository + ) {} + + // ============================================ + // DEVICES (Solo lectura) + // ============================================ + + /** + * Obtiene dispositivos de un usuario + */ + async findDevicesByUserId( + userId: string, + filters: DeviceFilters = {}, + pagination: PaginationOptions = {} + ): Promise<{ data: Device[]; total: number }> { + const { page = 1, limit = 20 } = pagination; + const where: FindOptionsWhere = { userId }; + + if (filters.platform) where.platform = filters.platform; + if (filters.isActive !== undefined) where.isActive = filters.isActive; + if (filters.biometricEnabled !== undefined) where.biometricEnabled = filters.biometricEnabled; + + const [data, total] = await this.deviceRepository.findAndCount({ + where, + order: { lastSeenAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total }; + } + + /** + * Obtiene un dispositivo por ID + */ + async findDeviceById(deviceId: string, userId?: string): Promise { + const where: FindOptionsWhere = { id: deviceId }; + if (userId) where.userId = userId; + + return this.deviceRepository.findOne({ + where, + relations: ['biometricCredentials', 'sessions'], + }); + } + + /** + * Obtiene un dispositivo por UUID + */ + async findDeviceByUuid(deviceUuid: string, userId: string): Promise { + return this.deviceRepository.findOne({ + where: { deviceUuid, userId }, + relations: ['biometricCredentials'], + }); + } + + // ============================================ + // BIOMETRIC CREDENTIALS (Solo lectura) + // ============================================ + + /** + * Obtiene credenciales biometricas de un usuario + */ + async findCredentialsByUserId( + userId: string, + filters: CredentialFilters = {}, + pagination: PaginationOptions = {} + ): Promise<{ data: BiometricCredential[]; total: number }> { + const { page = 1, limit = 20 } = pagination; + const where: FindOptionsWhere = { userId }; + + if (filters.biometricType) where.biometricType = filters.biometricType; + if (filters.isActive !== undefined) where.isActive = filters.isActive; + + const [data, total] = await this.credentialRepository.findAndCount({ + where, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + relations: ['device'], + }); + + return { data, total }; + } + + /** + * Obtiene credenciales de un dispositivo + */ + async findCredentialsByDeviceId( + deviceId: string, + userId?: string + ): Promise { + const where: FindOptionsWhere = { deviceId }; + if (userId) where.userId = userId; + + return this.credentialRepository.find({ + where, + order: { isPrimary: 'DESC', createdAt: 'DESC' }, + }); + } + + /** + * Obtiene una credencial por ID + */ + async findCredentialById( + credentialId: string, + userId?: string + ): Promise { + const where: FindOptionsWhere = { id: credentialId }; + if (userId) where.userId = userId; + + return this.credentialRepository.findOne({ + where, + relations: ['device'], + }); + } + + // ============================================ + // ENROLLMENT STATUS (Solo lectura) + // ============================================ + + /** + * Verifica el estado de enrollment biometrico de un usuario + */ + async getEnrollmentStatus(userId: string): Promise { + const credentials = await this.credentialRepository.find({ + where: { userId }, + relations: ['device'], + }); + + const devices = await this.deviceRepository.find({ + where: { userId, biometricEnabled: true }, + }); + + const activeCredentials = credentials.filter((c) => c.isActive); + const primaryCredential = credentials.find((c) => c.isPrimary && c.isActive) || null; + + // Obtener tipos biometricos unicos + const biometricTypes = [...new Set(activeCredentials.map((c) => c.biometricType))]; + + // Agrupar credenciales por dispositivo + const deviceCredentialsMap = new Map(); + for (const credential of activeCredentials) { + const device = credential.device; + if (device) { + const existing = deviceCredentialsMap.get(device.id); + if (existing) { + existing.count++; + } else { + deviceCredentialsMap.set(device.id, { + name: device.deviceName || device.deviceModel || 'Dispositivo desconocido', + count: 1, + }); + } + } + } + + const devicesList = Array.from(deviceCredentialsMap.entries()).map(([id, data]) => ({ + deviceId: id, + deviceName: data.name, + credentialsCount: data.count, + })); + + return { + userId, + hasEnrollment: activeCredentials.length > 0, + totalCredentials: credentials.length, + activeCredentials: activeCredentials.length, + primaryCredential, + biometricTypes, + devices: devicesList, + }; + } + + // ============================================ + // DEVICE SESSIONS (Solo lectura) + // ============================================ + + /** + * Lista sesiones de un usuario + */ + async findSessionsByUserId( + userId: string, + filters: SessionFilters = {}, + pagination: PaginationOptions = {} + ): Promise<{ data: DeviceSession[]; total: number }> { + const { page = 1, limit = 20 } = pagination; + const where: FindOptionsWhere = { userId }; + + if (filters.isActive !== undefined) where.isActive = filters.isActive; + + const [data, total] = await this.sessionRepository.findAndCount({ + where, + order: { issuedAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + relations: ['device'], + }); + + return { data, total }; + } + + /** + * Obtiene sesiones activas de un usuario + */ + async findActiveSessionsByUserId(userId: string): Promise { + const now = new Date(); + return this.sessionRepository.find({ + where: { + userId, + isActive: true, + expiresAt: MoreThanOrEqual(now), + }, + order: { issuedAt: 'DESC' }, + relations: ['device'], + }); + } + + /** + * Cuenta sesiones activas de un usuario + */ + async countActiveSessions(userId: string): Promise { + const now = new Date(); + return this.sessionRepository.count({ + where: { + userId, + isActive: true, + expiresAt: MoreThanOrEqual(now), + }, + }); + } + + // ============================================ + // ACTIVITY LOG (Solo lectura) + // ============================================ + + /** + * Lista actividad de un usuario + */ + async findActivityByUserId( + userId: string, + filters: ActivityLogFilters = {}, + pagination: PaginationOptions = {} + ): Promise<{ data: DeviceActivityLog[]; total: number }> { + const { page = 1, limit = 50 } = pagination; + const where: FindOptionsWhere = { userId }; + + if (filters.activityType) where.activityType = filters.activityType; + if (filters.activityStatus) where.activityStatus = filters.activityStatus; + + if (filters.startDate && filters.endDate) { + where.createdAt = Between(filters.startDate, filters.endDate); + } else if (filters.startDate) { + where.createdAt = MoreThanOrEqual(filters.startDate); + } else if (filters.endDate) { + where.createdAt = LessThanOrEqual(filters.endDate); + } + + const [data, total] = await this.activityLogRepository.findAndCount({ + where, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total }; + } + + /** + * Lista actividad de un dispositivo + */ + async findActivityByDeviceId( + deviceId: string, + filters: ActivityLogFilters = {}, + pagination: PaginationOptions = {} + ): Promise<{ data: DeviceActivityLog[]; total: number }> { + const { page = 1, limit = 50 } = pagination; + const where: FindOptionsWhere = { deviceId }; + + if (filters.activityType) where.activityType = filters.activityType; + if (filters.activityStatus) where.activityStatus = filters.activityStatus; + + if (filters.startDate && filters.endDate) { + where.createdAt = Between(filters.startDate, filters.endDate); + } else if (filters.startDate) { + where.createdAt = MoreThanOrEqual(filters.startDate); + } else if (filters.endDate) { + where.createdAt = LessThanOrEqual(filters.endDate); + } + + const [data, total] = await this.activityLogRepository.findAndCount({ + where, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { data, total }; + } + + /** + * Obtiene estadisticas de verificaciones biometricas + */ + async getVerificationStats( + userId: string, + startDate?: Date, + endDate?: Date + ): Promise<{ + total: number; + successful: number; + failed: number; + blocked: number; + successRate: number; + }> { + const where: FindOptionsWhere = { + userId, + activityType: 'biometric_auth', + }; + + if (startDate && endDate) { + where.createdAt = Between(startDate, endDate); + } else if (startDate) { + where.createdAt = MoreThanOrEqual(startDate); + } else if (endDate) { + where.createdAt = LessThanOrEqual(endDate); + } + + const logs = await this.activityLogRepository.find({ where }); + + const total = logs.length; + const successful = logs.filter((l) => l.activityStatus === 'success').length; + const failed = logs.filter((l) => l.activityStatus === 'failed').length; + const blocked = logs.filter((l) => l.activityStatus === 'blocked').length; + const successRate = total > 0 ? (successful / total) * 100 : 0; + + return { + total, + successful, + failed, + blocked, + successRate: Math.round(successRate * 100) / 100, + }; + } +} diff --git a/src/modules/biometrics/services/index.ts b/src/modules/biometrics/services/index.ts new file mode 100644 index 0000000..87c23fb --- /dev/null +++ b/src/modules/biometrics/services/index.ts @@ -0,0 +1,9 @@ +export { + BiometricsService, + DeviceFilters, + CredentialFilters, + ActivityLogFilters, + SessionFilters, + PaginationOptions, + EnrollmentStatus, +} from './biometrics.service'; diff --git a/src/modules/invoices/DEPRECATED.md b/src/modules/invoices/DEPRECATED.md new file mode 100644 index 0000000..ed621b8 --- /dev/null +++ b/src/modules/invoices/DEPRECATED.md @@ -0,0 +1,67 @@ +# MODULO DEPRECATED + +Este modulo esta deprecated desde 2026-02-03. + +## Razon + +La funcionalidad de facturacion esta siendo consolidada en el modulo `financial`. +El modulo `invoices` (schema `billing`) fue disenado para facturacion operativa, +pero para mantener una arquitectura limpia, toda la logica de facturas debe +residir en un unico modulo. + +## Mapeo de Entidades + +| Invoices (billing) | Financial (financial) | Notas | +|------------------------|------------------------------|------------------------------------------| +| Invoice | financial/invoice.entity.ts | financial tiene integracion con journals | +| InvoiceItem | financial/invoice-line.entity.ts | Renombrado a InvoiceLine | +| Payment | financial/payment.entity.ts | financial tiene PaymentType enum | +| PaymentAllocation | (sin equivalente directo) | Usar payment_invoices table | + +## Diferencias Clave + +### Invoices (deprecated) +- Schema: `billing` +- Enfoque: Facturacion operativa, CFDI Mexico, SaaS billing +- Sin integracion contable directa + +### Financial (activo) +- Schema: `financial` +- Enfoque: Contabilidad completa con journal entries +- Integracion con plan de cuentas +- Soporte para asientos automaticos + +## Migracion + +Para migrar codigo existente: + +```typescript +// ANTES (deprecated) +import { Invoice, Payment } from '@modules/invoices'; + +// DESPUES (recomendado) +import { Invoice, Payment } from '@modules/financial/entities'; +``` + +## Uso Actual + +Este modulo aun tiene las siguientes dependencias activas: + +1. `app.integration.ts` - Importa InvoicesModule y entities +2. `app.ts` - Importa invoicesRoutes +3. `billing-usage/entities/invoice.entity.ts` - Referencia en comentario deprecated + +**IMPORTANTE:** Estas referencias deben actualizarse antes de eliminar este modulo. + +## Plan de Deprecation + +1. **Fase 1 (Actual):** Marcar como deprecated, documentar +2. **Fase 2:** Migrar endpoints a financial module +3. **Fase 3:** Actualizar todas las referencias +4. **Fase 4:** Eliminar modulo invoices + +## Contacto + +Para dudas sobre la migracion, consultar la documentacion en: +- `orchestration/tareas/2026-02-03/` - Tarea de consolidacion +- `docs/40-estandares/` - Estandares de arquitectura diff --git a/src/modules/invoices/entities/invoice-item.entity.ts b/src/modules/invoices/entities/invoice-item.entity.ts index 38dc4cd..93d216f 100644 --- a/src/modules/invoices/entities/invoice-item.entity.ts +++ b/src/modules/invoices/entities/invoice-item.entity.ts @@ -1,6 +1,12 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm'; import { Invoice } from './invoice.entity'; +/** + * Invoice Item Entity + * + * @deprecated Since 2026-02-03. Use financial/invoice-line.entity.ts instead. + * @see InvoiceLine from '@modules/financial/entities' + */ @Entity({ name: 'invoice_items', schema: 'billing' }) export class InvoiceItem { @PrimaryGeneratedColumn('uuid') diff --git a/src/modules/invoices/entities/invoice.entity.ts b/src/modules/invoices/entities/invoice.entity.ts index 139e766..7d76636 100644 --- a/src/modules/invoices/entities/invoice.entity.ts +++ b/src/modules/invoices/entities/invoice.entity.ts @@ -19,6 +19,9 @@ import { InvoiceItem } from './invoice-item.entity'; * Context discriminator: * - 'commercial': Sales/purchase invoices (salesOrderId, purchaseOrderId, partnerId) * - 'saas': SaaS subscription invoices (subscriptionId, periodStart, periodEnd) + * + * @deprecated Since 2026-02-03. Use financial/invoice.entity.ts instead. + * @see Invoice from '@modules/financial/entities' */ export type InvoiceType = 'sale' | 'purchase' | 'credit_note' | 'debit_note'; diff --git a/src/modules/invoices/entities/payment-allocation.entity.ts b/src/modules/invoices/entities/payment-allocation.entity.ts index 9917804..18a3235 100644 --- a/src/modules/invoices/entities/payment-allocation.entity.ts +++ b/src/modules/invoices/entities/payment-allocation.entity.ts @@ -2,6 +2,14 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, ManyTo import { Payment } from './payment.entity'; import { Invoice } from './invoice.entity'; +/** + * Payment Allocation Entity + * + * Maps payments to invoices for partial payment tracking. + * + * @deprecated Since 2026-02-03. Use payment_invoices relationship in financial module. + * @see Payment from '@modules/financial/entities' + */ @Entity({ name: 'payment_allocations', schema: 'billing' }) export class PaymentAllocation { @PrimaryGeneratedColumn('uuid') diff --git a/src/modules/invoices/entities/payment.entity.ts b/src/modules/invoices/entities/payment.entity.ts index 83ca2fa..9e3863b 100644 --- a/src/modules/invoices/entities/payment.entity.ts +++ b/src/modules/invoices/entities/payment.entity.ts @@ -1,5 +1,11 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index } from 'typeorm'; +/** + * Payment Entity + * + * @deprecated Since 2026-02-03. Use financial/payment.entity.ts instead. + * @see Payment from '@modules/financial/entities' + */ @Entity({ name: 'payments', schema: 'billing' }) export class Payment { @PrimaryGeneratedColumn('uuid')