/** * Work-order gap filling from labor tickets. * * A connector's from-ERP work-order sync is windowed (newest N jobs, a lookback floor, * a plant-scoped query, …), so a long-running job can age out of the batch while operators * are still clocked into it. Labor tickets for that job then fail the SDK's labor import * filter (`notInSyncedWorkOrderOperation`, see {@link filterLaborTicketsForMmImport}) or, * worse, `erp.labor_ticket.work_order_operation_fk` on insert. * * This module holds the MM-side policy for closing that gap, independent of any ERP: * * - which work orders to FETCH from the ERP (bounded by OPEN labor, plus a persisted * watch list so a filled job is re-fetched until its terminal state is imported), * - which entities of the fetched batch to WRITE without fighting the bulk writer, * - which operation rows to hand back so the same cycle's labor filter accepts the tickets. * * The ERP query and the ERP → {@link MMSendWorkOrderBatch} mapping stay in the connector. * {@link StandardProcessDrivers.fillWorkOrderGapsFromLaborTickets} orchestrates these * functions around a connector-supplied fetch callback. * * Every key here comes from the labor-import helpers ({@link normalizeLaborTicketWorkOrderOpKey}, * {@link buildSyncedWorkOrderOperationKeySet}) so "missing" can never drift from what the * labor filter rejects. * * This path is SUPPLEMENTARY to the connector's regular work-order sync, never a replacement: * labor tickets do not describe the shop schedule. */ import type { MMSendWorkOrder, MMSendWorkOrderBatch, MMSendWorkOrderOperation } from "../services/mm-api-service/types/send-types.js"; import { type SyncedWorkOrderOperationRow } from "./labor-import-from-erp.js"; /** * The fields of a labor ticket that identify its work order operation and its state. * `lot` / `split` take part in the key only when the bulk rows opt in * ({@link isLotSplitAwareRowSet}), exactly as in the labor filter. */ export interface LaborTicketWorkOrderOpRef { workOrderId?: unknown; lot?: unknown; split?: unknown; sub?: unknown; sequenceNumber?: unknown; /** `"OPEN"` while the operator is clocked in (see {@link OPEN_LABOR_TICKET_STATE}). */ state?: unknown; } /** Labor ticket state meaning an operator is currently clocked in. Compared case-insensitively. */ export declare const OPEN_LABOR_TICKET_STATE = "OPEN"; /** Days a tracked work order is kept after the last cycle in which open labor referenced it. */ export declare const DEFAULT_TRACKED_WORK_ORDER_TTL_DAYS = 30; /** * Hard cap on the watch list, purely to prevent unbounded growth. Oldest entries are evicted * first. Pending close-outs (fetched terminal, write not yet landed) are exempt: they are * bounded by one cycle's fetch and leave the list once their write lands. */ export declare const DEFAULT_MAX_TRACKED_WORK_ORDERS = 1000; /** Fetch-list size above which the driver logs a warning (no cap; every planned id is fetched). */ export declare const DEFAULT_GAP_FILL_FETCH_WARN_THRESHOLD = 200; /** A work order on the gap-fill watch list. */ export interface TrackedLaborReferencedWorkOrder { workOrderId: string; /** ISO timestamp of the last cycle in which a labor ticket drove a fetch of this work order. */ lastLaborSeenAt: string; } /** * Distinct work order ids referenced by OPEN labor tickets whose * (workOrderId, sub, sequenceNumber) has no operation among the bulk-synced ops. * * OPEN-only, unlike {@link findMissingWorkOrderOpKeys}: this picks which work orders to * FETCH from the ERP, so restricting it to active labor bounds the fetch by how many * operators are clocked in rather than by the ticket volume in the labor window. What to * WRITE among the work orders it selects is a separate question. * * Ids are returned trimmed with their case preserved. */ export declare function findOpenLaborWorkOrderIdsMissingOps(laborTickets: ReadonlyArray, bulkSyncedWorkOrderOps: ReadonlyArray): string[]; /** * Like {@link findOpenLaborWorkOrderIdsMissingOps} but for tickets of EVERY state, so a * work order observed only through CLOSED tickets (an operation that opened and closed * between two labor polls, or the watch list lost to a container recreate while the job * was already closed) is fetched too, and its tickets are not rejected for good. * * The price is the bound: the fetch is then limited by the closed tickets in the * connector's labor window rather than by concurrent clock-ins, so a wide lookback or an * initial load can name many aged-out work orders at once. Opt in via * `includeClosedTickets` on the driver when the labor window is narrow enough. */ export declare function findLaborWorkOrderIdsMissingOps(laborTickets: ReadonlyArray, bulkSyncedWorkOrderOps: ReadonlyArray): string[]; /** * The normalized operation keys that labor tickets reference but the bulk-synced ops do * not contain — the same computation as {@link findOpenLaborWorkOrderIdsMissingOps}, * keeping the key instead of collapsing it to a work order id. Keys are in the form the * bulk row set uses (with lot / split when it opts in), and * {@link computeWorkOrderGapFillWriteScope} matches fetched operations the same way. * * These are the gap fill's primary write set, though not its only one: a terminal work * order also writes its full operation set for close-out, and a work order the bulk sync * is not covering writes its backing work order, part and part operation rows too. What * all three have in common is no competing writer — a missing operation has none by * definition, and neither case above is being written by the bulk sync this cycle. * * Every ticket state counts, not just OPEN. That is what makes the returned op set safe * to hand to the labor filter: an op a ticket references is then either in the bulk batch * (written this cycle by the bulk sync) or in this set (written this cycle by the gap * fill), so "returned" implies "in MM" without depending on an earlier cycle's write * having landed. Restricted to OPEN, a write that failed while a ticket was open would * never be retried once the operator clocked out — the key would be gone but the op still * returned, and the CLOSED ticket would violate `work_order_operation_fk` every cycle * until the work order's watch-list TTL expired. */ export declare function findMissingWorkOrderOpKeys(laborTickets: ReadonlyArray, bulkSyncedWorkOrderOps: ReadonlyArray): Set; /** * Work order ids the bulk sync touched this cycle, from its work order operations. Id-level * only: {@link computeWorkOrderGapFillWriteScope} judges coverage per MM work order row * (`workOrderId + lot + split + sub`), since the bulk window may carry one lot or * sub-assembly of a job but not its siblings. */ export declare function buildBulkSyncWorkOrderIdSet(bulkWorkOrderIds: Iterable): Set; /** Whether `workOrderId` is in a set built by {@link buildBulkSyncWorkOrderIdSet}. */ export declare function isWorkOrderCoveredByBulkSync(bulkWorkOrderIds: ReadonlySet, workOrderId: unknown): boolean; /** * Default terminal predicate for a fetched work order: a terminal status (per * {@link normalizeMMStatus}) or a non-blank `closedDate`. A terminal work order's final * state is imported once more, after which it leaves the watch list. Connectors whose * mapping encodes completion differently pass their own predicate to the driver. */ export declare function isTerminalWorkOrderForGapFill(workOrder: Pick): boolean; export interface PlanWorkOrderGapFillFetchInput { tracked: ReadonlyArray; /** * Work order ids labor tickets referenced this cycle with no bulk-synced operation: from * {@link findOpenLaborWorkOrderIdsMissingOps}, or {@link findLaborWorkOrderIdsMissingOps} * when closed tickets may drive a fetch too. */ laborWorkOrderIds: ReadonlyArray; now: Date; /** Default {@link DEFAULT_TRACKED_WORK_ORDER_TTL_DAYS}. */ trackedTtlDays?: number; } export interface PlanWorkOrderGapFillFetchResult { /** Work order ids to fetch from the ERP this cycle: labor-referenced ∪ carried watch list, deduped. */ fetchWorkOrderIds: string[]; /** Watch-list entries still valid after pruning; the next watch list is built from these. */ carriedTracked: TrackedLaborReferencedWorkOrder[]; dropped: { expired: number; }; } /** * Decide which work orders to fetch this cycle and which tracked entries to carry forward. * Prunes entries that expired (TTL). Entries the bulk sync covers again are NOT pruned here: * the bulk rows name a job id, but a job may have lots / sub-assemblies the window did not * carry, and only the fetched rows tell. {@link updateTrackedAfterWorkOrderGapFillFetch} * drops them once the fetch shows every row covered. */ export declare function planWorkOrderGapFillFetch(input: PlanWorkOrderGapFillFetchInput): PlanWorkOrderGapFillFetchResult; export interface UpdateTrackedAfterWorkOrderGapFillFetchInput { carriedTracked: ReadonlyArray; /** The same ids given to {@link planWorkOrderGapFillFetch}. */ laborWorkOrderIds: ReadonlyArray; /** Ids of fetched work orders the terminal predicate accepted. */ terminalWorkOrderIds: Iterable; /** * Fetched work order ids the bulk sync covered row for row this cycle and the gap fill is * writing nothing for (`bulkCoveredWorkOrderIds` from * {@link computeWorkOrderGapFillWriteScope}). The bulk sync owns them now, so they leave * the watch list; an id that later ages out again is re-added by labor like any other. */ bulkCoveredWorkOrderIds?: Iterable; now: Date; /** Default {@link DEFAULT_MAX_TRACKED_WORK_ORDERS}. */ maxTrackedWorkOrders?: number; } export interface UpdateTrackedAfterWorkOrderGapFillFetchResult { /** The next watch list. */ tracked: TrackedLaborReferencedWorkOrder[]; /** * Entries removed because their fetched work order is terminal. Returned so the caller * can keep persisting them until the close-out write actually lands — these work orders * are outside the bulk window, so one dropped before a failed write is never fetched * again and its MM work order keeps a stale status for good. */ terminal: TrackedLaborReferencedWorkOrder[]; dropped: { coveredByBulkSync: number; terminal: number; evicted: number; }; } /** * Produce the next watch list after a gap-fill fetch: refresh/add work orders that labor * referenced this cycle, remove work orders the bulk sync fully covers again (it owns them) * and work orders whose fetched state is terminal (their final state was just imported), * and evict the oldest entries beyond the cap. */ export declare function updateTrackedAfterWorkOrderGapFillFetch(input: UpdateTrackedAfterWorkOrderGapFillFetchInput): UpdateTrackedAfterWorkOrderGapFillFetchResult; /** * Work order operation rows in the shape the labor filter consumes * (`FilterLaborTicketsForMmImportOptions.syncedWorkOrderOperations`), so gap-filled ops can * be concatenated with the bulk sync's rows. */ export declare function toSyncedWorkOrderOperationRows(workOrderOperations: ReadonlyArray>): SyncedWorkOrderOperationRow[]; export interface ComputeWorkOrderGapFillWriteScopeInput { /** Everything the connector mapped for the fetched work orders. */ fullBatch: MMSendWorkOrderBatch; /** * Work order operation rows the bulk sync wrote this cycle. `work_order_id` + `sub` (and * `lot` / `split` when present) decide which MM work order rows are covered; their * `sequence_number`s decide which part operations of a shared part the bulk sync owns. */ bulkSyncedWorkOrderOperations: ReadonlyArray; /** From {@link findMissingWorkOrderOpKeys}. */ missingOpKeys: ReadonlySet; /** Fetched work orders judged terminal by the terminal predicate (any case / whitespace). */ terminalWorkOrderIds: Iterable; } export interface ComputeWorkOrderGapFillWriteScopeResult { /** The subset of `fullBatch` to write, FK-complete for every operation it contains. */ batchToWrite: MMSendWorkOrderBatch; /** Every work order operation of `fullBatch`, for widening this cycle's labor filter key set. */ keySetWorkOrderOps: SyncedWorkOrderOperationRow[]; /** * Terminal work orders with at least one fetched row the bulk sync did not cover: those * rows and their full operation sets are written. */ closingWorkOrderIds: string[]; /** * Fetched work order ids with at least one fetched row, every one of which the bulk sync * covered, and none of whose operations is written this cycle. Compared * case-insensitively, as the watch list keys ids: an id is listed (in every fetched * spelling) only when the rows of all its spellings are covered and no spelling has an * operation in `batchToWrite`. Feed to {@link updateTrackedAfterWorkOrderGapFillFetch}: * the bulk sync owns them, and the gap fill has nothing left to retry for them. */ bulkCoveredWorkOrderIds: string[]; /** Work order ids with at least one row (`workOrderId + lot + split + sub`) whose parents are written. */ uncoveredWorkOrderIds: string[]; /** * Part identities ({@link mmPartIdentityKey}) used by both an uncovered work order being * written and a covered fetched work order. Their part row is left to the bulk sync and * only the part operations the written operations need, and the bulk sync did not write, * are included. */ sharedPartIdentities: string[]; } /** * The one definition of "the same MM part" for SDK-side grouping: raw part number and * revision (`null` / `undefined` → ""), `method` defaulting to "Standard". No trimming or * case folding — the MM constructors send these values as given. Used by * `buildWorkOrderBatchFromFlattened` to dedupe rows into parts and by * {@link computeWorkOrderGapFillWriteScope} to select a work order's part rows. */ export declare function mmPartIdentityKey(row: { partNumber?: string | number | null; partRevision?: string | number | null; method?: string | null; }): string; /** * Decide what to write by whether the bulk sync covered each row this cycle. * * Coverage is judged at the finest identity the bulk rows can express. The fetch and the * watch list work per work order id (the ERP job), but MM identifies a work order by * `workOrderId + lot + split + sub`, and the bulk window may hold one lot or sub-assembly of * a job but not its siblings. A fetched work order row or operation is COVERED when a bulk * row matches its id and sub (empty sub → "0", as in the labor key) and, where the bulk rows * carry `lot` / `split`, those too. Bulk rows without lot / split cover every lot / split of * that id + sub. * * - A COVERED row has a live competing writer. The gap fill's mapping may derive fields * differently (Epicor's JobEntry path vs its BAQ/SQL paths differ on finish quantity, * quantity per part and cycle time), so rewriting its siblings would make the two paths * overwrite each other every cycle. Only its individually missing operations are written; * the work order row and its parts are left to the bulk sync, which wrote them this cycle. * - An UNCOVERED row has no other writer, so a missing operation on it brings its work * order row and part / part operations along — which it needs: a row that has never been * inside the window may be entirely new to MM, and an operation whose parents are absent * violates its FK. * - A CLOSING work order id (every fetched row terminal, and at least one row uncovered) * writes each UNCOVERED row and its full operation set, so terminal status and closed * date reach MM before it leaves the watch list. Its covered rows are left to the bulk * sync, which owns them; a terminal id the bulk sync covers entirely is not closing. * - Parts are SHARED across work orders, so the per-row gate is not enough for them: an * uncovered row being written and a bulk-written work order for the same part would have * the gap fill rewrite the part operations the bulk sync wrote. For such a part the part * row is left to the bulk sync, and only the part operations the written operations * need, minus the sequences the bulk sync wrote for that part, are included. Bulk * ownership is seen through the covered fetched rows and, when the bulk rows carry * `part_number` / `part_revision` / `method`, through every bulk row — without part * identity on the bulk rows, a bulk work order for the same part that was not fetched * cannot be seen. * - A missing operation on a COVERED row is written alone, plus its part operation when * no bulk row is known to own it: the bulk mapping may have dropped the operation and * its part operation together, and the operation's FK needs the part operation. * * `keySetWorkOrderOps` is every operation of `fullBatch`, not only the written ones: a * ticket that CLOSED since last cycle no longer produces a missing key, and narrowing the * key set to the written ops would reject its clock-out. */ export declare function computeWorkOrderGapFillWriteScope(input: ComputeWorkOrderGapFillWriteScopeInput): ComputeWorkOrderGapFillWriteScopeResult; /** * Persists the gap-fill watch list in the SDK's cross-process runtime state store * (`sdk_runtime_state` in the local SQLite file), under its own key per connector type, so * it never shares a row with record tracking or any other feature. * * On edge devices the SQLite file lives in the container's writable layer and is wiped by * any container recreate. The watch list rebuilds from open labor on later cycles, so that * loss is tolerable; the only effect is that a work order which closed during the gap is * not re-fetched for close-out. * * `load()` / `save()` are a plain read-modify-write with no locking. That is safe for the * SDK's own use — only the `from-erp` job calls the driver, and Bree never overlaps a * running job — but a connector must not run the gap fill concurrently for the same * `connectorType`, or one call's save can overwrite the other's. */ export declare class LaborWorkOrderGapFillTracker { private readonly key; constructor(connectorType: string); /** * The watch list, `[]` when none was saved. Throws when the store cannot be read: an * empty list in its place would be saved over the persisted one later in the cycle, * losing entries no current ticket references, and their pending close-outs with them. * * Content that is not a JSON array is the one exception. It cannot be recovered, and * failing on it would block every cycle until someone cleared it by hand, so it yields * `[]` with a warning and the next save replaces it. Malformed entries are dropped. */ load(): TrackedLaborReferencedWorkOrder[]; /** Replace the watch list. Throws on a store failure: the caller decides whether that is fatal. */ save(tracked: ReadonlyArray): void; } //# sourceMappingURL=labor-work-order-gap-fill.d.ts.map