[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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-04 00:16:18 -06:00
parent 0f7feff3f8
commit 3bc1cbf7a3
14 changed files with 1205 additions and 6 deletions

View File

@ -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
```

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
}

View File

@ -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];
}
}

View File

@ -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;
}

View File

@ -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<string, unknown>;
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;
}[];
}

View File

@ -0,0 +1,12 @@
export {
GetDeviceQueryDto,
GetDevicesQueryDto,
GetCredentialsQueryDto,
GetActivityLogQueryDto,
GetSessionsQueryDto,
DeviceResponseDto,
BiometricCredentialResponseDto,
DeviceSessionResponseDto,
DeviceActivityLogResponseDto,
EnrollmentStatusResponseDto,
} from './biometrics.dto';

View File

@ -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';

View File

@ -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<Device>,
private readonly credentialRepository: Repository<BiometricCredential>,
private readonly sessionRepository: Repository<DeviceSession>,
private readonly activityLogRepository: Repository<DeviceActivityLog>
) {}
// ============================================
// 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<Device> = { 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<Device | null> {
const where: FindOptionsWhere<Device> = { 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<Device | null> {
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<BiometricCredential> = { 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<BiometricCredential[]> {
const where: FindOptionsWhere<BiometricCredential> = { 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<BiometricCredential | null> {
const where: FindOptionsWhere<BiometricCredential> = { 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<EnrollmentStatus> {
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<string, { name: string; count: number }>();
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<DeviceSession> = { 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<DeviceSession[]> {
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<number> {
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<DeviceActivityLog> = { 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<DeviceActivityLog> = { 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<DeviceActivityLog> = {
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,
};
}
}

View File

@ -0,0 +1,9 @@
export {
BiometricsService,
DeviceFilters,
CredentialFilters,
ActivityLogFilters,
SessionFilters,
PaginationOptions,
EnrollmentStatus,
} from './biometrics.service';

View File

@ -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

View File

@ -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')

View File

@ -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';

View File

@ -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')

View File

@ -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')