import { MMReceiveLaborTicket } from "../services/mm-api-service/types/receive-types.js"; import { MMSendWorkOrderOperationLaborTicketSync, type MMSendWorkOrderOperationLaborTicketSyncPatch, } from "../services/mm-api-service/types/send-types.js"; import { MMWorkOrderOperationStatus, normalizeMMStatus, } from "../types/erp-types.js"; function parseCustomFieldsBag(v: unknown): Record { if (v == null) return {}; if (typeof v === "string") { try { const parsed = JSON.parse(v); return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? (parsed as Record) : {}; } catch { return {}; } } if (typeof v === "object" && !Array.isArray(v)) { return v as Record; } return {}; } function isValidClosedDate(closedDate: string | null | undefined): boolean { return ( typeof closedDate === "string" && closedDate !== "" && !Number.isNaN(Date.parse(closedDate)) ); } /** * Builds a partial work order operation upsert for MM when the merged labor ticket carries * a valid `workOrderOperation.closedDate` and/or non-empty WOO custom fields from NATS. * Returns null when neither is present (no MM update). Only identity keys plus `status`, * `closedDate`, and custom field properties are sent. * * A close-out sets `status` to COMPLETED — unless the ticket's `workOrderOperation.status` * says MM already reports the operation as DELETED (reconciliation found it gone from the * ERP), in which case the status is left alone so the late labor ticket does not flip it. * * `status` is optional on the payload and MM's labor-ticket export currently sends only * `closedDate` and `customFields` for the operation, so today the guard only engages when * a caller supplies the status. When it is absent, COMPLETED is written as before: a * COMPLETED operation with a closedDate is as inert as a DELETED one (hidden from the * operator view, excluded from schedule sync and from reconciliation candidates), so the * remaining exposure is cosmetic, and never writing a status on close-out would be a * regression for every connector. */ export function buildMMSendWorkOrderOperationFromLaborTicket( laborTicket: MMReceiveLaborTicket ): MMSendWorkOrderOperationLaborTicketSync | null { const wo = laborTicket.workOrderOperation; if (!wo) return null; const extras = wo as unknown as Record; const customFieldsMerged = parseCustomFieldsBag(extras.customFields); const hasCustomFieldKeys = Object.keys(customFieldsMerged).length > 0; const closedRaw = wo.closedDate ?? null; const hasClosedDate = isValidClosedDate(closedRaw); if (!hasClosedDate && !hasCustomFieldKeys) return null; if (!laborTicket.workOrderId) return null; const patch: MMSendWorkOrderOperationLaborTicketSyncPatch = {}; if (hasClosedDate) { patch.closedDate = closedRaw; if (normalizeMMStatus(wo.status) !== MMWorkOrderOperationStatus.DELETED) { patch.status = MMWorkOrderOperationStatus.COMPLETED; } } if (hasCustomFieldKeys) { patch.customFields = customFieldsMerged; const schemaRef = extras.customFieldsSchemaRef as number | null | undefined; if (schemaRef !== undefined && schemaRef !== null) { patch.customFieldsSchemaRef = schemaRef; } } return new MMSendWorkOrderOperationLaborTicketSync( laborTicket.workOrderId, laborTicket.lot ?? "", laborTicket.split ?? "", laborTicket.sub ?? "", String(laborTicket.sequenceNumber ?? ""), laborTicket.resourceId ?? "", patch ); }