fix(hr): Fix PaginatedResult format in employee and puesto services

- Remove meta wrapper from PaginatedResult returns in findWithFilters and findAll
- Return flat format with total, page, limit, totalPages at top level
- Align with base.service.ts interface definition
- Controllers already updated to access flat pagination structure
This commit is contained in:
Adrian Flores Cortes 2026-01-25 14:52:53 -06:00
parent 873693b1d1
commit c8a01c5f14
47 changed files with 1343 additions and 736 deletions

View File

@ -11,12 +11,17 @@ import { DataSource } from 'typeorm';
import { PresupuestoService, CreatePresupuestoDto, AddPartidaDto, UpdatePartidaDto } from '../services/presupuesto.service'; import { PresupuestoService, CreatePresupuestoDto, AddPartidaDto, UpdatePartidaDto } from '../services/presupuesto.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; 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 { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de presupuestos
@ -25,14 +30,12 @@ export function createPresupuestoController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const presupuestoRepository = dataSource.getRepository(Presupuesto);
const partidaRepository = dataSource.getRepository(PresupuestoPartida);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const presupuestoService = new PresupuestoService(presupuestoRepository, partidaRepository); const presupuestoService = new PresupuestoService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -73,7 +76,12 @@ export function createPresupuestoController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -7,11 +7,23 @@
* @module Budgets * @module Budgets
*/ */
import { Repository } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Presupuesto } from '../entities/presupuesto.entity'; import { Presupuesto } from '../entities/presupuesto.entity';
import { PresupuestoPartida } from '../entities/presupuesto-partida.entity'; import { PresupuestoPartida } from '../entities/presupuesto-partida.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreatePresupuestoDto { export interface CreatePresupuestoDto {
code: string; code: string;
name: string; name: string;
@ -34,12 +46,101 @@ export interface UpdatePartidaDto {
sequence?: number; sequence?: number;
} }
export class PresupuestoService extends BaseService<Presupuesto> { export class PresupuestoService {
constructor( private repository: Repository<Presupuesto>;
repository: Repository<Presupuesto>, private partidaRepository: Repository<PresupuestoPartida>;
private readonly partidaRepository: Repository<PresupuestoPartida>
) { constructor(dataSource: DataSource) {
super(repository); this.repository = dataSource.getRepository(Presupuesto);
this.partidaRepository = dataSource.getRepository(PresupuestoPartida);
}
/**
* Busca un presupuesto por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<Presupuesto | null> {
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<PaginatedResult<Presupuesto>> {
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<Presupuesto>): Promise<Presupuesto> {
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<Presupuesto>
): Promise<Presupuesto | null> {
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<boolean> {
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<Presupuesto> {
page = 1, page = 1,
limit = 20 limit = 20
): Promise<PaginatedResult<Presupuesto>> { ): Promise<PaginatedResult<Presupuesto>> {
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, page,
limit, limit,
where: { fraccionamientoId, isActive: true } as any, totalPages: Math.ceil(total / limit),
}); };
} }
/** /**
@ -84,8 +201,8 @@ export class PresupuestoService extends BaseService<Presupuesto> {
where: { where: {
id, id,
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
relations: ['partidas', 'partidas.concepto'], relations: ['partidas', 'partidas.concepto'],
}); });
} }
@ -131,8 +248,8 @@ export class PresupuestoService extends BaseService<Presupuesto> {
where: { where: {
id: partidaId, id: partidaId,
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
}); });
if (!partida) { if (!partida) {
@ -158,7 +275,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
where: { where: {
id: partidaId, id: partidaId,
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
} as any, },
}); });
if (!partida) { if (!partida) {
@ -191,7 +308,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
const total = parseFloat(result?.total || '0'); const total = parseFloat(result?.total || '0');
await this.repository.update( await this.repository.update(
{ id: presupuestoId }, { id: presupuestoId } as any,
{ totalAmount: total, updatedById: ctx.userId } { totalAmount: total, updatedById: ctx.userId }
); );
} }
@ -210,7 +327,7 @@ export class PresupuestoService extends BaseService<Presupuesto> {
// Desactivar versión anterior // Desactivar versión anterior
await this.repository.update( await this.repository.update(
{ id: presupuestoId }, { id: presupuestoId } as any,
{ isActive: false, updatedById: ctx.userId } { isActive: false, updatedById: ctx.userId }
); );

View File

@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
import { AnticipoService, CreateAnticipoDto, AnticipoFilters } from '../services/anticipo.service'; import { AnticipoService, CreateAnticipoDto, AnticipoFilters } from '../services/anticipo.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { Anticipo } from '../entities/anticipo.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createAnticipoController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const anticipoRepository = dataSource.getRepository(Anticipo);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const anticipoService = new AnticipoService(anticipoRepository); const anticipoService = new AnticipoService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -69,7 +71,12 @@ export function createAnticipoController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -120,7 +127,12 @@ export function createAnticipoController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -18,14 +18,17 @@ import {
} from '../services/estimacion.service'; } from '../services/estimacion.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; 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 { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de estimaciones
@ -34,22 +37,12 @@ export function createEstimacionController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // 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 userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const estimacionService = new EstimacionService( const estimacionService = new EstimacionService(dataSource);
estimacionRepository,
conceptoRepository,
generadorRepository,
workflowRepository,
dataSource
);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -92,7 +85,12 @@ export function createEstimacionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -11,23 +11,28 @@ import { DataSource } from 'typeorm';
import { FondoGarantiaService, CreateFondoGarantiaDto, ReleaseFondoDto } from '../services/fondo-garantia.service'; import { FondoGarantiaService, CreateFondoGarantiaDto, ReleaseFondoDto } from '../services/fondo-garantia.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { FondoGarantia } from '../entities/fondo-garantia.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createFondoGarantiaController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const fondoGarantiaRepository = dataSource.getRepository(FondoGarantia);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const fondoGarantiaService = new FondoGarantiaService(fondoGarantiaRepository); const fondoGarantiaService = new FondoGarantiaService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -60,7 +65,12 @@ export function createFondoGarantiaController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -11,23 +11,28 @@ import { DataSource } from 'typeorm';
import { RetencionService, CreateRetencionDto, ReleaseRetencionDto, RetencionFilters } from '../services/retencion.service'; import { RetencionService, CreateRetencionDto, ReleaseRetencionDto, RetencionFilters } from '../services/retencion.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { Retencion } from '../entities/retencion.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createRetencionController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const retencionRepository = dataSource.getRepository(Retencion);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const retencionService = new RetencionService(retencionRepository); const retencionService = new RetencionService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -70,7 +75,12 @@ export function createRetencionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -6,10 +6,22 @@
* @module Estimates * @module Estimates
*/ */
import { Repository } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Anticipo, AdvanceType } from '../entities/anticipo.entity'; import { Anticipo, AdvanceType } from '../entities/anticipo.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateAnticipoDto { export interface CreateAnticipoDto {
contratoId: string; contratoId: string;
advanceType: AdvanceType; advanceType: AdvanceType;
@ -45,9 +57,24 @@ export interface AnticipoStats {
}[]; }[];
} }
export class AnticipoService extends BaseService<Anticipo> { export class AnticipoService {
constructor(repository: Repository<Anticipo>) { private repository: Repository<Anticipo>;
super(repository);
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(Anticipo);
}
/**
* Busca un anticipo por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<Anticipo | null> {
return this.repository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
},
});
} }
/** /**
@ -61,13 +88,13 @@ export class AnticipoService extends BaseService<Anticipo> {
): Promise<PaginatedResult<Anticipo>> { ): Promise<PaginatedResult<Anticipo>> {
const qb = this.repository const qb = this.repository
.createQueryBuilder('a') .createQueryBuilder('a')
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
.andWhere('a.contrato_id = :contratoId', { contratoId }) .andWhere('a.contratoId = :contratoId', { contratoId })
.andWhere('a.deleted_at IS NULL'); .andWhere('a.deletedAt IS NULL');
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
qb.orderBy('a.advance_date', 'DESC') qb.orderBy('a.advanceDate', 'DESC')
.addOrderBy('a.advance_number', 'DESC') .addOrderBy('a.advanceNumber', 'DESC')
.skip(skip) .skip(skip)
.take(limit); .take(limit);
@ -75,12 +102,10 @@ export class AnticipoService extends BaseService<Anticipo> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }
@ -97,11 +122,57 @@ export class AnticipoService extends BaseService<Anticipo> {
throw new Error('El monto neto no coincide con el cálculo (bruto - impuestos)'); throw new Error('El monto neto no coincide con el cálculo (bruto - impuestos)');
} }
return this.create(ctx, { const anticipo = this.repository.create({
...dto, 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, amortizedAmount: 0,
isFullyAmortized: false, isFullyAmortized: false,
notes: dto.notes,
createdBy: ctx.userId,
}); });
return this.repository.save(anticipo);
}
/**
* Actualiza un anticipo
*/
async update(
ctx: ServiceContext,
id: string,
data: Partial<Anticipo>
): Promise<Anticipo | null> {
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<boolean> {
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<Anticipo> {
throw new Error('El anticipo ya está aprobado'); throw new Error('El anticipo ya está aprobado');
} }
const updateData: Partial<Anticipo> = { anticipo.approvedAt = new Date();
approvedAt: new Date(), anticipo.approvedById = approvedById;
approvedById,
};
if (notes) { if (notes) {
updateData.notes = anticipo.notes anticipo.notes = anticipo.notes
? `${anticipo.notes}\n\nAprobación: ${notes}` ? `${anticipo.notes}\n\nAprobación: ${notes}`
: `Aprobación: ${notes}`; : `Aprobación: ${notes}`;
} }
const updated = await this.update(ctx, id, updateData); anticipo.updatedById = ctx.userId;
if (!updated) {
throw new Error('Error al aprobar anticipo');
}
return updated; return this.repository.save(anticipo);
} }
/** /**
@ -163,16 +229,11 @@ export class AnticipoService extends BaseService<Anticipo> {
throw new Error('El anticipo ya está marcado como pagado'); throw new Error('El anticipo ya está marcado como pagado');
} }
const updated = await this.update(ctx, id, { anticipo.paidAt = paidAt || new Date();
paidAt: paidAt || new Date(), anticipo.paymentReference = paymentReference;
paymentReference, anticipo.updatedById = ctx.userId;
});
if (!updated) { return this.repository.save(anticipo);
throw new Error('Error al marcar anticipo como pagado');
}
return updated;
} }
/** /**
@ -184,28 +245,28 @@ export class AnticipoService extends BaseService<Anticipo> {
): Promise<AnticipoStats> { ): Promise<AnticipoStats> {
const qb = this.repository const qb = this.repository
.createQueryBuilder('a') .createQueryBuilder('a')
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
.andWhere('a.deleted_at IS NULL'); .andWhere('a.deletedAt IS NULL');
if (filters?.contratoId) { if (filters?.contratoId) {
qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
} }
if (filters?.advanceType) { if (filters?.advanceType) {
qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
} }
if (filters?.isFullyAmortized !== undefined) { if (filters?.isFullyAmortized !== undefined) {
qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
isFullyAmortized: filters.isFullyAmortized, isFullyAmortized: filters.isFullyAmortized,
}); });
} }
if (filters?.startDate) { if (filters?.startDate) {
qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
} }
if (filters?.endDate) { if (filters?.endDate) {
qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
} }
if (filters?.approvedById) { 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(); const anticipos = await qb.getMany();
@ -222,7 +283,7 @@ export class AnticipoService extends BaseService<Anticipo> {
const typeStats = new Map<AdvanceType, { count: number; totalAmount: number }>(); const typeStats = new Map<AdvanceType, { count: number; totalAmount: number }>();
anticipos.forEach((anticipo) => { anticipos.forEach((anticipo: Anticipo) => {
const netAmount = Number(anticipo.netAmount) || 0; const netAmount = Number(anticipo.netAmount) || 0;
const amortizedAmount = Number(anticipo.amortizedAmount) || 0; const amortizedAmount = Number(anticipo.amortizedAmount) || 0;
@ -267,33 +328,33 @@ export class AnticipoService extends BaseService<Anticipo> {
): Promise<PaginatedResult<Anticipo>> { ): Promise<PaginatedResult<Anticipo>> {
const qb = this.repository const qb = this.repository
.createQueryBuilder('a') .createQueryBuilder('a')
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) .where('a.tenantId = :tenantId', { tenantId: ctx.tenantId })
.andWhere('a.deleted_at IS NULL'); .andWhere('a.deletedAt IS NULL');
if (filters.contratoId) { if (filters.contratoId) {
qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); qb.andWhere('a.contratoId = :contratoId', { contratoId: filters.contratoId });
} }
if (filters.advanceType) { if (filters.advanceType) {
qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); qb.andWhere('a.advanceType = :advanceType', { advanceType: filters.advanceType });
} }
if (filters.isFullyAmortized !== undefined) { if (filters.isFullyAmortized !== undefined) {
qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { qb.andWhere('a.isFullyAmortized = :isFullyAmortized', {
isFullyAmortized: filters.isFullyAmortized, isFullyAmortized: filters.isFullyAmortized,
}); });
} }
if (filters.startDate) { if (filters.startDate) {
qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); qb.andWhere('a.advanceDate >= :startDate', { startDate: filters.startDate });
} }
if (filters.endDate) { if (filters.endDate) {
qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); qb.andWhere('a.advanceDate <= :endDate', { endDate: filters.endDate });
} }
if (filters.approvedById) { 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; const skip = (page - 1) * limit;
qb.orderBy('a.advance_date', 'DESC') qb.orderBy('a.advanceDate', 'DESC')
.addOrderBy('a.advance_number', 'DESC') .addOrderBy('a.advanceNumber', 'DESC')
.skip(skip) .skip(skip)
.take(limit); .take(limit);
@ -301,12 +362,10 @@ export class AnticipoService extends BaseService<Anticipo> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }
@ -337,15 +396,10 @@ export class AnticipoService extends BaseService<Anticipo> {
const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01; const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01;
const updated = await this.update(ctx, id, { anticipo.amortizedAmount = newAmortizedAmount;
amortizedAmount: newAmortizedAmount, anticipo.isFullyAmortized = isFullyAmortized;
isFullyAmortized, anticipo.updatedById = ctx.userId;
});
if (!updated) { return this.repository.save(anticipo);
throw new Error('Error al actualizar amortización');
}
return updated;
} }
} }

View File

@ -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. * Gestiona estimaciones periodicas con workflow de aprobacion.
* Incluye cálculo de anticipos, retenciones e IVA. * Incluye calculo de anticipos, retenciones e IVA.
* *
* @module Estimates * @module Estimates
*/ */
import { Repository, DataSource } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Estimacion, EstimateStatus } from '../entities/estimacion.entity'; import { Estimacion, EstimateStatus } from '../entities/estimacion.entity';
import { EstimacionConcepto } from '../entities/estimacion-concepto.entity'; import { EstimacionConcepto } from '../entities/estimacion-concepto.entity';
import { Generador } from '../entities/generador.entity'; import { Generador } from '../entities/generador.entity';
import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity'; import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateEstimacionDto { export interface CreateEstimacionDto {
contratoId: string; contratoId: string;
fraccionamientoId: string; fraccionamientoId: string;
@ -52,19 +64,74 @@ export interface EstimacionFilters {
periodTo?: Date; periodTo?: Date;
} }
export class EstimacionService extends BaseService<Estimacion> { interface ContractEstimateSummary {
constructor( totalEstimates: number;
repository: Repository<Estimacion>, totalApproved: number;
private readonly conceptoRepository: Repository<EstimacionConcepto>, totalPaid: number;
private readonly generadorRepository: Repository<Generador>, }
private readonly workflowRepository: Repository<EstimacionWorkflow>,
private readonly dataSource: DataSource export class EstimacionService {
) { private repository: Repository<Estimacion>;
super(repository); private conceptoRepository: Repository<EstimacionConcepto>;
private generadorRepository: Repository<Generador>;
private workflowRepository: Repository<EstimacionWorkflow>;
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<Estimacion | null> {
return this.repository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
},
});
}
/**
* Actualiza una estimacion
*/
async update(
ctx: ServiceContext,
id: string,
data: Partial<Estimacion>
): Promise<Estimacion | null> {
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<boolean> {
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( async createEstimacion(
ctx: ServiceContext, ctx: ServiceContext,
@ -73,21 +140,29 @@ export class EstimacionService extends BaseService<Estimacion> {
const sequenceNumber = await this.getNextSequenceNumber(ctx, data.contratoId); const sequenceNumber = await this.getNextSequenceNumber(ctx, data.contratoId);
const estimateNumber = await this.generateEstimateNumber(ctx, data.contratoId, sequenceNumber); const estimateNumber = await this.generateEstimateNumber(ctx, data.contratoId, sequenceNumber);
const estimacion = await this.create(ctx, { const estimacion = this.repository.create({
...data, tenantId: ctx.tenantId,
contratoId: data.contratoId,
fraccionamientoId: data.fraccionamientoId,
periodStart: data.periodStart,
periodEnd: data.periodEnd,
notes: data.notes,
estimateNumber, estimateNumber,
sequenceNumber, sequenceNumber,
status: 'draft', status: 'draft' as EstimateStatus,
createdById: ctx.userId,
}); });
// Registrar en workflow const savedEstimacion = await this.repository.save(estimacion);
await this.addWorkflowEntry(ctx, estimacion.id, null, 'draft', 'create', 'Estimación creada');
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( private async getNextSequenceNumber(
ctx: ServiceContext, ctx: ServiceContext,
@ -104,7 +179,7 @@ export class EstimacionService extends BaseService<Estimacion> {
} }
/** /**
* Generar número de estimación * Generar numero de estimacion
*/ */
private async generateEstimateNumber( private async generateEstimateNumber(
_ctx: ServiceContext, _ctx: ServiceContext,
@ -124,11 +199,24 @@ export class EstimacionService extends BaseService<Estimacion> {
page = 1, page = 1,
limit = 20 limit = 20
): Promise<PaginatedResult<Estimacion>> { ): Promise<PaginatedResult<Estimacion>> {
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, page,
limit, limit,
where: { contratoId } as any, totalPages: Math.ceil(total / limit),
}); };
} }
/** /**
@ -168,25 +256,23 @@ export class EstimacionService extends BaseService<Estimacion> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }
/** /**
* Obtener estimación con detalles completos * Obtener estimacion con detalles completos
*/ */
async findWithDetails(ctx: ServiceContext, id: string): Promise<Estimacion | null> { async findWithDetails(ctx: ServiceContext, id: string): Promise<Estimacion | null> {
return this.repository.findOne({ return this.repository.findOne({
where: { where: {
id, id,
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
relations: [ relations: [
'conceptos', 'conceptos',
'conceptos.concepto', 'conceptos.concepto',
@ -199,7 +285,7 @@ export class EstimacionService extends BaseService<Estimacion> {
} }
/** /**
* Agregar concepto a estimación * Agregar concepto a estimacion
*/ */
async addConcepto( async addConcepto(
ctx: ServiceContext, ctx: ServiceContext,
@ -231,7 +317,7 @@ export class EstimacionService extends BaseService<Estimacion> {
} }
/** /**
* Agregar generador a concepto de estimación * Agregar generador a concepto de estimacion
*/ */
async addGenerador( async addGenerador(
ctx: ServiceContext, ctx: ServiceContext,
@ -239,7 +325,7 @@ export class EstimacionService extends BaseService<Estimacion> {
data: AddGeneradorDto data: AddGeneradorDto
): Promise<Generador> { ): Promise<Generador> {
const concepto = await this.conceptoRepository.findOne({ const concepto = await this.conceptoRepository.findOne({
where: { id: estimacionConceptoId, tenantId: ctx.tenantId } as any, where: { id: estimacionConceptoId, tenantId: ctx.tenantId },
relations: ['estimacion'], relations: ['estimacion'],
}); });
@ -268,15 +354,15 @@ export class EstimacionService extends BaseService<Estimacion> {
} }
/** /**
* Recalcular totales de estimación * Recalcular totales de estimacion
*/ */
async recalculateTotals(_ctx: ServiceContext, estimacionId: string): Promise<void> { async recalculateTotals(_ctx: ServiceContext, estimacionId: string): Promise<void> {
// Ejecutar función de PostgreSQL // Ejecutar funcion de PostgreSQL
await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]); await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]);
} }
/** /**
* Cambiar estado de estimación * Cambiar estado de estimacion
*/ */
async changeStatus( async changeStatus(
ctx: ServiceContext, ctx: ServiceContext,
@ -362,28 +448,28 @@ export class EstimacionService extends BaseService<Estimacion> {
} }
/** /**
* Enviar estimación para revisión * Enviar estimacion para revision
*/ */
async submit(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> { async submit(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
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<Estimacion | null> { async review(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
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<Estimacion | null> { async approve(ctx: ServiceContext, estimacionId: string): Promise<Estimacion | null> {
return this.changeStatus(ctx, estimacionId, 'approved', 'approve', 'Aprobada'); return this.changeStatus(ctx, estimacionId, 'approved', 'approve', 'Aprobada');
} }
/** /**
* Rechazar estimación * Rechazar estimacion
*/ */
async reject( async reject(
ctx: ServiceContext, ctx: ServiceContext,
@ -416,9 +502,3 @@ export class EstimacionService extends BaseService<Estimacion> {
}; };
} }
} }
interface ContractEstimateSummary {
totalEstimates: number;
totalApproved: number;
totalPaid: number;
}

View File

@ -6,10 +6,22 @@
* @module Estimates * @module Estimates
*/ */
import { Repository } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { FondoGarantia } from '../entities/fondo-garantia.entity'; import { FondoGarantia } from '../entities/fondo-garantia.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateFondoGarantiaDto { export interface CreateFondoGarantiaDto {
contratoId: string; contratoId: string;
accumulatedAmount?: number; accumulatedAmount?: number;
@ -39,9 +51,103 @@ export interface FondoGarantiaStats {
fondosFullyReleased: number; fondosFullyReleased: number;
} }
export class FondoGarantiaService extends BaseService<FondoGarantia> { export class FondoGarantiaService {
constructor(repository: Repository<FondoGarantia>) { private repository: Repository<FondoGarantia>;
super(repository);
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(FondoGarantia);
}
/**
* Busca un fondo de garantía por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<FondoGarantia | null> {
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<PaginatedResult<FondoGarantia>> {
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<FondoGarantia>
): Promise<FondoGarantia> {
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<FondoGarantia>
): Promise<FondoGarantia | null> {
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<boolean> {
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<FondoGarantia> {
where: { where: {
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
contratoId, contratoId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
}); });
} }
@ -183,8 +289,8 @@ export class FondoGarantiaService extends BaseService<FondoGarantia> {
const fondos = await this.repository.find({ const fondos = await this.repository.find({
where: { where: {
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
}); });
const stats: FondoGarantiaStats = { const stats: FondoGarantiaStats = {
@ -260,16 +366,14 @@ export class FondoGarantiaService extends BaseService<FondoGarantia> {
}); });
} }
const finalTotal = filteredData.length !== data.length ? filteredData.length : total;
return { return {
data: filteredData, data: filteredData,
meta: { total: finalTotal,
total: filteredData.length !== data.length ? filteredData.length : total, page,
page, limit,
limit, totalPages: Math.ceil(finalTotal / limit),
totalPages: Math.ceil(
(filteredData.length !== data.length ? filteredData.length : total) / limit
),
},
}; };
} }
} }

View File

@ -6,10 +6,22 @@
* @module Estimates * @module Estimates
*/ */
import { Repository } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Retencion, RetentionType } from '../entities/retencion.entity'; import { Retencion, RetentionType } from '../entities/retencion.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateRetencionDto { export interface CreateRetencionDto {
estimacionId: string; estimacionId: string;
retentionType: RetentionType; retentionType: RetentionType;
@ -34,9 +46,24 @@ export interface RetencionFilters {
dateTo?: Date; dateTo?: Date;
} }
export class RetencionService extends BaseService<Retencion> { export class RetencionService {
constructor(repository: Repository<Retencion>) { private repository: Repository<Retencion>;
super(repository);
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(Retencion);
}
/**
* Busca una retención por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<Retencion | null> {
return this.repository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: IsNull(),
},
});
} }
/** /**
@ -50,8 +77,8 @@ export class RetencionService extends BaseService<Retencion> {
where: { where: {
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
estimacionId, estimacionId,
deletedAt: null, deletedAt: IsNull(),
} as any, },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
}); });
} }
@ -63,11 +90,55 @@ export class RetencionService extends BaseService<Retencion> {
ctx: ServiceContext, ctx: ServiceContext,
data: CreateRetencionDto data: CreateRetencionDto
): Promise<Retencion> { ): Promise<Retencion> {
return this.create(ctx, { const retencion = this.repository.create({
...data, 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, releasedAmount: null,
releasedAt: null, releasedAt: null,
createdById: ctx.userId ?? null,
}); });
return this.repository.save(retencion);
}
/**
* Actualiza una retención
*/
async update(
ctx: ServiceContext,
id: string,
data: Partial<Retencion>
): Promise<Retencion | null> {
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<boolean> {
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<Retencion> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -17,12 +17,17 @@ import {
} from '../services/employee.service'; } from '../services/employee.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; 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 { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de empleados
@ -31,14 +36,12 @@ export function createEmployeeController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const employeeRepository = dataSource.getRepository(Employee);
const asignacionRepository = dataSource.getRepository(EmployeeFraccionamiento);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const employeeService = new EmployeeService(employeeRepository, asignacionRepository); const employeeService = new EmployeeService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -81,7 +84,12 @@ export function createEmployeeController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -11,11 +11,17 @@ import { DataSource } from 'typeorm';
import { PuestoService, CreatePuestoDto, UpdatePuestoDto, PuestoFilters } from '../services/puesto.service'; import { PuestoService, CreatePuestoDto, UpdatePuestoDto, PuestoFilters } from '../services/puesto.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { Puesto } from '../entities/puesto.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de puestos
@ -24,13 +30,12 @@ export function createPuestoController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const puestoRepository = dataSource.getRepository(Puesto);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const puestoService = new PuestoService(puestoRepository); const puestoService = new PuestoService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -71,7 +76,12 @@ export function createPuestoController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -129,12 +129,10 @@ export class EmployeeService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -71,12 +71,10 @@ export class PuestoService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -35,7 +35,14 @@ import { QuejaAmbiental } from '../entities/queja-ambiental.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de gestión ambiental
@ -103,7 +110,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -163,7 +175,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -284,7 +301,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -417,7 +439,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -500,7 +527,12 @@ export function createAmbientalController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -20,7 +20,14 @@ import { Capacitacion } from '../entities/capacitacion.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de capacitaciones
@ -76,7 +83,12 @@ export function createCapacitacionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -29,7 +29,14 @@ import { EppMovimiento } from '../entities/epp-movimiento.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de EPP
@ -101,7 +108,12 @@ export function createEppController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -228,7 +240,12 @@ export function createEppController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -26,7 +26,14 @@ import { IncidenteAccion } from '../entities/incidente-accion.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de incidentes
@ -91,7 +98,12 @@ export function createIncidenteController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -25,7 +25,14 @@ import { AlertaIndicador } from '../entities/alerta-indicador.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de indicadores
@ -97,7 +104,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -258,7 +270,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -341,7 +358,12 @@ export function createIndicadorController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -25,7 +25,14 @@ import { TipoInspeccion } from '../entities/tipo-inspeccion.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de inspecciones
@ -107,7 +114,12 @@ export function createInspeccionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -277,7 +289,12 @@ export function createInspeccionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -26,7 +26,14 @@ import { PermisoDocumento } from '../entities/permiso-documento.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de permisos de trabajo

View File

@ -37,7 +37,14 @@ import { Auditoria, ResultadoAuditoria } from '../entities/auditoria.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de STPS

View File

@ -177,7 +177,10 @@ export class AmbientalService {
return { return {
data, 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 { return {
data, 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 { return {
data, 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 { return {
data, 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 { return {
data, data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, total,
page,
limit,
totalPages: Math.ceil(total / limit),
}; };
} }

View File

@ -77,12 +77,10 @@ export class CapacitacionService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -131,7 +131,10 @@ export class EppService {
return { return {
data, 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 { return {
data, data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, total,
page,
limit,
totalPages: Math.ceil(total / limit),
}; };
} }

View File

@ -143,12 +143,10 @@ export class IncidenteService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -91,7 +91,10 @@ export class IndicadorService {
return { return {
data, 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 { return {
data, 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 { return {
data, data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, total,
page,
limit,
totalPages: Math.ceil(total / limit),
}; };
} }

View File

@ -143,12 +143,10 @@ export class InspeccionService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }
@ -258,12 +256,10 @@ export class InspeccionService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -133,7 +133,10 @@ export class PermisoTrabajoService {
return { return {
data, data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, total,
page,
limit,
totalPages: Math.ceil(total / limit),
}; };
} }

View File

@ -195,7 +195,10 @@ export class StpsService {
return { return {
data, 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 { return {
data, data,
meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, total,
page,
limit,
totalPages: Math.ceil(total / limit),
}; };
} }

View File

@ -18,7 +18,14 @@ import { AuthService } from '../../auth/services/auth.service';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createAsignacionController(dataSource: DataSource): Router {
const router = 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 limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const result = await service.findWithFilters(getContext(req), filters, page, limit); 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) { } catch (error) {
next(error); next(error);
} }

View File

@ -17,7 +17,14 @@ import { AuthService } from '../../auth/services/auth.service';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createDerechohabienteController(dataSource: DataSource): Router {
const router = 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 limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const result = await service.findWithFilters(getContext(req), filters, page, limit); 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) { } catch (error) {
next(error); next(error);
} }

View File

@ -19,7 +19,11 @@ import { ConsumoObra } from '../entities/consumo-obra.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de consumos
@ -78,7 +82,12 @@ export function createConsumoObraController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -22,7 +22,11 @@ import { RequisicionLinea } from '../entities/requisicion-linea.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de requisiciones
@ -81,7 +85,12 @@ export function createRequisicionController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -8,7 +8,19 @@
import { Repository, FindOptionsWhere } from 'typeorm'; import { Repository, FindOptionsWhere } from 'typeorm';
import { ConsumoObra } from '../entities/consumo-obra.entity'; import { ConsumoObra } from '../entities/consumo-obra.entity';
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateConsumoDto { export interface CreateConsumoDto {
fraccionamientoId: string; fraccionamientoId: string;
@ -94,12 +106,10 @@ export class ConsumoObraService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -10,7 +10,19 @@
import { Repository, FindOptionsWhere } from 'typeorm'; import { Repository, FindOptionsWhere } from 'typeorm';
import { RequisicionObra, RequisitionStatus } from '../entities/requisicion-obra.entity'; import { RequisicionObra, RequisitionStatus } from '../entities/requisicion-obra.entity';
import { RequisicionLinea } from '../entities/requisicion-linea.entity'; import { RequisicionLinea } from '../entities/requisicion-linea.entity';
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateRequisicionDto { export interface CreateRequisicionDto {
fraccionamientoId: string; fraccionamientoId: string;
@ -98,12 +110,10 @@ export class RequisicionService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -17,7 +17,14 @@ import { FotoAvance } from '../entities/foto-avance.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de avances de obra
@ -77,7 +84,12 @@ export function createAvanceObraController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, 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) { } catch (error) {
next(error); next(error);

View File

@ -15,7 +15,14 @@ import { BitacoraObra } from '../entities/bitacora-obra.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 * Crear router de bitácora de obra
@ -77,7 +84,12 @@ export function createBitacoraObraController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, 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) { } catch (error) {
next(error); next(error);

View File

@ -18,7 +18,11 @@ import { AuthService } from '../../auth/services/auth.service';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createComparativoController(dataSource: DataSource): Router {
const router = 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 limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const result = await service.findWithFilters(getContext(req), filters, page, limit); 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) { } catch (error) {
next(error); next(error);
} }

View File

@ -10,7 +10,19 @@ import { Repository, FindOptionsWhere } from 'typeorm';
import { ComparativoCotizaciones, ComparativoStatus } from '../entities/comparativo-cotizaciones.entity'; import { ComparativoCotizaciones, ComparativoStatus } from '../entities/comparativo-cotizaciones.entity';
import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity'; import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity';
import { ComparativoProducto } from '../entities/comparativo-producto.entity'; import { ComparativoProducto } from '../entities/comparativo-producto.entity';
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateComparativoDto { export interface CreateComparativoDto {
requisicionId?: string; requisicionId?: string;
@ -99,12 +111,10 @@ export class ComparativoService {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -18,23 +18,25 @@ import {
} from '../services/dashboard.service'; } from '../services/dashboard.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { Dashboard } from '../entities/dashboard.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createDashboardController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const dashboardRepository = dataSource.getRepository(Dashboard);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const dashboardService = new DashboardService(dashboardRepository, dataSource); const dashboardService = new DashboardService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -76,7 +78,12 @@ export function createDashboardController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
import { KpiService, CreateKpiSnapshotDto, KpiFilters } from '../services/kpi.service'; import { KpiService, CreateKpiSnapshotDto, KpiFilters } from '../services/kpi.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { KpiSnapshot } from '../entities/kpi-snapshot.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createKpiController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const kpiRepository = dataSource.getRepository(KpiSnapshot);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const kpiService = new KpiService(kpiRepository); const kpiService = new KpiService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -69,7 +71,12 @@ export function createKpiController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -11,23 +11,25 @@ import { DataSource } from 'typeorm';
import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service'; import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service';
import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
import { AuthService } from '../../auth/services/auth.service'; import { AuthService } from '../../auth/services/auth.service';
import { Report } from '../entities/report.entity';
import { User } from '../../core/entities/user.entity'; import { User } from '../../core/entities/user.entity';
import { Tenant } from '../../core/entities/tenant.entity'; import { Tenant } from '../../core/entities/tenant.entity';
import { RefreshToken } from '../../auth/entities/refresh-token.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 { export function createReportController(dataSource: DataSource): Router {
const router = Router(); const router = Router();
// Repositorios // Repositorios
const reportRepository = dataSource.getRepository(Report);
const userRepository = dataSource.getRepository(User); const userRepository = dataSource.getRepository(User);
const tenantRepository = dataSource.getRepository(Tenant); const tenantRepository = dataSource.getRepository(Tenant);
const refreshTokenRepository = dataSource.getRepository(RefreshToken); const refreshTokenRepository = dataSource.getRepository(RefreshToken);
// Servicios // Servicios
const reportService = new ReportService(reportRepository, dataSource); const reportService = new ReportService(dataSource);
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
const authMiddleware = new AuthMiddleware(authService, dataSource); const authMiddleware = new AuthMiddleware(authService, dataSource);
@ -67,7 +69,12 @@ export function createReportController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -279,7 +286,12 @@ export function createReportController(dataSource: DataSource): Router {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: result.data, data: result.data,
pagination: result.meta, pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
}); });
} catch (error) { } catch (error) {
next(error); next(error);

View File

@ -7,10 +7,22 @@
*/ */
import { Repository, DataSource } from 'typeorm'; import { Repository, DataSource } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Dashboard, DashboardType, DashboardVisibility } from '../entities/dashboard.entity'; import { Dashboard, DashboardType, DashboardVisibility } from '../entities/dashboard.entity';
import { DashboardWidget, WidgetType, DataSourceType } from '../entities/dashboard-widget.entity'; import { DashboardWidget, WidgetType, DataSourceType } from '../entities/dashboard-widget.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateDashboardDto { export interface CreateDashboardDto {
code: string; code: string;
name: string; name: string;
@ -66,17 +78,61 @@ export interface DashboardFilters {
search?: string; search?: string;
} }
export class DashboardService extends BaseService<Dashboard> { export class DashboardService {
private repository: Repository<Dashboard>;
private widgetRepository: Repository<DashboardWidget>; private widgetRepository: Repository<DashboardWidget>;
constructor( constructor(dataSource: DataSource) {
repository: Repository<Dashboard>, this.repository = dataSource.getRepository(Dashboard);
dataSource: DataSource
) {
super(repository);
this.widgetRepository = dataSource.getRepository(DashboardWidget); this.widgetRepository = dataSource.getRepository(DashboardWidget);
} }
/**
* Buscar por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<Dashboard | null> {
return this.repository.findOne({
where: {
id,
tenantId: ctx.tenantId,
deletedAt: null,
} as any,
});
}
/**
* Crear entidad
*/
async create(ctx: ServiceContext, data: Partial<Dashboard>): Promise<Dashboard> {
const entity = this.repository.create({
...data,
tenantId: ctx.tenantId,
});
return this.repository.save(entity);
}
/**
* Actualizar entidad
*/
async update(ctx: ServiceContext, id: string, data: Partial<Dashboard>): Promise<Dashboard | null> {
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<boolean> {
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 * Buscar dashboards con filtros
*/ */

View File

@ -6,10 +6,22 @@
* @module Reports * @module Reports
*/ */
import { Repository, LessThanOrEqual } from 'typeorm'; import { Repository, DataSource, LessThanOrEqual } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { KpiSnapshot, KpiCategory, KpiPeriodType, TrendDirection } from '../entities/kpi-snapshot.entity'; import { KpiSnapshot, KpiCategory, KpiPeriodType, TrendDirection } from '../entities/kpi-snapshot.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateKpiSnapshotDto { export interface CreateKpiSnapshotDto {
kpiCode: string; kpiCode: string;
kpiName: string; kpiName: string;
@ -66,9 +78,34 @@ export interface KpiSummary {
}[]; }[];
} }
export class KpiService extends BaseService<KpiSnapshot> { export class KpiService {
constructor(repository: Repository<KpiSnapshot>) { private repository: Repository<KpiSnapshot>;
super(repository);
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(KpiSnapshot);
}
/**
* Buscar por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<KpiSnapshot | null> {
return this.repository.findOne({
where: {
id,
tenantId: ctx.tenantId,
} as any,
});
}
/**
* Crear entidad
*/
async create(ctx: ServiceContext, data: Partial<KpiSnapshot>): Promise<KpiSnapshot> {
const entity = this.repository.create({
...data,
tenantId: ctx.tenantId,
});
return this.repository.save(entity);
} }
/** /**
@ -110,12 +147,10 @@ export class KpiService extends BaseService<KpiSnapshot> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -6,11 +6,23 @@
* @module Reports * @module Reports
*/ */
import { Repository, DataSource } from 'typeorm'; import { DataSource, Repository, IsNull } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { Report, ReportType, ReportFormat, ReportFrequency } from '../entities/report.entity'; import { Report, ReportType, ReportFormat, ReportFrequency } from '../entities/report.entity';
import { ReportExecution, ExecutionStatus } from '../entities/report-execution.entity'; import { ReportExecution, ExecutionStatus } from '../entities/report-execution.entity';
interface ServiceContext {
tenantId: string;
userId?: string;
}
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface CreateReportDto { export interface CreateReportDto {
code: string; code: string;
name: string; name: string;
@ -49,17 +61,57 @@ export interface ExecuteReportDto {
distribute?: boolean; distribute?: boolean;
} }
export class ReportService extends BaseService<Report> { export class ReportService {
private repository: Repository<Report>;
private executionRepository: Repository<ReportExecution>; private executionRepository: Repository<ReportExecution>;
constructor( constructor(dataSource: DataSource) {
repository: Repository<Report>, this.repository = dataSource.getRepository(Report);
dataSource: DataSource
) {
super(repository);
this.executionRepository = dataSource.getRepository(ReportExecution); this.executionRepository = dataSource.getRepository(ReportExecution);
} }
/**
* Buscar reporte por ID
*/
async findById(ctx: ServiceContext, id: string): Promise<Report | null> {
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<Report> {
const report = this.repository.create({
...data,
tenantId: ctx.tenantId,
});
return this.repository.save(report);
}
/**
* Actualizar reporte
*/
async update(ctx: ServiceContext, id: string, data: Partial<UpdateReportDto>): Promise<Report | null> {
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<boolean> {
const result = await this.repository.update(
{ id, tenantId: ctx.tenantId, deletedAt: IsNull() },
{ deletedAt: new Date() }
);
return result.affected! > 0;
}
/** /**
* Buscar reportes con filtros * Buscar reportes con filtros
*/ */
@ -96,12 +148,10 @@ export class ReportService extends BaseService<Report> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }
@ -288,12 +338,10 @@ export class ReportService extends BaseService<Report> {
return { return {
data, data,
meta: { total,
total, page,
page, limit,
limit, totalPages: Math.ceil(total / limit),
totalPages: Math.ceil(total / limit),
},
}; };
} }

View File

@ -1,6 +1,15 @@
import { query, queryOne, getClient, PoolClient } from '../../config/database.js'; /**
import { NotFoundError, ValidationError } from '../errors/index.js'; * BaseService - Base service class (deprecated)
import { PaginationMeta } from '../types/index.js'; *
* 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 * Contexto de servicio para multi-tenant
@ -36,7 +45,6 @@ export interface BasePaginationFilters {
* Opciones para construcción de queries * Opciones para construcción de queries
*/ */
export interface QueryOptions { export interface QueryOptions {
client?: PoolClient;
includeDeleted?: boolean; includeDeleted?: boolean;
} }
@ -55,28 +63,41 @@ export interface BaseServiceConfig {
/** /**
* Clase base abstracta para servicios CRUD con soporte multi-tenant * Clase base abstracta para servicios CRUD con soporte multi-tenant
* *
* Proporciona implementaciones reutilizables para: * @deprecated Use standalone service pattern with TypeORM Repository instead
* - Paginación con filtros
* - Búsqueda por texto
* - CRUD básico
* - Soft delete
* - Transacciones
* *
* @example * Example of new pattern:
* ```typescript * ```typescript
* class PartnersService extends BaseService<Partner, CreatePartnerDto, UpdatePartnerDto> { * import { DataSource, Repository, IsNull } from 'typeorm';
* protected config: BaseServiceConfig = { *
* tableName: 'partners', * interface ServiceContext {
* schema: 'core', * tenantId: string;
* selectFields: 'id, tenant_id, name, email, phone, created_at', * userId?: string;
* searchFields: ['name', 'email', 'tax_id'], * }
* defaultSortField: 'name', *
* softDelete: true, * interface PaginatedResult<T> {
* }; * data: T[];
* total: number;
* page: number;
* limit: number;
* totalPages: number;
* }
*
* export class MyService {
* private repository: Repository<MyEntity>;
*
* constructor(dataSource: DataSource) {
* this.repository = dataSource.getRepository(MyEntity);
* }
*
* async findById(ctx: ServiceContext, id: string): Promise<MyEntity | null> {
* return this.repository.findOne({
* where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
* });
* }
* } * }
* ``` * ```
*/ */
export abstract class BaseService<T, CreateDto, UpdateDto> { export abstract class BaseService<T, _CreateDto = unknown, _UpdateDto = unknown> {
protected abstract config: BaseServiceConfig; protected abstract config: BaseServiceConfig;
/** /**
@ -86,345 +107,16 @@ export abstract class BaseService<T, CreateDto, UpdateDto> {
return `${this.config.schema}.${this.config.tableName}`; return `${this.config.schema}.${this.config.tableName}`;
} }
/**
* Obtiene todos los registros con paginación y filtros
*/
async findAll(
tenantId: string,
filters: BasePaginationFilters & Record<string, any> = {},
options: QueryOptions = {}
): Promise<PaginatedResult<T>> {
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<T>(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<T | null> {
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<T>(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<T> {
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<boolean> {
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<boolean> {
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<boolean> {
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<string, any> = {},
options: QueryOptions = {}
): Promise<number> {
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<R>(
fn: (client: PoolClient) => Promise<R>
): Promise<R> {
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 * Sanitiza nombre de campo para prevenir SQL injection
*/ */
protected sanitizeFieldName(field: string): string { protected sanitizeFieldName(field: string): string {
// Solo permite caracteres alfanuméricos y guiones bajos
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
return this.config.defaultSortField || 'created_at'; return this.config.defaultSortField || 'created_at';
} }
return field; return field;
} }
/**
* Construye un INSERT dinámico
*/
protected buildInsertQuery(
data: Record<string, any>,
additionalFields: Record<string, any> = {}
): { 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<string, any>
): { 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 * Redondea a N decimales
*/ */