metis-desk/modules/assets/src/api/domain/value-objects/asset-values.vo.ts
David 4edc1268b4 Refactor asset and company management features
- Simplified asset assignment validation in SequelizeAssetRepository.
- Updated asset module to include legacy asset assignment migration.
- Enhanced inventory assignment logic to better handle asset-person relationships.
- Improved localization strings for asset guidance and scope.
- Removed person assignment display from asset detail view.
- Streamlined inventory page by removing person-related filters and displays.
- Adjusted asset form to eliminate person assignment input.
- Updated tests to reflect changes in asset and person handling.
- Added phone link component for better phone number formatting and linking.
- Enhanced person details view to include current workstations and improved layout.
- Refactored person avatar component to support children and improved rendering.
2026-10-10 01:37:10 +02:00

210 lines
7.1 KiB
TypeScript

import { Name, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { AssetCatalogError } from "../asset-definition";
import type { AssetCharacteristic, AssetInput, AssetSpecificationData } from "../asset-values";
import { AssetInputSchema, AssetSpecificationSchema } from "../asset-values";
import {
freezeTemplateSections,
sameTemplateSections,
validTemplateSections,
} from "../template-sections";
export class AssetSpecification {
private constructor(
private readonly data: AssetSpecificationData,
public readonly templateId: UniqueID,
public readonly name: Name
) {
Object.freeze(this);
}
toPrimitive() {
return this.data;
}
static create(input: AssetSpecificationData): Result<AssetSpecification, AssetCatalogError> {
const parsed = AssetSpecificationSchema.safeParse(input);
if (
!(parsed.success && validTemplateSections(input.sections ?? [], input.fields)) ||
new Set(input.fields.map((field) => field.fieldId.toLowerCase())).size !== input.fields.length
) {
return Result.fail(new AssetCatalogError("INVALID_REQUEST"));
}
const template = UniqueID.create(input.templateId);
const name = Name.create(input.templateName, 100);
if (!(template.isSuccess && name.isSuccess)) {
return Result.fail(new AssetCatalogError("INVALID_REQUEST"));
}
const frozenFields = input.fields.map((field) => {
const choices = [...field.choices];
Object.freeze(choices);
return Object.freeze({
...field,
fieldId: field.fieldId.toLowerCase(),
choices,
...(field.sectionId ? { sectionId: field.sectionId.toLowerCase() } : {}),
});
});
Object.freeze(frozenFields);
return Result.ok(
new AssetSpecification(
Object.freeze({
templateId: template.data.toPrimitive(),
templateName: name.data.toPrimitive(),
fields: frozenFields,
...(input.sections ? { sections: freezeTemplateSections(input.sections) } : {}),
}),
template.data,
name.data
)
);
}
equals(other: AssetSpecification | undefined) {
if (!(other instanceof AssetSpecification)) {
return false;
}
const first = this.toPrimitive();
const second = other.toPrimitive();
return (
first.templateId === second.templateId &&
first.templateName === second.templateName &&
sameTemplateSections(first.sections ?? [], second.sections ?? []) &&
first.fields.length === second.fields.length &&
first.fields.every((field, index) => {
const candidate = second.fields[index];
return (
candidate !== undefined &&
field.fieldId === candidate.fieldId &&
field.name === candidate.name &&
field.type === candidate.type &&
field.unit === candidate.unit &&
field.required === candidate.required &&
field.sectionId === candidate.sectionId &&
field.choices.length === candidate.choices.length &&
field.choices.every((choice, position) => choice === candidate.choices[position])
);
})
);
}
}
function validCharacteristic(field: AssetCharacteristic, value: unknown) {
switch (field.type) {
case "text":
return typeof value === "string";
case "number":
return typeof value === "number" && Number.isFinite(value);
case "boolean":
return typeof value === "boolean";
case "choice":
return typeof value === "string" && field.choices.includes(value);
case "date":
return z.iso.date().safeParse(value).success;
}
}
export class AssetValues {
private constructor(
private readonly data: AssetInput,
public readonly name: Name,
public readonly siteId: Maybe<UniqueID>,
public readonly brandId: Maybe<UniqueID>
) {
Object.freeze(this);
}
toPrimitive() {
return this.data;
}
static create(
input: AssetInput,
specification: AssetSpecification
): Result<AssetValues, AssetCatalogError> {
const parsed = AssetInputSchema.safeParse(input);
const name = Name.create(input.name, 100);
if (
!(parsed.success && name.isSuccess) ||
input.templateId.toLowerCase() !== specification.toPrimitive().templateId
) {
return Result.fail(new AssetCatalogError("INVALID_REQUEST"));
}
const data = parsed.data;
const site = optionalIdentity(data.siteId);
const parsedCharacteristics = normalizeCharacteristics(data.characteristics, specification);
if (!parsedCharacteristics.isSuccess) {
return parsedCharacteristics;
}
const characteristics = parsedCharacteristics.data;
return Result.ok(
new AssetValues(
Object.freeze({
...data,
name: name.data.toPrimitive(),
templateId: data.templateId.toLowerCase(),
siteId: data.siteId?.toLowerCase() ?? null,
...(data.brandId === undefined ? {} : { brandId: data.brandId?.toLowerCase() ?? null }),
notes: data.notes.trim(),
characteristics: Object.freeze(characteristics),
}),
name.data,
site,
optionalIdentity(data.brandId ?? null)
)
);
}
equals(other: AssetValues | undefined) {
if (!(other instanceof AssetValues)) {
return false;
}
const first = this.toPrimitive();
const second = other.toPrimitive();
return (
first.name === second.name &&
first.templateId === second.templateId &&
first.siteId === second.siteId &&
(first.brandId ?? null) === (second.brandId ?? null) &&
first.status === second.status &&
first.notes === second.notes &&
Object.keys(first.characteristics).length === Object.keys(second.characteristics).length &&
Object.entries(first.characteristics).every(
([key, value]) => value === second.characteristics[key]
)
);
}
}
function normalizeCharacteristics(
input: AssetInput["characteristics"],
specification: AssetSpecification
): Result<AssetInput["characteristics"], AssetCatalogError> {
const characteristics: AssetInput["characteristics"] = {};
for (const [key, raw] of Object.entries(input)) {
const field = specification
.toPrimitive()
.fields.find((item) => item.fieldId === key.toLowerCase());
const value = typeof raw === "string" ? raw.trim() : raw;
if (!field || key.toLowerCase() in characteristics || !validCharacteristic(field, value)) {
return Result.fail(new AssetCatalogError("INVALID_CHARACTERISTICS"));
}
if (value !== "") {
characteristics[key.toLowerCase()] = value;
}
}
if (
specification
.toPrimitive()
.fields.some((field) => field.required && !(field.fieldId in characteristics))
) {
return Result.fail(new AssetCatalogError("REQUIRED_CHARACTERISTICS"));
}
return Result.ok(characteristics);
}
function optionalIdentity(value: string | null): Maybe<UniqueID> {
if (value === null) {
return Maybe.none();
}
const parsed = UniqueID.create(value);
if (!parsed.isSuccess) {
throw new Error("Referencia validada inconsistente.");
}
return Maybe.some(parsed.data);
}