fix(inventory): make guided creation area-owned and support companies
This commit is contained in:
@@ -36,18 +36,17 @@ type ParentRow = {
|
||||
code: string;
|
||||
name: string;
|
||||
typeCode: string;
|
||||
operationalAreaId: string | null;
|
||||
operatorCompanyId: string | null;
|
||||
inventoryFamilyId: string | null;
|
||||
};
|
||||
|
||||
const TYPE_CODE_BY_KIND: Record<InventoryStructureKind, string> = {
|
||||
const TYPE_CODE_BY_KIND: Record<Exclude<InventoryStructureKind, 'EMPRESA'>, string> = {
|
||||
AREA: 'area',
|
||||
YACIMIENTO: 'yacimiento',
|
||||
INSTALACION: 'instalacion',
|
||||
SUBINSTALACION: 'subinstalacion',
|
||||
};
|
||||
const PARENT_TYPE_BY_KIND: Record<InventoryStructureKind, string | null> = {
|
||||
EMPRESA: null,
|
||||
AREA: null,
|
||||
YACIMIENTO: 'area',
|
||||
INSTALACION: 'yacimiento',
|
||||
@@ -70,18 +69,29 @@ export class InventoryStructureService {
|
||||
const types = (await this.dataSource.query(`
|
||||
SELECT id,code,name
|
||||
FROM asset_types
|
||||
WHERE lower(code) IN ('area','yacimiento','instalacion','subinstalacion')
|
||||
AND is_active=true
|
||||
ORDER BY CASE lower(code)
|
||||
WHEN 'area' THEN 1 WHEN 'yacimiento' THEN 2
|
||||
WHEN 'instalacion' THEN 3 WHEN 'subinstalacion' THEN 4 ELSE 9 END
|
||||
WHERE (
|
||||
lower(code) IN ('area','yacimiento','instalacion','subinstalacion')
|
||||
OR operational_role='COMPANY'
|
||||
) AND is_active=true
|
||||
ORDER BY CASE
|
||||
WHEN operational_role='COMPANY' THEN 0
|
||||
WHEN lower(code)='area' THEN 1
|
||||
WHEN lower(code)='yacimiento' THEN 2
|
||||
WHEN lower(code)='instalacion' THEN 3
|
||||
WHEN lower(code)='subinstalacion' THEN 4 ELSE 9 END
|
||||
`)) as StructureTypeRow[];
|
||||
if (types.length !== 4) {
|
||||
const company = types.find((item) => ['empresa','organizacion'].includes(item.code.toLowerCase()));
|
||||
const area = types.find((item) => item.code.toLowerCase()==='area');
|
||||
const yacimiento = types.find((item) => item.code.toLowerCase()==='yacimiento');
|
||||
const instalacion = types.find((item) => item.code.toLowerCase()==='instalacion');
|
||||
const subinstalacion = types.find((item) => item.code.toLowerCase()==='subinstalacion');
|
||||
if (!company || !area || !yacimiento || !instalacion || !subinstalacion) {
|
||||
throw new ConflictException({
|
||||
code: 'INVENTORY_STRUCTURE_TYPES_INCOMPLETE',
|
||||
message: 'La estructura del Inventario todavía no está completamente configurada',
|
||||
message: 'La configuración maestra de Empresa e Inventario todavía no está completa',
|
||||
});
|
||||
}
|
||||
|
||||
const families = (await this.dataSource.query(`
|
||||
SELECT family.id,family.code,family.name,family.level,
|
||||
family.legacy_type_code AS "legacyTypeCode",
|
||||
@@ -93,12 +103,16 @@ export class InventoryStructureService {
|
||||
WHERE family.is_active=true
|
||||
ORDER BY family.level,family.name,family.code
|
||||
`)) as FamilyRow[];
|
||||
|
||||
return {
|
||||
independentMasters: [
|
||||
{ kind: 'EMPRESA', label: 'Empresa', type: company, parentKind: null, requiresFamily: false },
|
||||
],
|
||||
levels: [
|
||||
{ kind: 'AREA', label: 'Área', type: types.find((item) => item.code.toLowerCase()==='area'), parentKind: null, requiresFamily: false },
|
||||
{ kind: 'YACIMIENTO', label: 'Yacimiento', type: types.find((item) => item.code.toLowerCase()==='yacimiento'), parentKind: 'AREA', requiresFamily: false },
|
||||
{ kind: 'INSTALACION', label: 'Instalación', type: types.find((item) => item.code.toLowerCase()==='instalacion'), parentKind: 'YACIMIENTO', requiresFamily: true },
|
||||
{ kind: 'SUBINSTALACION', label: 'Subinstalación', type: types.find((item) => item.code.toLowerCase()==='subinstalacion'), parentKind: 'INSTALACION', requiresFamily: true },
|
||||
{ kind: 'AREA', label: 'Área', type: area, parentKind: null, requiresFamily: false },
|
||||
{ kind: 'YACIMIENTO', label: 'Yacimiento', type: yacimiento, parentKind: 'AREA', requiresFamily: false },
|
||||
{ kind: 'INSTALACION', label: 'Instalación', type: instalacion, parentKind: 'YACIMIENTO', requiresFamily: true },
|
||||
{ kind: 'SUBINSTALACION', label: 'Subinstalación', type: subinstalacion, parentKind: 'INSTALACION', requiresFamily: true },
|
||||
],
|
||||
installationFamilies: families.filter((item) => item.level==='INSTALLATION'),
|
||||
subinstallationFamilies: families.filter((item) => item.level==='SUBINSTALLATION'),
|
||||
@@ -107,7 +121,7 @@ export class InventoryStructureService {
|
||||
|
||||
async parents(kindValue: string, search?: string) {
|
||||
const kind = kindValue.toUpperCase() as InventoryStructureKind;
|
||||
if (!(kind in PARENT_TYPE_BY_KIND) || kind === 'AREA') {
|
||||
if (!(kind in PARENT_TYPE_BY_KIND) || kind === 'AREA' || kind === 'EMPRESA') {
|
||||
throw new BadRequestException({
|
||||
code: 'INVENTORY_STRUCTURE_PARENT_KIND_INVALID',
|
||||
message: 'El nivel indicado no requiere un registro padre',
|
||||
@@ -154,9 +168,8 @@ export class InventoryStructureService {
|
||||
const type = await this.requireStructureType(manager, dto.kind);
|
||||
const parent = await this.requireParent(manager, dto.kind, dto.parentId ?? null);
|
||||
const family = await this.requireFamily(manager, dto.kind, dto.familyId ?? null, parent);
|
||||
const code = dto.code?.trim().toUpperCase() || this.generatedCode(dto.kind, dto.name);
|
||||
const operationalAreaId = parent?.operationalAreaId ?? null;
|
||||
const operatorCompanyId = parent?.operatorCompanyId ?? null;
|
||||
const generatedCode = dto.code?.trim().toUpperCase() || this.generatedCode(dto.kind, dto.name);
|
||||
const operationalAreaId = parent ? await this.resolveAreaId(manager, parent) : null;
|
||||
|
||||
const inserted = (await manager.query(`
|
||||
INSERT INTO assets (
|
||||
@@ -164,30 +177,37 @@ export class InventoryStructureService {
|
||||
code,name,common_name,description,information_status,operational_status,
|
||||
data_origin,source_name,source_reference,source_notes,created_by,updated_by,provenance_updated_by
|
||||
) VALUES (
|
||||
$1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,
|
||||
$6::varchar,$7::varchar,$8::varchar,$9::text,$10::asset_information_status,$11::asset_operational_status,
|
||||
$12::varchar,$13::varchar,$14::varchar,$15::text,$16::uuid,$16::uuid,$16::uuid
|
||||
$1::uuid,$2::uuid,$3::uuid,NULL,$4::uuid,
|
||||
$5::varchar,$6::varchar,$7::varchar,$8::text,$9::asset_information_status,$10::asset_operational_status,
|
||||
$11::varchar,$12::varchar,$13::varchar,$14::text,$15::uuid,$15::uuid,$15::uuid
|
||||
) RETURNING id
|
||||
`, [
|
||||
type.id,
|
||||
parent?.id ?? null,
|
||||
operationalAreaId,
|
||||
operatorCompanyId,
|
||||
family?.id ?? null,
|
||||
code,
|
||||
generatedCode,
|
||||
dto.name,
|
||||
dto.commonName ?? null,
|
||||
dto.description ?? null,
|
||||
AssetInformationStatus.DRAFT,
|
||||
AssetOperationalStatus.UNKNOWN,
|
||||
AssetDataOrigin.MANUAL,
|
||||
'Inventario estructural F3.1',
|
||||
`inventory-structure:${dto.kind.toLowerCase()}`,
|
||||
dto.kind === 'EMPRESA' ? 'Maestro de Empresas F5' : 'Estructura de Inventario F5',
|
||||
dto.kind === 'EMPRESA' ? 'inventory-master:empresa' : `inventory-structure:${dto.kind.toLowerCase()}`,
|
||||
family ? `Familia técnica: ${family.code} · ${family.name}` : null,
|
||||
principal.userId,
|
||||
])) as Array<{ id: string }>;
|
||||
const id = inserted[0]?.id;
|
||||
if (!id) throw new Error('No se pudo crear el registro estructural');
|
||||
if (!id) throw new Error('No se pudo crear el registro');
|
||||
|
||||
if (dto.kind === 'EMPRESA') {
|
||||
await manager.query(`
|
||||
INSERT INTO organization_profiles(asset_id,organization_kind,legal_name,updated_by)
|
||||
VALUES ($1::uuid,'COMPANY',$2,$3::uuid)
|
||||
ON CONFLICT (asset_id) DO UPDATE SET legal_name=EXCLUDED.legal_name,updated_by=EXCLUDED.updated_by,updated_at=CURRENT_TIMESTAMP
|
||||
`,[id,dto.name,principal.userId]);
|
||||
}
|
||||
|
||||
const versionNumber = await this.history.capture(
|
||||
manager,
|
||||
@@ -200,13 +220,12 @@ export class InventoryStructureService {
|
||||
INSERT INTO asset_context_history (
|
||||
asset_id,parent_id,operational_area_id,operator_company_id,valid_from,
|
||||
change_reason,asset_version_number,source,request_id,created_by
|
||||
) VALUES ($1,$2,$3,$4,CURRENT_TIMESTAMP,$5,$6,'WEB',$7,$8)
|
||||
) VALUES ($1,$2,$3,NULL,CURRENT_TIMESTAMP,$4,$5,'WEB',$6,$7)
|
||||
`, [
|
||||
id,
|
||||
parent?.id ?? null,
|
||||
operationalAreaId,
|
||||
operatorCompanyId,
|
||||
'Alta guiada de Inventario estructural F3.1',
|
||||
dto.kind === 'EMPRESA' ? 'Alta guiada de Empresa independiente F5' : 'Alta guiada de estructura de Inventario F5',
|
||||
versionNumber,
|
||||
request.requestId,
|
||||
principal.userId,
|
||||
@@ -223,6 +242,7 @@ export class InventoryStructureService {
|
||||
inventoryStructureKind: dto.kind,
|
||||
inventoryFamilyId: family?.id ?? null,
|
||||
inventoryFamilyCode: family?.code ?? null,
|
||||
operatorOwnership: false,
|
||||
},
|
||||
}, manager);
|
||||
return created;
|
||||
@@ -231,7 +251,7 @@ export class InventoryStructureService {
|
||||
if (isUniqueViolation(error)) {
|
||||
throw new ConflictException({
|
||||
code: 'ASSET_CODE_ALREADY_EXISTS',
|
||||
message: 'Ya existe un registro de Inventario con ese código',
|
||||
message: 'Ya existe un registro con ese código',
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
@@ -239,10 +259,11 @@ export class InventoryStructureService {
|
||||
}
|
||||
|
||||
private async requireStructureType(manager: EntityManager, kind: InventoryStructureKind): Promise<StructureTypeRow> {
|
||||
const rows = (await manager.query(`
|
||||
SELECT id,code,name FROM asset_types
|
||||
WHERE lower(code)=lower($1::text) AND is_active=true LIMIT 1
|
||||
`, [TYPE_CODE_BY_KIND[kind]])) as StructureTypeRow[];
|
||||
const sql = kind === 'EMPRESA'
|
||||
? `SELECT id,code,name FROM asset_types WHERE operational_role='COMPANY' AND is_active=true ORDER BY (lower(code)='empresa') DESC,created_at LIMIT 1`
|
||||
: `SELECT id,code,name FROM asset_types WHERE lower(code)=lower($1::text) AND is_active=true LIMIT 1`;
|
||||
const params = kind === 'EMPRESA' ? [] : [TYPE_CODE_BY_KIND[kind as Exclude<InventoryStructureKind,'EMPRESA'>]];
|
||||
const rows = (await manager.query(sql,params)) as StructureTypeRow[];
|
||||
if (!rows[0]) {
|
||||
throw new ConflictException({
|
||||
code: 'INVENTORY_STRUCTURE_TYPE_NOT_CONFIGURED',
|
||||
@@ -261,8 +282,10 @@ export class InventoryStructureService {
|
||||
if (!expectedType) {
|
||||
if (parentId) {
|
||||
throw new BadRequestException({
|
||||
code: 'INVENTORY_AREA_MUST_BE_ROOT',
|
||||
message: 'Un Área se crea como registro raíz y no puede tener padre',
|
||||
code: 'INVENTORY_ROOT_MUST_NOT_HAVE_PARENT',
|
||||
message: kind === 'EMPRESA'
|
||||
? 'Una Empresa es un maestro independiente y no puede tener padre'
|
||||
: 'Un Área es un registro raíz y no puede tener padre',
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -275,8 +298,6 @@ export class InventoryStructureService {
|
||||
}
|
||||
const rows = (await manager.query(`
|
||||
SELECT asset.id,asset.code,asset.name,type.code AS "typeCode",
|
||||
asset.operational_area_id AS "operationalAreaId",
|
||||
asset.operator_company_id AS "operatorCompanyId",
|
||||
asset.inventory_family_id AS "inventoryFamilyId"
|
||||
FROM assets asset
|
||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
||||
@@ -294,6 +315,30 @@ export class InventoryStructureService {
|
||||
return parent;
|
||||
}
|
||||
|
||||
private async resolveAreaId(manager: EntityManager,parent: ParentRow):Promise<string> {
|
||||
if (parent.typeCode.toLowerCase()==='area') return parent.id;
|
||||
const rows = (await manager.query(`
|
||||
WITH RECURSIVE lineage AS (
|
||||
SELECT asset.id,asset.parent_id,asset.asset_type_id FROM assets asset WHERE asset.id=$1::uuid
|
||||
UNION ALL
|
||||
SELECT parent.id,parent.parent_id,parent.asset_type_id
|
||||
FROM assets parent JOIN lineage child ON child.parent_id=parent.id
|
||||
)
|
||||
SELECT lineage.id
|
||||
FROM lineage JOIN asset_types type ON type.id=lineage.asset_type_id
|
||||
WHERE type.operational_role='AREA'
|
||||
LIMIT 1
|
||||
`,[parent.id])) as IdRow[];
|
||||
const areaId=rows[0]?.id;
|
||||
if (!areaId) {
|
||||
throw new ConflictException({
|
||||
code:'INVENTORY_STRUCTURE_AREA_ANCESTOR_MISSING',
|
||||
message:'La ubicación seleccionada no pertenece a un Área válida',
|
||||
});
|
||||
}
|
||||
return areaId;
|
||||
}
|
||||
|
||||
private async requireFamily(
|
||||
manager: EntityManager,
|
||||
kind: InventoryStructureKind,
|
||||
@@ -304,7 +349,7 @@ export class InventoryStructureService {
|
||||
if (!expectedLevel) {
|
||||
if (familyId) throw new BadRequestException({
|
||||
code: 'INVENTORY_STRUCTURE_FAMILY_NOT_ALLOWED',
|
||||
message: 'Área y Yacimiento no llevan familia técnica',
|
||||
message: 'Empresa, Área y Yacimiento no llevan familia técnica',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -339,7 +384,7 @@ export class InventoryStructureService {
|
||||
}
|
||||
|
||||
private generatedCode(kind: InventoryStructureKind, name: string): string {
|
||||
const prefix = kind === 'YACIMIENTO' ? 'YAC' : kind === 'INSTALACION' ? 'INST' : kind === 'SUBINSTALACION' ? 'SUB' : 'AREA';
|
||||
const prefix = kind === 'EMPRESA' ? 'EMP' : kind === 'YACIMIENTO' ? 'YAC' : kind === 'INSTALACION' ? 'INST' : kind === 'SUBINSTALACION' ? 'SUB' : 'AREA';
|
||||
const readable = name
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
@@ -356,6 +401,7 @@ export class InventoryStructureService {
|
||||
asset.information_status AS "informationStatus",asset.operational_status AS "operationalStatus",
|
||||
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
||||
CASE WHEN parent.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) END AS parent,
|
||||
CASE WHEN area.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',area.id,'code',area.code,'name',area.name) END AS "operationalArea",
|
||||
CASE WHEN family.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT(
|
||||
'id',family.id,'code',family.code,'name',family.name,'level',family.level,
|
||||
'informationLabels',family.information_labels
|
||||
@@ -364,9 +410,12 @@ export class InventoryStructureService {
|
||||
FROM assets asset
|
||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
||||
LEFT JOIN assets parent ON parent.id=asset.parent_id
|
||||
LEFT JOIN assets area ON area.id=asset.operational_area_id
|
||||
LEFT JOIN inventory_families family ON family.id=asset.inventory_family_id
|
||||
WHERE asset.id=$1::uuid
|
||||
`, [id]);
|
||||
return rows[0];
|
||||
}
|
||||
}
|
||||
|
||||
type IdRow = { id: string };
|
||||
|
||||
Reference in New Issue
Block a user