217 lines
9.5 KiB
TypeScript
217 lines
9.5 KiB
TypeScript
import 'reflect-metadata';
|
||
|
||
import { randomUUID } from 'node:crypto';
|
||
import { NestFactory } from '@nestjs/core';
|
||
import { DataSource } from 'typeorm';
|
||
import { AppModule } from '../app.module';
|
||
import { AssetsService } from '../asset-master/assets.service';
|
||
import { AssetOperationalRelationsService } from '../asset-master/asset-operational-relations.service';
|
||
import { AssetProvenanceService } from '../asset-master/asset-provenance.service';
|
||
import { AssetRegistryService } from '../asset-master/asset-registry.service';
|
||
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
|
||
import {
|
||
AreaOrganizationRole,
|
||
AssetDataOrigin,
|
||
AssetInformationStatus,
|
||
AssetOperationalStatus,
|
||
AssetSourceDocumentRelationType,
|
||
OrganizationKind,
|
||
SourceDocumentType,
|
||
} from '../database/entities';
|
||
|
||
const DEMO_PREFIX = 'DEV-DEMO-';
|
||
const SOURCE_REFERENCE = 'DH-DEV-DEMO:ITN124/2026';
|
||
|
||
function fakeRequest(): RequestWithContext {
|
||
return {
|
||
requestId: `dev-seed-${randomUUID()}`,
|
||
ip: '127.0.0.1',
|
||
socket: { remoteAddress: '127.0.0.1' },
|
||
header: () => 'dhv2-dev-seed',
|
||
} as unknown as RequestWithContext;
|
||
}
|
||
|
||
async function buildPrincipal(dataSource: DataSource): Promise<AuthPrincipal> {
|
||
const [user] = (await dataSource.query(`
|
||
SELECT id, username, first_name AS "firstName", last_name AS "lastName", email,
|
||
must_change_password AS "mustChangePassword"
|
||
FROM users
|
||
WHERE status = 'ACTIVE'
|
||
ORDER BY CASE WHEN username = 'admin' THEN 0 ELSE 1 END, created_at
|
||
LIMIT 1
|
||
`)) as Array<{ id: string; username: string; firstName: string; lastName: string; email: string | null; mustChangePassword: boolean }>;
|
||
if (!user) throw new Error('No hay un usuario activo para registrar la auditoría del demo');
|
||
const roles = (await dataSource.query(`
|
||
SELECT role.code FROM user_roles ur JOIN roles role ON role.id = ur.role_id WHERE ur.user_id = $1
|
||
`, [user.id])) as Array<{ code: string }>;
|
||
const permissions = (await dataSource.query(`
|
||
SELECT DISTINCT permission.code
|
||
FROM user_roles ur
|
||
JOIN role_permissions rp ON rp.role_id = ur.role_id
|
||
JOIN permissions permission ON permission.id = rp.permission_id
|
||
WHERE ur.user_id = $1
|
||
`, [user.id])) as Array<{ code: string }>;
|
||
return {
|
||
userId: user.id,
|
||
username: user.username,
|
||
sessionId: 'development-seed',
|
||
firstName: user.firstName,
|
||
lastName: user.lastName,
|
||
email: user.email,
|
||
mustChangePassword: user.mustChangePassword,
|
||
roles: roles.map((row) => row.code),
|
||
permissions: permissions.map((row) => row.code),
|
||
transport: 'cookie',
|
||
};
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
const app = await NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn'] });
|
||
try {
|
||
const dataSource = app.get(DataSource);
|
||
const assets = app.get(AssetsService);
|
||
const relations = app.get(AssetOperationalRelationsService);
|
||
const provenance = app.get(AssetProvenanceService);
|
||
const registry = app.get(AssetRegistryService);
|
||
const principal = await buildPrincipal(dataSource);
|
||
const request = fakeRequest();
|
||
|
||
const [existing] = (await dataSource.query(
|
||
`SELECT COUNT(*)::integer AS count FROM assets WHERE code LIKE $1 OR source_reference = $2`,
|
||
[`${DEMO_PREFIX}%`, SOURCE_REFERENCE],
|
||
)) as Array<{ count: number }>;
|
||
if (Number(existing?.count ?? 0) > 0) {
|
||
throw new Error('Ya existen datos DEV-DEMO. Ejecutá primero scripts/dev-clean-demo.sh si querés reconstruir el ejemplo.');
|
||
}
|
||
|
||
const typeRows = (await dataSource.query(`
|
||
SELECT id, code FROM asset_types WHERE code = ANY($1::varchar[]) AND is_active = true
|
||
`, [['area', 'empresa', 'planta', 'tanque']])) as Array<{ id: string; code: string }>;
|
||
const types = new Map(typeRows.map((row) => [row.code, row.id]));
|
||
const missing = ['area', 'empresa', 'planta', 'tanque'].filter((code) => !types.has(code));
|
||
if (missing.length) {
|
||
throw new Error(`Faltan tipos técnicos (${missing.join(', ')}). En Maestro > Tipos y atributos ejecutá “Completar catálogo técnico”.`);
|
||
}
|
||
|
||
const attributeRows = (await dataSource.query(`
|
||
SELECT d.id, d.code, t.code AS type_code
|
||
FROM asset_attribute_definitions d
|
||
JOIN asset_types t ON t.id = d.asset_type_id
|
||
WHERE t.code = 'tanque' AND d.is_active = true
|
||
`)) as Array<{ id: string; code: string; type_code: string }>;
|
||
const tankAttrs = new Map(attributeRows.map((row) => [row.code, row.id]));
|
||
|
||
const createAsset = async (input: {
|
||
code: string; name: string; typeCode: string; parentId?: string | null;
|
||
areaId?: string | null; companyId?: string | null; description: string;
|
||
attributes?: Record<string, unknown>;
|
||
}) => {
|
||
const created = await assets.create({
|
||
code: input.code,
|
||
name: input.name,
|
||
typeId: types.get(input.typeCode)!,
|
||
parentId: input.parentId ?? null,
|
||
operationalAreaId: input.areaId ?? null,
|
||
operatorCompanyId: input.companyId ?? null,
|
||
description: input.description,
|
||
informationStatus: AssetInformationStatus.DRAFT,
|
||
attributes: input.attributes ?? {},
|
||
}, principal, request);
|
||
await provenance.update(created.id, {
|
||
origin: AssetDataOrigin.PROVIDED_DOCUMENT,
|
||
sourceName: 'DH DEV DEMO · Informe Técnico Nº 124/2026',
|
||
sourceReference: SOURCE_REFERENCE,
|
||
observedAt: '2026-06-25T12:00:00-03:00',
|
||
notes: 'DATO DE PRUEBA. Caso construido a partir del informe técnico recibido para validar nomenclatura, jerarquía y flujo. No constituye carga maestra real ni validación de vigencia.',
|
||
}, principal, request);
|
||
return created;
|
||
};
|
||
|
||
const area = await createAsset({
|
||
code: 'DEV-DEMO-AREA-ATAMISQUI',
|
||
name: '[PRUEBA] Atamisqui',
|
||
typeCode: 'area',
|
||
description: 'Área usada exclusivamente como ejemplo de desarrollo a partir del Informe Técnico Nº 124/2026.',
|
||
});
|
||
const company = await createAsset({
|
||
code: 'DEV-DEMO-EMP-PS',
|
||
name: '[PRUEBA] Petróleos Sudamericanos Energy S.A.',
|
||
typeCode: 'empresa',
|
||
description: 'Organización de prueba para validar la relación Área–Operadora. No implica vigencia administrativa real.',
|
||
});
|
||
await registry.upsertOrganizationProfile(company.id, {
|
||
organizationKind: OrganizationKind.COMPANY,
|
||
legalName: 'Petróleos Sudamericanos Energy S.A.',
|
||
notes: 'Perfil de prueba DH DEV DEMO.',
|
||
}, principal, request);
|
||
const sourceDocument = await registry.createSourceDocument({
|
||
documentType: SourceDocumentType.TECHNICAL_REPORT,
|
||
documentNumber: 'IT 124/2026 · DH-DEV-DEMO',
|
||
title: 'DH DEV DEMO · Informe Técnico Nº 124/2026',
|
||
issuer: 'Dirección de Hidrocarburos · Gobierno de Mendoza',
|
||
documentDate: '2026-06-25',
|
||
externalReference: SOURCE_REFERENCE,
|
||
notes: 'Documento de referencia cargado exclusivamente para el caso de prueba del sistema.',
|
||
}, principal, request);
|
||
await relations.create({
|
||
areaId: area.id,
|
||
companyId: company.id,
|
||
relationRole: AreaOrganizationRole.OPERATOR,
|
||
sourceDocumentId: sourceDocument.id,
|
||
reason: 'Relación creada para el caso de prueba DH DEV DEMO basado en ITN 124/2026.',
|
||
}, principal, request);
|
||
|
||
const plant = await createAsset({
|
||
code: 'DEV-DEMO-PLANTA-3PB',
|
||
name: '[PRUEBA] Planta de Entrega 3 PB',
|
||
typeCode: 'planta',
|
||
parentId: area.id,
|
||
areaId: area.id,
|
||
companyId: company.id,
|
||
description: 'Instalación seleccionada como referencia en el Informe Técnico Nº 124/2026.',
|
||
});
|
||
|
||
const tankAttributes = (capacity: number): Record<string, unknown> => {
|
||
const values: Record<string, unknown> = {};
|
||
const capacityId = tankAttrs.get('capacidad_nominal');
|
||
const productId = tankAttrs.get('producto_servicio');
|
||
if (capacityId) values[capacityId] = capacity;
|
||
if (productId) values[productId] = 'Petróleo';
|
||
return values;
|
||
};
|
||
|
||
const tank57 = await createAsset({
|
||
code: 'DEV-DEMO-TK-57', name: '[PRUEBA] Tanque TK-57', typeCode: 'tanque',
|
||
parentId: plant.id, areaId: area.id, companyId: company.id,
|
||
description: 'Tanque de prueba citado en ITN 124/2026; capacidad informada 480 m³.',
|
||
attributes: tankAttributes(480),
|
||
});
|
||
const tank58 = await createAsset({
|
||
code: 'DEV-DEMO-TK-58', name: '[PRUEBA] Tanque TK-58', typeCode: 'tanque',
|
||
parentId: plant.id, areaId: area.id, companyId: company.id,
|
||
description: 'Tanque de prueba citado en ITN 124/2026; capacidad informada 320 m³.',
|
||
attributes: tankAttributes(320),
|
||
});
|
||
|
||
for (const assetId of [area.id, company.id, plant.id, tank57.id, tank58.id]) {
|
||
await registry.linkDocument(assetId, sourceDocument.id, { relationType: AssetSourceDocumentRelationType.SOURCE }, principal, request);
|
||
}
|
||
await assets.changeOperationalStatus(tank57.id, { status: AssetOperationalStatus.OUT_OF_SERVICE }, principal, request);
|
||
await assets.changeOperationalStatus(tank58.id, { status: AssetOperationalStatus.OUT_OF_SERVICE }, principal, request);
|
||
|
||
console.log('DEMO DH creado correctamente');
|
||
console.log('Área: [PRUEBA] Atamisqui');
|
||
console.log('Organización operadora: [PRUEBA] Petróleos Sudamericanos Energy S.A.');
|
||
console.log('Instalación: [PRUEBA] Planta de Entrega 3 PB');
|
||
console.log('Equipos: [PRUEBA] TK-57 (480 m³), TK-58 (320 m³)');
|
||
console.log(`Marca de limpieza: ${SOURCE_REFERENCE}`);
|
||
} finally {
|
||
await app.close();
|
||
}
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(error instanceof Error ? error.message : error);
|
||
process.exitCode = 1;
|
||
});
|