From 3f0a1e544155ded7d6c124303c7f58e1fc99dc8c Mon Sep 17 00:00:00 2001 From: enlineawork Date: Tue, 8 Sep 2026 07:39:38 -0300 Subject: [PATCH] refactor(f4): remove legacy inspection lifecycle and public title/end date --- .../inspection-visits.service.ts | 211 +----------------- 1 file changed, 3 insertions(+), 208 deletions(-) diff --git a/api-v3/src/inspection-visits/inspection-visits.service.ts b/api-v3/src/inspection-visits/inspection-visits.service.ts index 8bb29d6..b4208b7 100644 --- a/api-v3/src/inspection-visits/inspection-visits.service.ts +++ b/api-v3/src/inspection-visits/inspection-visits.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, ConflictException, - ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -18,14 +17,11 @@ import { InspectionVisitAssetPlanningSource, InspectionVisitStatus, } from '../database/entities'; -import type { ChangeInspectionVisitStatusDto } from './dto/change-inspection-visit-status.dto'; -import type { CloseInspectionVisitDto } from './dto/close-inspection-visit.dto'; import type { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto'; import type { ListInspectionVisitsQueryDto } from './dto/list-inspection-visits-query.dto'; import type { ReplaceInspectionVisitAssetsDto } from './dto/replace-inspection-visit-assets.dto'; import type { ReplaceInspectionVisitTeamDto } from './dto/replace-inspection-visit-team.dto'; import type { UpdateInspectionVisitDto } from './dto/update-inspection-visit.dto'; -import { assertMobileInspector } from '../inspection-operations/mobile-inspector-policy'; import { nextInspectionVisitCode } from './inspection-visit-code'; export interface InspectionPerson { @@ -91,7 +87,6 @@ export interface InspectionVisitChecklistSummary { export interface InspectionVisitListItem { id: string; code: string; - title: string; objective: string | null; status: InspectionVisitStatus; scopeAsset: InspectionAssetSummary | null; @@ -99,7 +94,6 @@ export interface InspectionVisitListItem { operatorCompany: InspectionPlanningContextAsset | null; leadInspector: InspectionPerson | null; plannedStartAt: Date | null; - plannedEndAt: Date | null; actualStartedAt: Date | null; actualClosedAt: Date | null; instructions: string | null; @@ -165,7 +159,7 @@ export class InspectionVisitsService { }; if (query.search?.trim()) { const search = add(`%${query.search.trim()}%`); - conditions.push(`(visit.code ILIKE ${search} OR visit.title ILIKE ${search})`); + conditions.push(`visit.code ILIKE ${search}`); } if (query.status) conditions.push(`visit.status = ${add(query.status)}`); if (query.companyId) { @@ -324,6 +318,8 @@ export class InspectionVisitsService { const code = await nextInspectionVisitCode(manager, plannedStartAt); const visit = manager.getRepository(InspectionVisit).create({ code, + // Compatibilidad de persistencia temporal: estas dos columnas legacy se + // conservan hasta eliminar sus últimas lecturas históricas del repositorio. title: code, objective: null, status: InspectionVisitStatus.DRAFT, @@ -715,209 +711,11 @@ export class InspectionVisitsService { }); } - async changeStatus( - id: string, - dto: ChangeInspectionVisitStatusDto, - principal: AuthPrincipal, - request: RequestWithContext, - ): Promise { - return this.dataSource.transaction(async (manager) => { - const visit = await this.lockVisit(manager, id); - if (visit.status === dto.status) return this.loadView(manager, id); - const cancellation = dto.status === InspectionVisitStatus.CANCELLED; - const reversiblePlanning = - (visit.status === InspectionVisitStatus.DRAFT && dto.status === InspectionVisitStatus.PLANNED) || - (visit.status === InspectionVisitStatus.PLANNED && dto.status === InspectionVisitStatus.DRAFT); - const cancellable = cancellation && [ - InspectionVisitStatus.DRAFT, - InspectionVisitStatus.PLANNED, - InspectionVisitStatus.IN_PROGRESS, - ].includes(visit.status); - if (!reversiblePlanning && !cancellable) { - throw new ConflictException({ - code: 'INSPECTION_VISIT_INVALID_TRANSITION', - message: 'La transición solicitada no está permitida en esta fase', - }); - } - if (dto.status === InspectionVisitStatus.PLANNED) { - await this.validatePlan(manager, visit); - } - const reason = dto.reason?.trim() || null; - if (cancellation && (!reason || reason.length < 10)) { - throw new BadRequestException({ - code: 'INSPECTION_CANCELLATION_REASON_REQUIRED', - message: 'Indicá un motivo de cancelación de al menos 10 caracteres', - }); - } - const beforeStatus = visit.status; - visit.status = dto.status; - visit.cancellationReason = cancellation ? reason : null; - visit.updatedBy = principal.userId; - await manager.getRepository(InspectionVisit).save(visit); - const updated = await this.loadView(manager, id); - await this.audit.record({ - ...administrationAuditContext(principal, request), - action: AuditAction.INSPECTION_VISIT_STATUS_CHANGED, - entityType: 'inspection_visit', - entityId: id, - beforeData: { status: beforeStatus }, - afterData: { status: dto.status, cancellationReason: visit.cancellationReason }, - }, manager); - return updated; - }); - } - - async start( - id: string, - principal: AuthPrincipal, - request: RequestWithContext, - ): Promise { - assertMobileInspector(principal); - return this.dataSource.transaction(async (manager) => { - const visit = await this.lockVisit(manager, id); - if (visit.status !== InspectionVisitStatus.PLANNED) { - throw new ConflictException({ - code: 'INSPECTION_VISIT_NOT_PLANNED', - message: 'La visita debe estar planificada antes de iniciarse', - }); - } - await this.validatePlan(manager, visit); - if (!principal.permissions.includes('inspections.manage')) { - const memberIds = await this.activeMemberIds(manager, id); - if (!memberIds.includes(principal.userId)) { - throw new ForbiddenException({ - code: 'INSPECTION_VISIT_NOT_ASSIGNED', - message: 'La visita no está asignada al usuario actual', - }); - } - } - visit.status = InspectionVisitStatus.IN_PROGRESS; - visit.actualStartedAt = new Date(); - visit.updatedBy = principal.userId; - await manager.getRepository(InspectionVisit).save(visit); - const updated = await this.loadView(manager, id); - await this.audit.record({ - ...administrationAuditContext(principal, request), - action: AuditAction.INSPECTION_VISIT_STARTED, - entityType: 'inspection_visit', - entityId: id, - beforeData: { status: InspectionVisitStatus.PLANNED, actualStartedAt: null }, - afterData: { - status: InspectionVisitStatus.IN_PROGRESS, - actualStartedAt: visit.actualStartedAt, - }, - }, manager); - return updated; - }); - } - - - async close( - id: string, - dto: CloseInspectionVisitDto, - principal: AuthPrincipal, - request: RequestWithContext, - ): Promise { - assertMobileInspector(principal); - return this.dataSource.transaction(async (manager) => { - const visit = await this.lockVisit(manager, id); - if (visit.status !== InspectionVisitStatus.IN_PROGRESS) { - throw new ConflictException({ - code: 'INSPECTION_VISIT_NOT_IN_PROGRESS', - message: 'La inspección debe estar en curso para cerrarse', - }); - } - if (!principal.permissions.includes('inspections.manage')) { - const memberIds = await this.activeMemberIds(manager, id); - if (!memberIds.includes(principal.userId)) { - throw new ForbiddenException({ - code: 'INSPECTION_VISIT_NOT_ASSIGNED', - message: 'La inspección no está asignada al usuario actual', - }); - } - } - const [acts] = (await manager.query(` - SELECT - COUNT(*) FILTER (WHERE act.status = 'DRAFT')::integer AS drafts, - COUNT(*) FILTER ( - WHERE act.status = 'READY' - AND NOT EXISTS ( - SELECT 1 FROM inspection_act_signatures signature - WHERE signature.act_id = act.id - AND signature.signer_type = 'INSPECTOR' - AND signature.status = 'SIGNED' - ) - )::integer AS "readyWithoutInspector", - COUNT(*) FILTER ( - WHERE act.status = 'READY' - AND NOT EXISTS ( - SELECT 1 FROM inspection_act_signatures signature - WHERE signature.act_id = act.id - AND signature.signer_type = 'COMPANY_RESPONSIBLE' - ) - )::integer AS "pendingCompany" - FROM inspection_acts act - WHERE act.visit_id = $1 - AND act.status <> 'CANCELLED' - `, [id])) as Array<{ drafts: number; readyWithoutInspector: number; pendingCompany: number }>; - if (Number(acts?.drafts ?? 0) > 0) { - throw new ConflictException({ - code: 'INSPECTION_VISIT_DRAFT_ACTS_PENDING', - message: 'Prepará o cancelá las actas en borrador antes de cerrar la inspección', - }); - } - if (Number(acts?.readyWithoutInspector ?? 0) > 0) { - throw new ConflictException({ - code: 'INSPECTION_VISIT_INSPECTOR_SIGNATURE_PENDING', - message: 'Toda acta preparada debe tener firma de inspector antes de cerrar la inspección', - }); - } - const serverClosedAt = new Date(); - const clientClosedAt = new Date(dto.clientClosedAt); - if (visit.actualStartedAt && clientClosedAt.getTime() < visit.actualStartedAt.getTime()) { - throw new BadRequestException({ - code: 'INSPECTION_VISIT_INVALID_CLOSE_TIME', - message: 'La fecha de cierre del dispositivo no puede ser anterior al inicio de la inspección', - }); - } - if (clientClosedAt.getTime() > serverClosedAt.getTime() + 24 * 60 * 60 * 1000) { - throw new BadRequestException({ - code: 'INSPECTION_VISIT_INVALID_DEVICE_TIME', - message: 'La fecha informada por el dispositivo no puede estar más de 24 horas en el futuro', - }); - } - visit.status = InspectionVisitStatus.CLOSED; - visit.actualClosedAt = serverClosedAt; - visit.updatedBy = principal.userId; - await manager.getRepository(InspectionVisit).save(visit); - const updated = await this.loadView(manager, id); - await this.audit.record({ - ...administrationAuditContext(principal, request), - action: AuditAction.INSPECTION_VISIT_STATUS_CHANGED, - entityType: 'inspection_visit', - entityId: id, - beforeData: { status: InspectionVisitStatus.IN_PROGRESS }, - afterData: { - status: InspectionVisitStatus.CLOSED, - clientClosedAt: clientClosedAt.toISOString(), - serverClosedAt: serverClosedAt.toISOString(), - pendingCompanySignatures: Number(acts?.pendingCompany ?? 0), - }, - metadata: { - closeSource: 'ANDROID', - actsMayCompleteCompanySignatureLater: true, - }, - }, manager); - return updated; - }); - } - private visitSelect(where: string): string { return ` SELECT visit.id, visit.code, - visit.title, visit.objective, visit.status, CASE WHEN scope.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT( @@ -943,7 +741,6 @@ export class InspectionVisitsService { 'lastName', lead.last_name ) END AS "leadInspector", visit.planned_start_at AS "plannedStartAt", - visit.planned_end_at AS "plannedEndAt", visit.actual_started_at AS "actualStartedAt", visit.actual_closed_at AS "actualClosedAt", visit.instructions, @@ -1615,7 +1412,6 @@ export class InspectionVisitsService { private auditView(visit: InspectionVisitView): Record { return { code: visit.code, - title: visit.title, objective: visit.objective, status: visit.status, scopeAssetId: visit.scopeAsset?.id ?? null, @@ -1623,7 +1419,6 @@ export class InspectionVisitsService { operatorCompanyId: visit.operatorCompany?.id ?? null, leadInspectorUserId: visit.leadInspector?.id ?? null, plannedStartAt: visit.plannedStartAt, - plannedEndAt: visit.plannedEndAt, actualStartedAt: visit.actualStartedAt, actualClosedAt: visit.actualClosedAt, instructions: visit.instructions,