Files
dh-inspeccion-v2/api-v3/src/inspection-visits/f4-inspection-visits.service.ts
T
admin 7be5d89204 F6.1 hotfix · jerarquía de Inspecciones y scope Yacimiento (#37)
Corrige creación de Inspección, separa Departamento→Área→Yacimiento, elimina el UPDATE oculto de checklist en lectura, integra Preparación para campo como etapa de Inspección, limita operaciones Android al Yacimiento, preserva scope en edición, auto-incluye Inventarios accionables, valida Operadora a plannedStartAt y recompila Android 0.15.2/vc24 con el ícono exacto de presentación.
2026-09-10 16:06:48 -03:00

166 lines
6.5 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { AuditService } from '../audit/audit.service';
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
import type { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto';
import type { ListInspectionVisitsQueryDto } from './dto/list-inspection-visits-query.dto';
import type {
InspectionChecklistItemKind,
InspectionVisitView,
} from './inspection-visits.service';
import { InspectionVisitsService } from './inspection-visits.service';
type F4ChecklistClassification = {
findingId: string;
itemKind: InspectionChecklistItemKind;
referenceOn: string | null;
};
/**
* Capa activa F4 sobre el servicio de Inspecciones.
*
* El checklist persistido es una generación histórica append-only. F4 necesita
* mostrar la clasificación vigente de controles, pero una lectura nunca debe
* reescribir esa generación. La clasificación se deriva con SELECT y se aplica
* sólo a la vista devuelta por la API.
*/
@Injectable()
export class F4InspectionVisitsService extends InspectionVisitsService {
constructor(
private readonly f4DataSource: DataSource,
audit: AuditService,
) {
super(f4DataSource, audit);
}
override async list(query: ListInspectionVisitsQueryDto) {
const response = await super.list(query);
return {
...response,
data: response.data.map((visit) => this.normalizeChecklist(visit)),
};
}
override async getById(id: string): Promise<InspectionVisitView> {
const visit = await super.getById(id);
return this.normalizeChecklist(await this.classifyChecklistView(visit));
}
override async create(
dto: CreateInspectionVisitDto,
principal: AuthPrincipal,
request: RequestWithContext,
): Promise<InspectionVisitView> {
const visit = await super.create(dto, principal, request);
return this.normalizeChecklist(await this.classifyChecklistView(visit));
}
override async generateChecklist(
id: string,
principal: AuthPrincipal,
request: RequestWithContext,
): Promise<InspectionVisitView> {
const visit = await super.generateChecklist(id, principal, request);
return this.normalizeChecklist(await this.classifyChecklistView(visit));
}
private normalizeChecklist<T>(visit: T): T {
if (!visit || typeof visit !== 'object' || !('checklist' in visit)) return visit;
const view = visit as T & InspectionVisitView;
const items = view.checklist.items.filter((item) => item.itemKind !== 'COMPANY_OVERDUE');
const actionableAssetIds = new Set(
items
.filter((item) => ['VERIFICATION_OVERDUE', 'UPCOMING_CONTROL'].includes(item.itemKind))
.map((item) => item.asset.id),
);
view.checklist = {
...view.checklist,
antecedents: items.filter((item) => item.itemKind === 'ANTECEDENT').length,
companyOverdue: 0,
verificationOverdue: items.filter((item) => item.itemKind === 'VERIFICATION_OVERDUE').length,
upcomingControls: items.filter((item) => item.itemKind === 'UPCOMING_CONTROL').length,
actionableAssets: actionableAssetIds.size,
items,
};
return view;
}
/**
* Deriva el estado F4 actual de los ítems sin mutar la generación persistida.
* La fuente administrativa de empresa vive a nivel Acta; en este checklist de
* campo sólo importan próximos controles y verificaciones no resueltas.
*/
private async classifyChecklistView(visit: InspectionVisitView): Promise<InspectionVisitView> {
if (!visit.plannedStartAt || visit.checklist.items.length === 0) return visit;
const findingIds = [...new Set(visit.checklist.items.map((item) => item.findingId))];
if (findingIds.length === 0) return visit;
const targetDate = new Date(visit.plannedStartAt).toISOString().slice(0, 10);
const classifications = (await this.f4DataSource.query(`
SELECT
finding.id AS "findingId",
CASE
WHEN finding.status='OPEN'
AND finding.next_control_on IS NOT NULL
AND finding.next_control_on < $2::date
AND COALESCE((
SELECT verification.outcome
FROM inspection_finding_verification_visits verification
WHERE verification.finding_id=finding.id
AND verification.outcome IS NOT NULL
ORDER BY verification.result_recorded_at DESC NULLS LAST,
verification.created_at DESC,
verification.id DESC
LIMIT 1
), '') <> 'RESOLVED'
THEN 'VERIFICATION_OVERDUE'
WHEN finding.status='OPEN'
AND finding.next_control_on IS NOT NULL
AND finding.next_control_on BETWEEN $2::date AND ($2::date + 30)
AND COALESCE((
SELECT verification.outcome
FROM inspection_finding_verification_visits verification
WHERE verification.finding_id=finding.id
AND verification.outcome IS NOT NULL
ORDER BY verification.result_recorded_at DESC NULLS LAST,
verification.created_at DESC,
verification.id DESC
LIMIT 1
), '') <> 'RESOLVED'
THEN 'UPCOMING_CONTROL'
ELSE 'ANTECEDENT'
END AS "itemKind",
CASE
WHEN finding.status='OPEN'
AND finding.next_control_on IS NOT NULL
AND COALESCE((
SELECT verification.outcome
FROM inspection_finding_verification_visits verification
WHERE verification.finding_id=finding.id
AND verification.outcome IS NOT NULL
ORDER BY verification.result_recorded_at DESC NULLS LAST,
verification.created_at DESC,
verification.id DESC
LIMIT 1
), '') <> 'RESOLVED'
THEN finding.next_control_on::text
ELSE NULL
END AS "referenceOn"
FROM inspection_findings finding
WHERE finding.id=ANY($1::uuid[])
`, [findingIds, targetDate])) as F4ChecklistClassification[];
const byFinding = new Map(classifications.map((item) => [item.findingId, item]));
visit.checklist = {
...visit.checklist,
items: visit.checklist.items.map((item) => {
const classification = byFinding.get(item.findingId);
return classification
? { ...item, itemKind: classification.itemKind, referenceOn: classification.referenceOn }
: item;
}),
};
return visit;
}
}