diff --git a/src/modules/budgets/controllers/presupuesto.controller.ts b/src/modules/budgets/controllers/presupuesto.controller.ts index d56c1a0..1688d1b 100644 --- a/src/modules/budgets/controllers/presupuesto.controller.ts +++ b/src/modules/budgets/controllers/presupuesto.controller.ts @@ -11,12 +11,17 @@ import { DataSource } from 'typeorm'; import { PresupuestoService, CreatePresupuestoDto, AddPartidaDto, UpdatePartidaDto } from '../services/presupuesto.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Presupuesto } from '../entities/presupuesto.entity'; -import { PresupuestoPartida } from '../entities/presupuesto-partida.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * ServiceContext - Contexto local para operaciones de servicio + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de presupuestos @@ -25,14 +30,12 @@ export function createPresupuestoController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const presupuestoRepository = dataSource.getRepository(Presupuesto); - const partidaRepository = dataSource.getRepository(PresupuestoPartida); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const presupuestoService = new PresupuestoService(presupuestoRepository, partidaRepository); + const presupuestoService = new PresupuestoService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -73,7 +76,12 @@ export function createPresupuestoController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/budgets/services/presupuesto.service.ts b/src/modules/budgets/services/presupuesto.service.ts index d3879be..f52f7a9 100644 --- a/src/modules/budgets/services/presupuesto.service.ts +++ b/src/modules/budgets/services/presupuesto.service.ts @@ -7,11 +7,23 @@ * @module Budgets */ -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { Presupuesto } from '../entities/presupuesto.entity'; import { PresupuestoPartida } from '../entities/presupuesto-partida.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreatePresupuestoDto { code: string; name: string; @@ -34,12 +46,101 @@ export interface UpdatePartidaDto { sequence?: number; } -export class PresupuestoService extends BaseService { - constructor( - repository: Repository, - private readonly partidaRepository: Repository - ) { - super(repository); +export class PresupuestoService { + private repository: Repository; + private partidaRepository: Repository; + + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(Presupuesto); + this.partidaRepository = dataSource.getRepository(PresupuestoPartida); + } + + /** + * Busca un presupuesto por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + }); + } + + /** + * Busca todos los presupuestos con paginación + */ + async findAll( + ctx: ServiceContext, + page = 1, + limit = 20 + ): Promise> { + const skip = (page - 1) * limit; + + const [data, total] = await this.repository.findAndCount({ + where: { + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + order: { createdAt: 'DESC' }, + skip, + take: limit, + }); + + return { + data, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }; + } + + /** + * Crea un nuevo registro + */ + async create(ctx: ServiceContext, data: Partial): Promise { + const entity = this.repository.create({ + ...data, + tenantId: ctx.tenantId, + createdById: ctx.userId, + }); + return this.repository.save(entity); + } + + /** + * Actualiza un registro + */ + async update( + ctx: ServiceContext, + id: string, + data: Partial + ): Promise { + const entity = await this.findById(ctx, id); + if (!entity) { + return null; + } + + Object.assign(entity, data); + entity.updatedById = ctx.userId || null; + + return this.repository.save(entity); + } + + /** + * Soft delete de un registro + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const entity = await this.findById(ctx, id); + if (!entity) { + return false; + } + + entity.deletedAt = new Date(); + entity.deletedById = ctx.userId || null; + await this.repository.save(entity); + return true; } /** @@ -66,11 +167,27 @@ export class PresupuestoService extends BaseService { page = 1, limit = 20 ): Promise> { - return this.findAll(ctx, { + const skip = (page - 1) * limit; + + const [data, total] = await this.repository.findAndCount({ + where: { + tenantId: ctx.tenantId, + fraccionamientoId, + isActive: true, + deletedAt: IsNull(), + }, + order: { createdAt: 'DESC' }, + skip, + take: limit, + }); + + return { + data, + total, page, limit, - where: { fraccionamientoId, isActive: true } as any, - }); + totalPages: Math.ceil(total / limit), + }; } /** @@ -84,8 +201,8 @@ export class PresupuestoService extends BaseService { where: { id, tenantId: ctx.tenantId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, relations: ['partidas', 'partidas.concepto'], }); } @@ -131,8 +248,8 @@ export class PresupuestoService extends BaseService { where: { id: partidaId, tenantId: ctx.tenantId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, }); if (!partida) { @@ -158,7 +275,7 @@ export class PresupuestoService extends BaseService { where: { id: partidaId, tenantId: ctx.tenantId, - } as any, + }, }); if (!partida) { @@ -191,7 +308,7 @@ export class PresupuestoService extends BaseService { const total = parseFloat(result?.total || '0'); await this.repository.update( - { id: presupuestoId }, + { id: presupuestoId } as any, { totalAmount: total, updatedById: ctx.userId } ); } @@ -210,7 +327,7 @@ export class PresupuestoService extends BaseService { // Desactivar versión anterior await this.repository.update( - { id: presupuestoId }, + { id: presupuestoId } as any, { isActive: false, updatedById: ctx.userId } ); diff --git a/src/modules/estimates/controllers/anticipo.controller.ts b/src/modules/estimates/controllers/anticipo.controller.ts index b599d83..9258e0a 100644 --- a/src/modules/estimates/controllers/anticipo.controller.ts +++ b/src/modules/estimates/controllers/anticipo.controller.ts @@ -11,23 +11,25 @@ import { DataSource } from 'typeorm'; import { AnticipoService, CreateAnticipoDto, AnticipoFilters } from '../services/anticipo.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Anticipo } from '../entities/anticipo.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createAnticipoController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const anticipoRepository = dataSource.getRepository(Anticipo); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const anticipoService = new AnticipoService(anticipoRepository); + const anticipoService = new AnticipoService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -69,7 +71,12 @@ export function createAnticipoController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -120,7 +127,12 @@ export function createAnticipoController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/estimates/controllers/estimacion.controller.ts b/src/modules/estimates/controllers/estimacion.controller.ts index dd59cf7..ee4f099 100644 --- a/src/modules/estimates/controllers/estimacion.controller.ts +++ b/src/modules/estimates/controllers/estimacion.controller.ts @@ -18,14 +18,17 @@ import { } from '../services/estimacion.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Estimacion } from '../entities/estimacion.entity'; -import { EstimacionConcepto } from '../entities/estimacion-concepto.entity'; -import { Generador } from '../entities/generador.entity'; -import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Local interface for service context + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de estimaciones @@ -34,22 +37,12 @@ export function createEstimacionController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const estimacionRepository = dataSource.getRepository(Estimacion); - const conceptoRepository = dataSource.getRepository(EstimacionConcepto); - const generadorRepository = dataSource.getRepository(Generador); - const workflowRepository = dataSource.getRepository(EstimacionWorkflow); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const estimacionService = new EstimacionService( - estimacionRepository, - conceptoRepository, - generadorRepository, - workflowRepository, - dataSource - ); + const estimacionService = new EstimacionService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -92,7 +85,12 @@ export function createEstimacionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/estimates/controllers/fondo-garantia.controller.ts b/src/modules/estimates/controllers/fondo-garantia.controller.ts index b0f98f4..9a81e92 100644 --- a/src/modules/estimates/controllers/fondo-garantia.controller.ts +++ b/src/modules/estimates/controllers/fondo-garantia.controller.ts @@ -11,23 +11,28 @@ import { DataSource } from 'typeorm'; import { FondoGarantiaService, CreateFondoGarantiaDto, ReleaseFondoDto } from '../services/fondo-garantia.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { FondoGarantia } from '../entities/fondo-garantia.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Local interface for service context + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createFondoGarantiaController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const fondoGarantiaRepository = dataSource.getRepository(FondoGarantia); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const fondoGarantiaService = new FondoGarantiaService(fondoGarantiaRepository); + const fondoGarantiaService = new FondoGarantiaService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -60,7 +65,12 @@ export function createFondoGarantiaController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/estimates/controllers/retencion.controller.ts b/src/modules/estimates/controllers/retencion.controller.ts index b82e076..0e0a8ea 100644 --- a/src/modules/estimates/controllers/retencion.controller.ts +++ b/src/modules/estimates/controllers/retencion.controller.ts @@ -11,23 +11,28 @@ import { DataSource } from 'typeorm'; import { RetencionService, CreateRetencionDto, ReleaseRetencionDto, RetencionFilters } from '../services/retencion.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Retencion } from '../entities/retencion.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Local interface for service context + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createRetencionController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const retencionRepository = dataSource.getRepository(Retencion); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const retencionService = new RetencionService(retencionRepository); + const retencionService = new RetencionService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -70,7 +75,12 @@ export function createRetencionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/estimates/services/anticipo.service.ts b/src/modules/estimates/services/anticipo.service.ts index 691eb0d..f1c395d 100644 --- a/src/modules/estimates/services/anticipo.service.ts +++ b/src/modules/estimates/services/anticipo.service.ts @@ -6,10 +6,22 @@ * @module Estimates */ -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { Anticipo, AdvanceType } from '../entities/anticipo.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateAnticipoDto { contratoId: string; advanceType: AdvanceType; @@ -45,9 +57,24 @@ export interface AnticipoStats { }[]; } -export class AnticipoService extends BaseService { - constructor(repository: Repository) { - super(repository); +export class AnticipoService { + private repository: Repository; + + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(Anticipo); + } + + /** + * Busca un anticipo por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + }); } /** @@ -61,13 +88,13 @@ export class AnticipoService extends BaseService { ): Promise> { const qb = this.repository .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.contrato_id = :contratoId', { contratoId }) - .andWhere('a.deleted_at IS NULL'); + .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId }) + .andWhere('a.contratoId = :contratoId', { contratoId }) + .andWhere('a.deletedAt IS NULL'); const skip = (page - 1) * limit; - qb.orderBy('a.advance_date', 'DESC') - .addOrderBy('a.advance_number', 'DESC') + qb.orderBy('a.advanceDate', 'DESC') + .addOrderBy('a.advanceNumber', 'DESC') .skip(skip) .take(limit); @@ -75,12 +102,10 @@ export class AnticipoService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -97,11 +122,57 @@ export class AnticipoService extends BaseService { throw new Error('El monto neto no coincide con el cálculo (bruto - impuestos)'); } - return this.create(ctx, { - ...dto, + const anticipo = this.repository.create({ + tenantId: ctx.tenantId, + contratoId: dto.contratoId, + advanceType: dto.advanceType, + advanceNumber: dto.advanceNumber, + advanceDate: dto.advanceDate, + grossAmount: dto.grossAmount, + taxAmount: dto.taxAmount, + netAmount: dto.netAmount, + amortizationPercentage: dto.amortizationPercentage, amortizedAmount: 0, isFullyAmortized: false, + notes: dto.notes, + createdBy: ctx.userId, }); + + return this.repository.save(anticipo); + } + + /** + * Actualiza un anticipo + */ + async update( + ctx: ServiceContext, + id: string, + data: Partial + ): Promise { + const anticipo = await this.findById(ctx, id); + if (!anticipo) { + return null; + } + + Object.assign(anticipo, data); + anticipo.updatedById = ctx.userId; + + return this.repository.save(anticipo); + } + + /** + * Soft delete de un anticipo + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const anticipo = await this.findById(ctx, id); + if (!anticipo) { + return false; + } + + anticipo.deletedAt = new Date(); + anticipo.deletedById = ctx.userId; + await this.repository.save(anticipo); + return true; } /** @@ -122,23 +193,18 @@ export class AnticipoService extends BaseService { throw new Error('El anticipo ya está aprobado'); } - const updateData: Partial = { - approvedAt: new Date(), - approvedById, - }; + anticipo.approvedAt = new Date(); + anticipo.approvedById = approvedById; if (notes) { - updateData.notes = anticipo.notes + anticipo.notes = anticipo.notes ? `${anticipo.notes}\n\nAprobación: ${notes}` : `Aprobación: ${notes}`; } - const updated = await this.update(ctx, id, updateData); - if (!updated) { - throw new Error('Error al aprobar anticipo'); - } + anticipo.updatedById = ctx.userId; - return updated; + return this.repository.save(anticipo); } /** @@ -163,16 +229,11 @@ export class AnticipoService extends BaseService { throw new Error('El anticipo ya está marcado como pagado'); } - const updated = await this.update(ctx, id, { - paidAt: paidAt || new Date(), - paymentReference, - }); + anticipo.paidAt = paidAt || new Date(); + anticipo.paymentReference = paymentReference; + anticipo.updatedById = ctx.userId; - if (!updated) { - throw new Error('Error al marcar anticipo como pagado'); - } - - return updated; + return this.repository.save(anticipo); } /** @@ -184,28 +245,28 @@ export class AnticipoService extends BaseService { ): Promise { const qb = this.repository .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL'); + .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId }) + .andWhere('a.deletedAt IS NULL'); if (filters?.contratoId) { - qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); + qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId }); } if (filters?.advanceType) { - qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); + qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType }); } if (filters?.isFullyAmortized !== undefined) { - qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { + qb.andWhere('a.isFullyAmortized = :isFullyAmortized', { isFullyAmortized: filters.isFullyAmortized, }); } if (filters?.startDate) { - qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); + qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate }); } if (filters?.endDate) { - qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); + qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate }); } if (filters?.approvedById) { - qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById }); + qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById }); } const anticipos = await qb.getMany(); @@ -222,7 +283,7 @@ export class AnticipoService extends BaseService { const typeStats = new Map(); - anticipos.forEach((anticipo) => { + anticipos.forEach((anticipo: Anticipo) => { const netAmount = Number(anticipo.netAmount) || 0; const amortizedAmount = Number(anticipo.amortizedAmount) || 0; @@ -267,33 +328,33 @@ export class AnticipoService extends BaseService { ): Promise> { const qb = this.repository .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL'); + .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId }) + .andWhere('a.deletedAt IS NULL'); if (filters.contratoId) { - qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); + qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId }); } if (filters.advanceType) { - qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); + qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType }); } if (filters.isFullyAmortized !== undefined) { - qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { + qb.andWhere('a.isFullyAmortized = :isFullyAmortized', { isFullyAmortized: filters.isFullyAmortized, }); } if (filters.startDate) { - qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); + qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate }); } if (filters.endDate) { - qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); + qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate }); } if (filters.approvedById) { - qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById }); + qb.andWhere('a.approvedById = :approvedById', { approvedById: filters.approvedById }); } const skip = (page - 1) * limit; - qb.orderBy('a.advance_date', 'DESC') - .addOrderBy('a.advance_number', 'DESC') + qb.orderBy('a.advanceDate', 'DESC') + .addOrderBy('a.advanceNumber', 'DESC') .skip(skip) .take(limit); @@ -301,12 +362,10 @@ export class AnticipoService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -337,15 +396,10 @@ export class AnticipoService extends BaseService { const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01; - const updated = await this.update(ctx, id, { - amortizedAmount: newAmortizedAmount, - isFullyAmortized, - }); + anticipo.amortizedAmount = newAmortizedAmount; + anticipo.isFullyAmortized = isFullyAmortized; + anticipo.updatedById = ctx.userId; - if (!updated) { - throw new Error('Error al actualizar amortización'); - } - - return updated; + return this.repository.save(anticipo); } } diff --git a/src/modules/estimates/services/estimacion.service.ts b/src/modules/estimates/services/estimacion.service.ts index 0512294..704f35d 100644 --- a/src/modules/estimates/services/estimacion.service.ts +++ b/src/modules/estimates/services/estimacion.service.ts @@ -1,19 +1,31 @@ /** - * EstimacionService - Gestión de Estimaciones de Obra + * EstimacionService - Gestion de Estimaciones de Obra * - * Gestiona estimaciones periódicas con workflow de aprobación. - * Incluye cálculo de anticipos, retenciones e IVA. + * Gestiona estimaciones periodicas con workflow de aprobacion. + * Incluye calculo de anticipos, retenciones e IVA. * * @module Estimates */ -import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { Estimacion, EstimateStatus } from '../entities/estimacion.entity'; import { EstimacionConcepto } from '../entities/estimacion-concepto.entity'; import { Generador } from '../entities/generador.entity'; import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateEstimacionDto { contratoId: string; fraccionamientoId: string; @@ -52,19 +64,74 @@ export interface EstimacionFilters { periodTo?: Date; } -export class EstimacionService extends BaseService { - constructor( - repository: Repository, - private readonly conceptoRepository: Repository, - private readonly generadorRepository: Repository, - private readonly workflowRepository: Repository, - private readonly dataSource: DataSource - ) { - super(repository); +interface ContractEstimateSummary { + totalEstimates: number; + totalApproved: number; + totalPaid: number; +} + +export class EstimacionService { + private repository: Repository; + private conceptoRepository: Repository; + private generadorRepository: Repository; + private workflowRepository: Repository; + + constructor(private readonly dataSource: DataSource) { + this.repository = dataSource.getRepository(Estimacion); + this.conceptoRepository = dataSource.getRepository(EstimacionConcepto); + this.generadorRepository = dataSource.getRepository(Generador); + this.workflowRepository = dataSource.getRepository(EstimacionWorkflow); } /** - * Crear nueva estimación + * Busca una estimacion por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + }); + } + + /** + * Actualiza una estimacion + */ + async update( + ctx: ServiceContext, + id: string, + data: Partial + ): Promise { + const estimacion = await this.findById(ctx, id); + if (!estimacion) { + return null; + } + + Object.assign(estimacion, data); + estimacion.updatedById = ctx.userId; + + return this.repository.save(estimacion); + } + + /** + * Soft delete de una estimacion + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const estimacion = await this.findById(ctx, id); + if (!estimacion) { + return false; + } + + estimacion.deletedAt = new Date(); + estimacion.deletedById = ctx.userId; + await this.repository.save(estimacion); + return true; + } + + /** + * Crear nueva estimacion */ async createEstimacion( ctx: ServiceContext, @@ -73,21 +140,29 @@ export class EstimacionService extends BaseService { const sequenceNumber = await this.getNextSequenceNumber(ctx, data.contratoId); const estimateNumber = await this.generateEstimateNumber(ctx, data.contratoId, sequenceNumber); - const estimacion = await this.create(ctx, { - ...data, + const estimacion = this.repository.create({ + tenantId: ctx.tenantId, + contratoId: data.contratoId, + fraccionamientoId: data.fraccionamientoId, + periodStart: data.periodStart, + periodEnd: data.periodEnd, + notes: data.notes, estimateNumber, sequenceNumber, - status: 'draft', + status: 'draft' as EstimateStatus, + createdById: ctx.userId, }); - // Registrar en workflow - await this.addWorkflowEntry(ctx, estimacion.id, null, 'draft', 'create', 'Estimación creada'); + const savedEstimacion = await this.repository.save(estimacion); - return estimacion; + // Registrar en workflow + await this.addWorkflowEntry(ctx, savedEstimacion.id, null, 'draft', 'create', 'Estimacion creada'); + + return savedEstimacion; } /** - * Obtener siguiente número de secuencia + * Obtener siguiente numero de secuencia */ private async getNextSequenceNumber( ctx: ServiceContext, @@ -104,7 +179,7 @@ export class EstimacionService extends BaseService { } /** - * Generar número de estimación + * Generar numero de estimacion */ private async generateEstimateNumber( _ctx: ServiceContext, @@ -124,11 +199,24 @@ export class EstimacionService extends BaseService { page = 1, limit = 20 ): Promise> { - return this.findAll(ctx, { + const qb = this.repository + .createQueryBuilder('e') + .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId }) + .andWhere('e.contrato_id = :contratoId', { contratoId }) + .andWhere('e.deleted_at IS NULL'); + + const skip = (page - 1) * limit; + qb.orderBy('e.sequence_number', 'DESC').skip(skip).take(limit); + + const [data, total] = await qb.getManyAndCount(); + + return { + data, + total, page, limit, - where: { contratoId } as any, - }); + totalPages: Math.ceil(total / limit), + }; } /** @@ -168,25 +256,23 @@ export class EstimacionService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } /** - * Obtener estimación con detalles completos + * Obtener estimacion con detalles completos */ async findWithDetails(ctx: ServiceContext, id: string): Promise { return this.repository.findOne({ where: { id, tenantId: ctx.tenantId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, relations: [ 'conceptos', 'conceptos.concepto', @@ -199,7 +285,7 @@ export class EstimacionService extends BaseService { } /** - * Agregar concepto a estimación + * Agregar concepto a estimacion */ async addConcepto( ctx: ServiceContext, @@ -231,7 +317,7 @@ export class EstimacionService extends BaseService { } /** - * Agregar generador a concepto de estimación + * Agregar generador a concepto de estimacion */ async addGenerador( ctx: ServiceContext, @@ -239,7 +325,7 @@ export class EstimacionService extends BaseService { data: AddGeneradorDto ): Promise { const concepto = await this.conceptoRepository.findOne({ - where: { id: estimacionConceptoId, tenantId: ctx.tenantId } as any, + where: { id: estimacionConceptoId, tenantId: ctx.tenantId }, relations: ['estimacion'], }); @@ -268,15 +354,15 @@ export class EstimacionService extends BaseService { } /** - * Recalcular totales de estimación + * Recalcular totales de estimacion */ async recalculateTotals(_ctx: ServiceContext, estimacionId: string): Promise { - // Ejecutar función de PostgreSQL + // Ejecutar funcion de PostgreSQL await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]); } /** - * Cambiar estado de estimación + * Cambiar estado de estimacion */ async changeStatus( ctx: ServiceContext, @@ -362,28 +448,28 @@ export class EstimacionService extends BaseService { } /** - * Enviar estimación para revisión + * Enviar estimacion para revision */ async submit(ctx: ServiceContext, estimacionId: string): Promise { - return this.changeStatus(ctx, estimacionId, 'submitted', 'submit', 'Enviada para revisión'); + return this.changeStatus(ctx, estimacionId, 'submitted', 'submit', 'Enviada para revision'); } /** - * Revisar estimación + * Revisar estimacion */ async review(ctx: ServiceContext, estimacionId: string): Promise { - return this.changeStatus(ctx, estimacionId, 'reviewed', 'review', 'Revisión completada'); + return this.changeStatus(ctx, estimacionId, 'reviewed', 'review', 'Revision completada'); } /** - * Aprobar estimación + * Aprobar estimacion */ async approve(ctx: ServiceContext, estimacionId: string): Promise { return this.changeStatus(ctx, estimacionId, 'approved', 'approve', 'Aprobada'); } /** - * Rechazar estimación + * Rechazar estimacion */ async reject( ctx: ServiceContext, @@ -416,9 +502,3 @@ export class EstimacionService extends BaseService { }; } } - -interface ContractEstimateSummary { - totalEstimates: number; - totalApproved: number; - totalPaid: number; -} diff --git a/src/modules/estimates/services/fondo-garantia.service.ts b/src/modules/estimates/services/fondo-garantia.service.ts index 87ca697..3ffd08f 100644 --- a/src/modules/estimates/services/fondo-garantia.service.ts +++ b/src/modules/estimates/services/fondo-garantia.service.ts @@ -6,10 +6,22 @@ * @module Estimates */ -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { FondoGarantia } from '../entities/fondo-garantia.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateFondoGarantiaDto { contratoId: string; accumulatedAmount?: number; @@ -39,9 +51,103 @@ export interface FondoGarantiaStats { fondosFullyReleased: number; } -export class FondoGarantiaService extends BaseService { - constructor(repository: Repository) { - super(repository); +export class FondoGarantiaService { + private repository: Repository; + + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(FondoGarantia); + } + + /** + * Busca un fondo de garantía por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + }); + } + + /** + * Busca todos los fondos de garantía con paginación + */ + async findAll( + ctx: ServiceContext, + page = 1, + limit = 20 + ): Promise> { + const skip = (page - 1) * limit; + + const [data, total] = await this.repository.findAndCount({ + where: { + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + order: { createdAt: 'DESC' }, + skip, + take: limit, + }); + + return { + data, + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }; + } + + /** + * Crea un nuevo fondo de garantía + */ + async create( + ctx: ServiceContext, + data: Partial + ): Promise { + const fondo = this.repository.create({ + ...data, + tenantId: ctx.tenantId, + createdById: ctx.userId ?? null, + }); + + return this.repository.save(fondo); + } + + /** + * Actualiza un fondo de garantía + */ + async update( + ctx: ServiceContext, + id: string, + data: Partial + ): Promise { + const fondo = await this.findById(ctx, id); + if (!fondo) { + return null; + } + + Object.assign(fondo, data); + fondo.updatedById = ctx.userId ?? null; + + return this.repository.save(fondo); + } + + /** + * Soft delete de un fondo de garantía + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const fondo = await this.findById(ctx, id); + if (!fondo) { + return false; + } + + fondo.deletedAt = new Date(); + fondo.deletedById = ctx.userId ?? null; + await this.repository.save(fondo); + return true; } /** @@ -55,8 +161,8 @@ export class FondoGarantiaService extends BaseService { where: { tenantId: ctx.tenantId, contratoId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, }); } @@ -183,8 +289,8 @@ export class FondoGarantiaService extends BaseService { const fondos = await this.repository.find({ where: { tenantId: ctx.tenantId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, }); const stats: FondoGarantiaStats = { @@ -260,16 +366,14 @@ export class FondoGarantiaService extends BaseService { }); } + const finalTotal = filteredData.length !== data.length ? filteredData.length : total; + return { data: filteredData, - meta: { - total: filteredData.length !== data.length ? filteredData.length : total, - page, - limit, - totalPages: Math.ceil( - (filteredData.length !== data.length ? filteredData.length : total) / limit - ), - }, + total: finalTotal, + page, + limit, + totalPages: Math.ceil(finalTotal / limit), }; } } diff --git a/src/modules/estimates/services/retencion.service.ts b/src/modules/estimates/services/retencion.service.ts index cb4ec6d..7703f48 100644 --- a/src/modules/estimates/services/retencion.service.ts +++ b/src/modules/estimates/services/retencion.service.ts @@ -6,10 +6,22 @@ * @module Estimates */ -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { Retencion, RetentionType } from '../entities/retencion.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateRetencionDto { estimacionId: string; retentionType: RetentionType; @@ -34,9 +46,24 @@ export interface RetencionFilters { dateTo?: Date; } -export class RetencionService extends BaseService { - constructor(repository: Repository) { - super(repository); +export class RetencionService { + private repository: Repository; + + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(Retencion); + } + + /** + * Busca una retención por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: IsNull(), + }, + }); } /** @@ -50,8 +77,8 @@ export class RetencionService extends BaseService { where: { tenantId: ctx.tenantId, estimacionId, - deletedAt: null, - } as any, + deletedAt: IsNull(), + }, order: { createdAt: 'DESC' }, }); } @@ -63,11 +90,55 @@ export class RetencionService extends BaseService { ctx: ServiceContext, data: CreateRetencionDto ): Promise { - return this.create(ctx, { - ...data, + const retencion = this.repository.create({ + tenantId: ctx.tenantId, + estimacionId: data.estimacionId, + retentionType: data.retentionType, + description: data.description, + percentage: data.percentage ?? null, + amount: data.amount, + releaseDate: data.releaseDate ?? null, + notes: data.notes ?? null, releasedAmount: null, releasedAt: null, + createdById: ctx.userId ?? null, }); + + return this.repository.save(retencion); + } + + /** + * Actualiza una retención + */ + async update( + ctx: ServiceContext, + id: string, + data: Partial + ): Promise { + const retencion = await this.findById(ctx, id); + if (!retencion) { + return null; + } + + Object.assign(retencion, data); + retencion.updatedById = ctx.userId ?? null; + + return this.repository.save(retencion); + } + + /** + * Soft delete de una retención + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const retencion = await this.findById(ctx, id); + if (!retencion) { + return false; + } + + retencion.deletedAt = new Date(); + retencion.deletedById = ctx.userId ?? null; + await this.repository.save(retencion); + return true; } /** @@ -115,12 +186,10 @@ export class RetencionService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hr/controllers/employee.controller.ts b/src/modules/hr/controllers/employee.controller.ts index 9612614..1075475 100644 --- a/src/modules/hr/controllers/employee.controller.ts +++ b/src/modules/hr/controllers/employee.controller.ts @@ -17,12 +17,17 @@ import { } from '../services/employee.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Employee } from '../entities/employee.entity'; -import { EmployeeFraccionamiento } from '../entities/employee-fraccionamiento.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Local service context interface + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de empleados @@ -31,14 +36,12 @@ export function createEmployeeController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const employeeRepository = dataSource.getRepository(Employee); - const asignacionRepository = dataSource.getRepository(EmployeeFraccionamiento); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const employeeService = new EmployeeService(employeeRepository, asignacionRepository); + const employeeService = new EmployeeService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -81,7 +84,12 @@ export function createEmployeeController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hr/controllers/puesto.controller.ts b/src/modules/hr/controllers/puesto.controller.ts index 461c6d2..338b58f 100644 --- a/src/modules/hr/controllers/puesto.controller.ts +++ b/src/modules/hr/controllers/puesto.controller.ts @@ -11,11 +11,17 @@ import { DataSource } from 'typeorm'; import { PuestoService, CreatePuestoDto, UpdatePuestoDto, PuestoFilters } from '../services/puesto.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Puesto } from '../entities/puesto.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Local service context interface + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de puestos @@ -24,13 +30,12 @@ export function createPuestoController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const puestoRepository = dataSource.getRepository(Puesto); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const puestoService = new PuestoService(puestoRepository); + const puestoService = new PuestoService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -71,7 +76,12 @@ export function createPuestoController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hr/services/employee.service.ts b/src/modules/hr/services/employee.service.ts index d52cf2e..82a03d2 100644 --- a/src/modules/hr/services/employee.service.ts +++ b/src/modules/hr/services/employee.service.ts @@ -129,12 +129,10 @@ export class EmployeeService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hr/services/puesto.service.ts b/src/modules/hr/services/puesto.service.ts index a58a7d1..d180a81 100644 --- a/src/modules/hr/services/puesto.service.ts +++ b/src/modules/hr/services/puesto.service.ts @@ -71,12 +71,10 @@ export class PuestoService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/controllers/ambiental.controller.ts b/src/modules/hse/controllers/ambiental.controller.ts index 6364c8e..eba70fe 100644 --- a/src/modules/hse/controllers/ambiental.controller.ts +++ b/src/modules/hse/controllers/ambiental.controller.ts @@ -35,7 +35,14 @@ import { QuejaAmbiental } from '../entities/queja-ambiental.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de gestión ambiental @@ -103,7 +110,12 @@ export function createAmbientalController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -163,7 +175,12 @@ export function createAmbientalController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -284,7 +301,12 @@ export function createAmbientalController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -417,7 +439,12 @@ export function createAmbientalController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -500,7 +527,12 @@ export function createAmbientalController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/capacitacion.controller.ts b/src/modules/hse/controllers/capacitacion.controller.ts index 9d50660..500408a 100644 --- a/src/modules/hse/controllers/capacitacion.controller.ts +++ b/src/modules/hse/controllers/capacitacion.controller.ts @@ -20,7 +20,14 @@ import { Capacitacion } from '../entities/capacitacion.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de capacitaciones @@ -76,7 +83,12 @@ export function createCapacitacionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/epp.controller.ts b/src/modules/hse/controllers/epp.controller.ts index d6bdce2..1036e5c 100644 --- a/src/modules/hse/controllers/epp.controller.ts +++ b/src/modules/hse/controllers/epp.controller.ts @@ -29,7 +29,14 @@ import { EppMovimiento } from '../entities/epp-movimiento.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de EPP @@ -101,7 +108,12 @@ export function createEppController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -228,7 +240,12 @@ export function createEppController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/incidente.controller.ts b/src/modules/hse/controllers/incidente.controller.ts index 3e97906..30cf77a 100644 --- a/src/modules/hse/controllers/incidente.controller.ts +++ b/src/modules/hse/controllers/incidente.controller.ts @@ -26,7 +26,14 @@ import { IncidenteAccion } from '../entities/incidente-accion.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de incidentes @@ -91,7 +98,12 @@ export function createIncidenteController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/indicador.controller.ts b/src/modules/hse/controllers/indicador.controller.ts index 05c14ea..d4b5248 100644 --- a/src/modules/hse/controllers/indicador.controller.ts +++ b/src/modules/hse/controllers/indicador.controller.ts @@ -25,7 +25,14 @@ import { AlertaIndicador } from '../entities/alerta-indicador.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de indicadores @@ -97,7 +104,12 @@ export function createIndicadorController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -258,7 +270,12 @@ export function createIndicadorController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -341,7 +358,12 @@ export function createIndicadorController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/inspeccion.controller.ts b/src/modules/hse/controllers/inspeccion.controller.ts index 243e271..060d313 100644 --- a/src/modules/hse/controllers/inspeccion.controller.ts +++ b/src/modules/hse/controllers/inspeccion.controller.ts @@ -25,7 +25,14 @@ import { TipoInspeccion } from '../entities/tipo-inspeccion.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de inspecciones @@ -107,7 +114,12 @@ export function createInspeccionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -277,7 +289,12 @@ export function createInspeccionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/hse/controllers/permiso-trabajo.controller.ts b/src/modules/hse/controllers/permiso-trabajo.controller.ts index 384e3c6..a5c13a1 100644 --- a/src/modules/hse/controllers/permiso-trabajo.controller.ts +++ b/src/modules/hse/controllers/permiso-trabajo.controller.ts @@ -26,7 +26,14 @@ import { PermisoDocumento } from '../entities/permiso-documento.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de permisos de trabajo diff --git a/src/modules/hse/controllers/stps.controller.ts b/src/modules/hse/controllers/stps.controller.ts index 4990305..974d197 100644 --- a/src/modules/hse/controllers/stps.controller.ts +++ b/src/modules/hse/controllers/stps.controller.ts @@ -37,7 +37,14 @@ import { Auditoria, ResultadoAuditoria } from '../entities/auditoria.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de STPS diff --git a/src/modules/hse/services/ambiental.service.ts b/src/modules/hse/services/ambiental.service.ts index afa5498..c817fc0 100644 --- a/src/modules/hse/services/ambiental.service.ts +++ b/src/modules/hse/services/ambiental.service.ts @@ -177,7 +177,10 @@ export class AmbientalService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -242,7 +245,10 @@ export class AmbientalService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -341,7 +347,10 @@ export class AmbientalService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -441,7 +450,10 @@ export class AmbientalService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -526,7 +538,10 @@ export class AmbientalService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/capacitacion.service.ts b/src/modules/hse/services/capacitacion.service.ts index 62740cc..7983820 100644 --- a/src/modules/hse/services/capacitacion.service.ts +++ b/src/modules/hse/services/capacitacion.service.ts @@ -77,12 +77,10 @@ export class CapacitacionService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/epp.service.ts b/src/modules/hse/services/epp.service.ts index d5567ef..810fd0b 100644 --- a/src/modules/hse/services/epp.service.ts +++ b/src/modules/hse/services/epp.service.ts @@ -131,7 +131,10 @@ export class EppService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -249,7 +252,10 @@ export class EppService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/incidente.service.ts b/src/modules/hse/services/incidente.service.ts index 030a263..4aa839b 100644 --- a/src/modules/hse/services/incidente.service.ts +++ b/src/modules/hse/services/incidente.service.ts @@ -143,12 +143,10 @@ export class IncidenteService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/indicador.service.ts b/src/modules/hse/services/indicador.service.ts index bc2c97d..93cfd7d 100644 --- a/src/modules/hse/services/indicador.service.ts +++ b/src/modules/hse/services/indicador.service.ts @@ -91,7 +91,10 @@ export class IndicadorService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -187,7 +190,10 @@ export class IndicadorService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -248,7 +254,10 @@ export class IndicadorService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/inspeccion.service.ts b/src/modules/hse/services/inspeccion.service.ts index 9f3d190..1a3ceff 100644 --- a/src/modules/hse/services/inspeccion.service.ts +++ b/src/modules/hse/services/inspeccion.service.ts @@ -143,12 +143,10 @@ export class InspeccionService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -258,12 +256,10 @@ export class InspeccionService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/permiso-trabajo.service.ts b/src/modules/hse/services/permiso-trabajo.service.ts index 05a538e..7bc1fb1 100644 --- a/src/modules/hse/services/permiso-trabajo.service.ts +++ b/src/modules/hse/services/permiso-trabajo.service.ts @@ -133,7 +133,10 @@ export class PermisoTrabajoService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/hse/services/stps.service.ts b/src/modules/hse/services/stps.service.ts index 06cec5c..0c9b5ec 100644 --- a/src/modules/hse/services/stps.service.ts +++ b/src/modules/hse/services/stps.service.ts @@ -195,7 +195,10 @@ export class StpsService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -559,7 +562,10 @@ export class StpsService { return { data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/infonavit/controllers/asignacion.controller.ts b/src/modules/infonavit/controllers/asignacion.controller.ts index 57a5117..c31d18c 100644 --- a/src/modules/infonavit/controllers/asignacion.controller.ts +++ b/src/modules/infonavit/controllers/asignacion.controller.ts @@ -18,7 +18,14 @@ import { AuthService } from '../../auth/services/auth.service'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createAsignacionController(dataSource: DataSource): Router { const router = Router(); @@ -70,7 +77,16 @@ export function createAsignacionController(dataSource: DataSource): Router { const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); + res.status(200).json({ + success: true, + data: result.data, + pagination: { + total: result.meta.total, + page: result.meta.page, + limit: result.meta.limit, + totalPages: result.meta.totalPages, + }, + }); } catch (error) { next(error); } diff --git a/src/modules/infonavit/controllers/derechohabiente.controller.ts b/src/modules/infonavit/controllers/derechohabiente.controller.ts index 3ea777a..5fd1dde 100644 --- a/src/modules/infonavit/controllers/derechohabiente.controller.ts +++ b/src/modules/infonavit/controllers/derechohabiente.controller.ts @@ -17,7 +17,14 @@ import { AuthService } from '../../auth/services/auth.service'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createDerechohabienteController(dataSource: DataSource): Router { const router = Router(); @@ -68,7 +75,16 @@ export function createDerechohabienteController(dataSource: DataSource): Router const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); + res.status(200).json({ + success: true, + data: result.data, + pagination: { + total: result.meta.total, + page: result.meta.page, + limit: result.meta.limit, + totalPages: result.meta.totalPages, + }, + }); } catch (error) { next(error); } diff --git a/src/modules/inventory/controllers/consumo-obra.controller.ts b/src/modules/inventory/controllers/consumo-obra.controller.ts index 91f5394..ab90d22 100644 --- a/src/modules/inventory/controllers/consumo-obra.controller.ts +++ b/src/modules/inventory/controllers/consumo-obra.controller.ts @@ -19,7 +19,11 @@ import { ConsumoObra } from '../entities/consumo-obra.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de consumos @@ -78,7 +82,12 @@ export function createConsumoObraController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/inventory/controllers/requisicion.controller.ts b/src/modules/inventory/controllers/requisicion.controller.ts index e83d8ed..73fcbfd 100644 --- a/src/modules/inventory/controllers/requisicion.controller.ts +++ b/src/modules/inventory/controllers/requisicion.controller.ts @@ -22,7 +22,11 @@ import { RequisicionLinea } from '../entities/requisicion-linea.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de requisiciones @@ -81,7 +85,12 @@ export function createRequisicionController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/inventory/services/consumo-obra.service.ts b/src/modules/inventory/services/consumo-obra.service.ts index ca1aedc..68312e5 100644 --- a/src/modules/inventory/services/consumo-obra.service.ts +++ b/src/modules/inventory/services/consumo-obra.service.ts @@ -8,7 +8,19 @@ import { Repository, FindOptionsWhere } from 'typeorm'; import { ConsumoObra } from '../entities/consumo-obra.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} export interface CreateConsumoDto { fraccionamientoId: string; @@ -94,12 +106,10 @@ export class ConsumoObraService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/inventory/services/requisicion.service.ts b/src/modules/inventory/services/requisicion.service.ts index bc8a312..6c0ee3b 100644 --- a/src/modules/inventory/services/requisicion.service.ts +++ b/src/modules/inventory/services/requisicion.service.ts @@ -10,7 +10,19 @@ import { Repository, FindOptionsWhere } from 'typeorm'; import { RequisicionObra, RequisitionStatus } from '../entities/requisicion-obra.entity'; import { RequisicionLinea } from '../entities/requisicion-linea.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} export interface CreateRequisicionDto { fraccionamientoId: string; @@ -98,12 +110,10 @@ export class RequisicionService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/progress/controllers/avance-obra.controller.ts b/src/modules/progress/controllers/avance-obra.controller.ts index 602b8b8..4ab12a3 100644 --- a/src/modules/progress/controllers/avance-obra.controller.ts +++ b/src/modules/progress/controllers/avance-obra.controller.ts @@ -17,7 +17,14 @@ import { FotoAvance } from '../entities/foto-avance.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de avances de obra @@ -77,7 +84,12 @@ export function createAvanceObraController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.meta.total, + page: result.meta.page, + limit: result.meta.limit, + totalPages: result.meta.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/progress/controllers/bitacora-obra.controller.ts b/src/modules/progress/controllers/bitacora-obra.controller.ts index 520e4f5..4756e79 100644 --- a/src/modules/progress/controllers/bitacora-obra.controller.ts +++ b/src/modules/progress/controllers/bitacora-obra.controller.ts @@ -15,7 +15,14 @@ import { BitacoraObra } from '../entities/bitacora-obra.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +/** + * Service context for multi-tenant operations + */ +interface ServiceContext { + tenantId: string; + userId?: string; +} /** * Crear router de bitácora de obra @@ -77,7 +84,12 @@ export function createBitacoraObraController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.meta.total, + page: result.meta.page, + limit: result.meta.limit, + totalPages: result.meta.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/purchase/controllers/comparativo.controller.ts b/src/modules/purchase/controllers/comparativo.controller.ts index 7062c29..e5daf5e 100644 --- a/src/modules/purchase/controllers/comparativo.controller.ts +++ b/src/modules/purchase/controllers/comparativo.controller.ts @@ -18,7 +18,11 @@ import { AuthService } from '../../auth/services/auth.service'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createComparativoController(dataSource: DataSource): Router { const router = Router(); @@ -77,7 +81,16 @@ export function createComparativoController(dataSource: DataSource): Router { const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); + res.status(200).json({ + success: true, + data: result.data, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, + }); } catch (error) { next(error); } diff --git a/src/modules/purchase/services/comparativo.service.ts b/src/modules/purchase/services/comparativo.service.ts index f43b4fa..af189b0 100644 --- a/src/modules/purchase/services/comparativo.service.ts +++ b/src/modules/purchase/services/comparativo.service.ts @@ -10,7 +10,19 @@ import { Repository, FindOptionsWhere } from 'typeorm'; import { ComparativoCotizaciones, ComparativoStatus } from '../entities/comparativo-cotizaciones.entity'; import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity'; import { ComparativoProducto } from '../entities/comparativo-producto.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} export interface CreateComparativoDto { requisicionId?: string; @@ -99,12 +111,10 @@ export class ComparativoService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/reports/controllers/dashboard.controller.ts b/src/modules/reports/controllers/dashboard.controller.ts index 1710a6d..00975b3 100644 --- a/src/modules/reports/controllers/dashboard.controller.ts +++ b/src/modules/reports/controllers/dashboard.controller.ts @@ -18,23 +18,25 @@ import { } from '../services/dashboard.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Dashboard } from '../entities/dashboard.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createDashboardController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const dashboardRepository = dataSource.getRepository(Dashboard); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const dashboardService = new DashboardService(dashboardRepository, dataSource); + const dashboardService = new DashboardService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -76,7 +78,12 @@ export function createDashboardController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/reports/controllers/kpi.controller.ts b/src/modules/reports/controllers/kpi.controller.ts index 0fb4d4f..8696cd5 100644 --- a/src/modules/reports/controllers/kpi.controller.ts +++ b/src/modules/reports/controllers/kpi.controller.ts @@ -11,23 +11,25 @@ import { DataSource } from 'typeorm'; import { KpiService, CreateKpiSnapshotDto, KpiFilters } from '../services/kpi.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { KpiSnapshot } from '../entities/kpi-snapshot.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createKpiController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const kpiRepository = dataSource.getRepository(KpiSnapshot); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const kpiService = new KpiService(kpiRepository); + const kpiService = new KpiService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -69,7 +71,12 @@ export function createKpiController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/reports/controllers/report.controller.ts b/src/modules/reports/controllers/report.controller.ts index 79fdd10..5b3fad9 100644 --- a/src/modules/reports/controllers/report.controller.ts +++ b/src/modules/reports/controllers/report.controller.ts @@ -11,23 +11,25 @@ import { DataSource } from 'typeorm'; import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; -import { Report } from '../entities/report.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; + +interface ServiceContext { + tenantId: string; + userId?: string; +} export function createReportController(dataSource: DataSource): Router { const router = Router(); // Repositorios - const reportRepository = dataSource.getRepository(Report); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios - const reportService = new ReportService(reportRepository, dataSource); + const reportService = new ReportService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -67,7 +69,12 @@ export function createReportController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); @@ -279,7 +286,12 @@ export function createReportController(dataSource: DataSource): Router { res.status(200).json({ success: true, data: result.data, - pagination: result.meta, + pagination: { + total: result.total, + page: result.page, + limit: result.limit, + totalPages: result.totalPages, + }, }); } catch (error) { next(error); diff --git a/src/modules/reports/services/dashboard.service.ts b/src/modules/reports/services/dashboard.service.ts index 6103fab..82d0d54 100644 --- a/src/modules/reports/services/dashboard.service.ts +++ b/src/modules/reports/services/dashboard.service.ts @@ -7,10 +7,22 @@ */ import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; import { Dashboard, DashboardType, DashboardVisibility } from '../entities/dashboard.entity'; import { DashboardWidget, WidgetType, DataSourceType } from '../entities/dashboard-widget.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateDashboardDto { code: string; name: string; @@ -66,17 +78,61 @@ export interface DashboardFilters { search?: string; } -export class DashboardService extends BaseService { +export class DashboardService { + private repository: Repository; private widgetRepository: Repository; - constructor( - repository: Repository, - dataSource: DataSource - ) { - super(repository); + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(Dashboard); this.widgetRepository = dataSource.getRepository(DashboardWidget); } + /** + * Buscar por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + deletedAt: null, + } as any, + }); + } + + /** + * Crear entidad + */ + async create(ctx: ServiceContext, data: Partial): Promise { + const entity = this.repository.create({ + ...data, + tenantId: ctx.tenantId, + }); + return this.repository.save(entity); + } + + /** + * Actualizar entidad + */ + async update(ctx: ServiceContext, id: string, data: Partial): Promise { + await this.repository.update( + { id, tenantId: ctx.tenantId, deletedAt: null } as any, + data as any + ); + return this.findById(ctx, id); + } + + /** + * Soft delete + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const result = await this.repository.update( + { id, tenantId: ctx.tenantId, deletedAt: null } as any, + { deletedAt: new Date() } as any + ); + return (result.affected ?? 0) > 0; + } + /** * Buscar dashboards con filtros */ diff --git a/src/modules/reports/services/kpi.service.ts b/src/modules/reports/services/kpi.service.ts index 8df4937..e668776 100644 --- a/src/modules/reports/services/kpi.service.ts +++ b/src/modules/reports/services/kpi.service.ts @@ -6,10 +6,22 @@ * @module Reports */ -import { Repository, LessThanOrEqual } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { Repository, DataSource, LessThanOrEqual } from 'typeorm'; import { KpiSnapshot, KpiCategory, KpiPeriodType, TrendDirection } from '../entities/kpi-snapshot.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateKpiSnapshotDto { kpiCode: string; kpiName: string; @@ -66,9 +78,34 @@ export interface KpiSummary { }[]; } -export class KpiService extends BaseService { - constructor(repository: Repository) { - super(repository); +export class KpiService { + private repository: Repository; + + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(KpiSnapshot); + } + + /** + * Buscar por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { + id, + tenantId: ctx.tenantId, + } as any, + }); + } + + /** + * Crear entidad + */ + async create(ctx: ServiceContext, data: Partial): Promise { + const entity = this.repository.create({ + ...data, + tenantId: ctx.tenantId, + }); + return this.repository.save(entity); } /** @@ -110,12 +147,10 @@ export class KpiService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/modules/reports/services/report.service.ts b/src/modules/reports/services/report.service.ts index 12cc465..fa3c1d2 100644 --- a/src/modules/reports/services/report.service.ts +++ b/src/modules/reports/services/report.service.ts @@ -6,11 +6,23 @@ * @module Reports */ -import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; +import { DataSource, Repository, IsNull } from 'typeorm'; import { Report, ReportType, ReportFormat, ReportFrequency } from '../entities/report.entity'; import { ReportExecution, ExecutionStatus } from '../entities/report-execution.entity'; +interface ServiceContext { + tenantId: string; + userId?: string; +} + +interface PaginatedResult { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface CreateReportDto { code: string; name: string; @@ -49,17 +61,57 @@ export interface ExecuteReportDto { distribute?: boolean; } -export class ReportService extends BaseService { +export class ReportService { + private repository: Repository; private executionRepository: Repository; - constructor( - repository: Repository, - dataSource: DataSource - ) { - super(repository); + constructor(dataSource: DataSource) { + this.repository = dataSource.getRepository(Report); this.executionRepository = dataSource.getRepository(ReportExecution); } + /** + * Buscar reporte por ID + */ + async findById(ctx: ServiceContext, id: string): Promise { + return this.repository.findOne({ + where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() }, + }); + } + + /** + * Crear reporte + */ + async create(ctx: ServiceContext, data: CreateReportDto & { isActive: boolean; isSystem: boolean; executionCount: number }): Promise { + const report = this.repository.create({ + ...data, + tenantId: ctx.tenantId, + }); + return this.repository.save(report); + } + + /** + * Actualizar reporte + */ + async update(ctx: ServiceContext, id: string, data: Partial): Promise { + await this.repository.update( + { id, tenantId: ctx.tenantId, deletedAt: IsNull() }, + data + ); + return this.findById(ctx, id); + } + + /** + * Soft delete reporte + */ + async softDelete(ctx: ServiceContext, id: string): Promise { + const result = await this.repository.update( + { id, tenantId: ctx.tenantId, deletedAt: IsNull() }, + { deletedAt: new Date() } + ); + return result.affected! > 0; + } + /** * Buscar reportes con filtros */ @@ -96,12 +148,10 @@ export class ReportService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } @@ -288,12 +338,10 @@ export class ReportService extends BaseService { return { data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; } diff --git a/src/shared/services/base.service.ts b/src/shared/services/base.service.ts index 6886014..ce8abc6 100644 --- a/src/shared/services/base.service.ts +++ b/src/shared/services/base.service.ts @@ -1,6 +1,15 @@ -import { query, queryOne, getClient, PoolClient } from '../../config/database.js'; -import { NotFoundError, ValidationError } from '../errors/index.js'; -import { PaginationMeta } from '../types/index.js'; +/** + * BaseService - Base service class (deprecated) + * + * NOTE: This base class is deprecated. New services should be standalone + * and use TypeORM Repository pattern directly with DataSource injection. + * + * The ServiceContext and PaginatedResult interfaces are still exported + * for backwards compatibility with existing code. + * + * @deprecated Use standalone service pattern with TypeORM Repository instead + * @module Shared + */ /** * Contexto de servicio para multi-tenant @@ -36,7 +45,6 @@ export interface BasePaginationFilters { * Opciones para construcción de queries */ export interface QueryOptions { - client?: PoolClient; includeDeleted?: boolean; } @@ -55,28 +63,41 @@ export interface BaseServiceConfig { /** * Clase base abstracta para servicios CRUD con soporte multi-tenant * - * Proporciona implementaciones reutilizables para: - * - Paginación con filtros - * - Búsqueda por texto - * - CRUD básico - * - Soft delete - * - Transacciones + * @deprecated Use standalone service pattern with TypeORM Repository instead * - * @example + * Example of new pattern: * ```typescript - * class PartnersService extends BaseService { - * protected config: BaseServiceConfig = { - * tableName: 'partners', - * schema: 'core', - * selectFields: 'id, tenant_id, name, email, phone, created_at', - * searchFields: ['name', 'email', 'tax_id'], - * defaultSortField: 'name', - * softDelete: true, - * }; + * import { DataSource, Repository, IsNull } from 'typeorm'; + * + * interface ServiceContext { + * tenantId: string; + * userId?: string; + * } + * + * interface PaginatedResult { + * data: T[]; + * total: number; + * page: number; + * limit: number; + * totalPages: number; + * } + * + * export class MyService { + * private repository: Repository; + * + * constructor(dataSource: DataSource) { + * this.repository = dataSource.getRepository(MyEntity); + * } + * + * async findById(ctx: ServiceContext, id: string): Promise { + * return this.repository.findOne({ + * where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() }, + * }); + * } * } * ``` */ -export abstract class BaseService { +export abstract class BaseService { protected abstract config: BaseServiceConfig; /** @@ -86,345 +107,16 @@ export abstract class BaseService { return `${this.config.schema}.${this.config.tableName}`; } - /** - * Obtiene todos los registros con paginación y filtros - */ - async findAll( - tenantId: string, - filters: BasePaginationFilters & Record = {}, - options: QueryOptions = {} - ): Promise> { - const { - page = 1, - limit = 20, - sortBy = this.config.defaultSortField || 'created_at', - sortOrder = 'desc', - search, - ...customFilters - } = filters; - - const offset = (page - 1) * limit; - const params: any[] = [tenantId]; - let paramIndex = 2; - - // Construir WHERE clause - let whereClause = 'WHERE tenant_id = $1'; - - // Soft delete - if (this.config.softDelete && !options.includeDeleted) { - whereClause += ' AND deleted_at IS NULL'; - } - - // Búsqueda por texto - if (search && this.config.searchFields?.length) { - const searchConditions = this.config.searchFields - .map(field => `${field} ILIKE $${paramIndex}`) - .join(' OR '); - whereClause += ` AND (${searchConditions})`; - params.push(`%${search}%`); - paramIndex++; - } - - // Filtros custom - for (const [key, value] of Object.entries(customFilters)) { - if (value !== undefined && value !== null && value !== '') { - whereClause += ` AND ${key} = $${paramIndex++}`; - params.push(value); - } - } - - // Validar sortBy para prevenir SQL injection - const safeSortBy = this.sanitizeFieldName(sortBy); - const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC'; - - // Query de conteo - const countSql = ` - SELECT COUNT(*) as count - FROM ${this.fullTableName} - ${whereClause} - `; - - // Query de datos - const dataSql = ` - SELECT ${this.config.selectFields} - FROM ${this.fullTableName} - ${whereClause} - ORDER BY ${safeSortBy} ${safeSortOrder} - LIMIT $${paramIndex++} OFFSET $${paramIndex} - `; - - if (options.client) { - const [countResult, dataResult] = await Promise.all([ - options.client.query(countSql, params), - options.client.query(dataSql, [...params, limit, offset]), - ]); - - const total = parseInt(countResult.rows[0]?.count || '0', 10); - - return { - data: dataResult.rows as T[], - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }; - } - - const [countRows, dataRows] = await Promise.all([ - query<{ count: string }>(countSql, params), - query(dataSql, [...params, limit, offset]), - ]); - - const total = parseInt(countRows[0]?.count || '0', 10); - - return { - data: dataRows, - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }; - } - - /** - * Obtiene un registro por ID - */ - async findById( - id: string, - tenantId: string, - options: QueryOptions = {} - ): Promise { - let whereClause = 'WHERE id = $1 AND tenant_id = $2'; - - if (this.config.softDelete && !options.includeDeleted) { - whereClause += ' AND deleted_at IS NULL'; - } - - const sql = ` - SELECT ${this.config.selectFields} - FROM ${this.fullTableName} - ${whereClause} - `; - - if (options.client) { - const result = await options.client.query(sql, [id, tenantId]); - return result.rows[0] as T || null; - } - const rows = await query(sql, [id, tenantId]); - return rows[0] || null; - } - - /** - * Obtiene un registro por ID o lanza error si no existe - */ - async findByIdOrFail( - id: string, - tenantId: string, - options: QueryOptions = {} - ): Promise { - const entity = await this.findById(id, tenantId, options); - if (!entity) { - throw new NotFoundError(`${this.config.tableName} with id ${id} not found`); - } - return entity; - } - - /** - * Verifica si existe un registro - */ - async exists( - id: string, - tenantId: string, - options: QueryOptions = {} - ): Promise { - let whereClause = 'WHERE id = $1 AND tenant_id = $2'; - - if (this.config.softDelete && !options.includeDeleted) { - whereClause += ' AND deleted_at IS NULL'; - } - - const sql = ` - SELECT 1 FROM ${this.fullTableName} - ${whereClause} - LIMIT 1 - `; - - if (options.client) { - const result = await options.client.query(sql, [id, tenantId]); - return result.rows.length > 0; - } - const rows = await query(sql, [id, tenantId]); - return rows.length > 0; - } - - /** - * Soft delete de un registro - */ - async softDelete( - id: string, - tenantId: string, - userId: string, - options: QueryOptions = {} - ): Promise { - if (!this.config.softDelete) { - throw new ValidationError('Soft delete not enabled for this entity'); - } - - const sql = ` - UPDATE ${this.fullTableName} - SET deleted_at = CURRENT_TIMESTAMP, - deleted_by = $3, - updated_at = CURRENT_TIMESTAMP - WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL - RETURNING id - `; - - if (options.client) { - const result = await options.client.query(sql, [id, tenantId, userId]); - return result.rows.length > 0; - } - const rows = await query(sql, [id, tenantId, userId]); - return rows.length > 0; - } - - /** - * Hard delete de un registro - */ - async hardDelete( - id: string, - tenantId: string, - options: QueryOptions = {} - ): Promise { - const sql = ` - DELETE FROM ${this.fullTableName} - WHERE id = $1 AND tenant_id = $2 - RETURNING id - `; - - if (options.client) { - const result = await options.client.query(sql, [id, tenantId]); - return result.rows.length > 0; - } - const rows = await query(sql, [id, tenantId]); - return rows.length > 0; - } - - /** - * Cuenta registros con filtros - */ - async count( - tenantId: string, - filters: Record = {}, - options: QueryOptions = {} - ): Promise { - const params: any[] = [tenantId]; - let paramIndex = 2; - let whereClause = 'WHERE tenant_id = $1'; - - if (this.config.softDelete && !options.includeDeleted) { - whereClause += ' AND deleted_at IS NULL'; - } - - for (const [key, value] of Object.entries(filters)) { - if (value !== undefined && value !== null) { - whereClause += ` AND ${key} = $${paramIndex++}`; - params.push(value); - } - } - - const sql = ` - SELECT COUNT(*) as count - FROM ${this.fullTableName} - ${whereClause} - `; - - if (options.client) { - const result = await options.client.query(sql, params); - return parseInt(result.rows[0]?.count || '0', 10); - } - const rows = await query<{ count: string }>(sql, params); - return parseInt(rows[0]?.count || '0', 10); - } - - /** - * Ejecuta una función dentro de una transacción - */ - protected async withTransaction( - fn: (client: PoolClient) => Promise - ): Promise { - const client = await getClient(); - try { - await client.query('BEGIN'); - const result = await fn(client); - await client.query('COMMIT'); - return result; - } catch (error) { - await client.query('ROLLBACK'); - throw error; - } finally { - client.release(); - } - } - /** * Sanitiza nombre de campo para prevenir SQL injection */ protected sanitizeFieldName(field: string): string { - // Solo permite caracteres alfanuméricos y guiones bajos if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) { return this.config.defaultSortField || 'created_at'; } return field; } - /** - * Construye un INSERT dinámico - */ - protected buildInsertQuery( - data: Record, - additionalFields: Record = {} - ): { sql: string; params: any[] } { - const allData = { ...data, ...additionalFields }; - const fields = Object.keys(allData); - const values = Object.values(allData); - const placeholders = fields.map((_, i) => `$${i + 1}`); - - const sql = ` - INSERT INTO ${this.fullTableName} (${fields.join(', ')}) - VALUES (${placeholders.join(', ')}) - RETURNING ${this.config.selectFields} - `; - - return { sql, params: values }; - } - - /** - * Construye un UPDATE dinámico - */ - protected buildUpdateQuery( - id: string, - tenantId: string, - data: Record - ): { sql: string; params: any[] } { - const fields = Object.keys(data).filter(k => data[k] !== undefined); - const setClauses = fields.map((f, i) => `${f} = $${i + 1}`); - const values = fields.map(f => data[f]); - - // Agregar updated_at automáticamente - setClauses.push(`updated_at = CURRENT_TIMESTAMP`); - - const paramIndex = fields.length + 1; - - const sql = ` - UPDATE ${this.fullTableName} - SET ${setClauses.join(', ')} - WHERE id = $${paramIndex} AND tenant_id = $${paramIndex + 1} - RETURNING ${this.config.selectFields} - `; - - return { sql, params: [...values, id, tenantId] }; - } - /** * Redondea a N decimales */