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 { 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, public readonly personId: Maybe, public readonly brandId: Maybe ) { Object.freeze(this); } toPrimitive() { return this.data; } static create( input: AssetInput, specification: AssetSpecification ): Result { 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 person = optionalIdentity(data.personId); const parsedCharacteristics = normalizeCharacteristics(data.characteristics, specification); if (!parsedCharacteristics.isSuccess) { return parsedCharacteristics; } const characteristics = parsedCharacteristics.data; if (data.status === "retired" && data.personId !== null) { return Result.fail(new AssetCatalogError("RETIRED_ASSIGNED")); } return Result.ok( new AssetValues( Object.freeze({ ...data, name: name.data.toPrimitive(), templateId: data.templateId.toLowerCase(), siteId: data.siteId?.toLowerCase() ?? null, personId: data.personId?.toLowerCase() ?? null, ...(data.brandId === undefined ? {} : { brandId: data.brandId?.toLowerCase() ?? null }), notes: data.notes.trim(), characteristics: Object.freeze(characteristics), }), name.data, site, person, 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.personId === second.personId && (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 { 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 { 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); }