import { MMReceiveLaborTicket, MMReceiveTimesheet, MMReceiveWorkOrder, MMReceiveWorkOrderOperation, } from "../services/mm-api-service/types/receive-types.js"; /** * Controls how the SDK proceeds after `recoverLaborTicketId`. */ export enum RecoverLaborTicketIdAction { RECOVER = "RECOVER", CREATE = "CREATE", DEFER = "DEFER", } /** * Result of `recoverLaborTicketId` when using the structured return shape. */ export type RecoverLaborTicketIdResult = | { action: RecoverLaborTicketIdAction.RECOVER; laborTicketId: string } | { action: RecoverLaborTicketIdAction.CREATE } | { action: RecoverLaborTicketIdAction.DEFER; reason?: string }; /** Accepted return shapes from `recoverLaborTicketId` (structured result or legacy shorthand). */ export type RecoverLaborTicketIdReturn = | RecoverLaborTicketIdResult | string | null; /** * Interface for ERP connectors. * * Required for any connector. */ export interface IERPConnector { /** * The type of ERP system this connector is for. */ readonly type: string; /** * Sync data from the ERP system to MM. * Timezone expectations: * - When sending ERP data to MachineMetrics (e.g., via StandardProcessDrivers or MMEntityProcessor), * connectors MUST convert all datetime fields to ISO-8601 UTC strings (trailing `Z` or explicit offset) * before invoking SDK helpers. The SDK validates/forwards these values but does not apply timezone shifts. */ syncFromERP(): Promise; syncFromERPCompleted(): Promise; /** * Sync data to the ERP system from MM (labor tickets and timesheet clock in/out via StandardProcessDrivers). */ syncToERP(): Promise; syncToERPCompleted(): Promise; /** * Retry labor tickets that have failed to import to the ERP system during syncToERP(). */ retryFailedLaborTickets(): Promise; retryFailedLaborTicketsCompleted(): Promise; /** * Optional: reconcile MM work orders (and their operations) against the ERP, marking * those that no longer exist / are active in the ERP as DELETED in MM. Runs rarely * (default every 24 hours). * * The `reconcile-work-orders` job runs on its schedule for every connector and invokes * this method when implemented; otherwise the job cycle no-ops with an info log. * Connectors typically delegate to * {@code StandardProcessDrivers.reconcileDeletedWorkOrders()} with an * {@link IERPWorkOrderReconciliationHandler}. */ reconcileWorkOrders?(): Promise; reconcileWorkOrdersCompleted?(): Promise; /** * Start the connector. Typically a connector should delegate control to the data-sync-service via runDataSyncService(). */ startUp(): Promise; } /** * Composite identity of an ERP work order as stored in MM. * `lot`, `split`, and `sub` default to the empty string when absent; all four * parts are stringified and compared case-insensitively after trimming (numbers, as * returned by many DB drivers, are accepted). * Used by the {@code selectWorkOrdersMissingFromErp} matching helper. */ export interface ErpWorkOrderKey { workOrderId: string | number; lot?: string | number | null; split?: string | number | null; sub?: string | number | null; } /** * Composite identity of an ERP work order operation as stored in MM: the * {@link ErpWorkOrderKey} parts plus `sequenceNumber`. Same normalization rules. * Used by the {@code selectWorkOrderOperationsMissingFromErp} matching helper. */ export interface ErpWorkOrderOperationKey extends ErpWorkOrderKey { /** * Compared numerically when numeric-looking ("010", "10.0", 10 and "10" all match), * because MM stores sequence numbers as numbers. */ sequenceNumber: string | number; } /** * ERP-specific handling for work order (and work order operation) deletion reconciliation. * * The connector owns the selection — it sees the full candidate rows and can apply any * ERP- or site-specific rules (including "never remove" lists). The SDK owns everything * around it: fetching candidates, validating the returned subset, the blast-radius * guard, dry-run handling, and the DELETED writes. */ export interface IERPWorkOrderReconciliationHandler { /** * From the given MM candidate work orders (status RELEASED or ACTIVE), return the * subset that no longer exists / is active in the ERP and should be marked DELETED. * * Common case: fetch the composite keys of all active ERP jobs and delegate the * matching to {@code selectWorkOrdersMissingFromErp(candidates, activeErpKeys)}, * optionally filtering its result with ERP-specific rules. * * CONTRACT: this method MUST throw on any ERP fetch failure (API down, permission * error, truncated result) — never guess. Every returned row must be one of the given * candidate rows; the SDK validates this and rejects the run otherwise. */ selectWorkOrdersToDelete( candidates: MMReceiveWorkOrder[] ): Promise; /** * Optional but strongly recommended: from the given MM candidate work order operations * — status RELEASED or ACTIVE, no `closedDate`, and belonging to a work order that is * itself still a live candidate in MM — return the subset that no longer exists / is * active in the ERP and should be marked DELETED. This covers an operation deleted * from a job that itself survives, which work-order-level reconciliation cannot see. * * Common case: fetch the composite keys of all operations of all active ERP jobs and * delegate to {@code selectWorkOrderOperationsMissingFromErp(candidates, activeErpKeys)}. * * Operations whose parent work order is DELETED in MM (by this run or an earlier one) * are never offered here; the SDK marks them DELETED on its own. * * Same CONTRACT as {@link selectWorkOrdersToDelete}: MUST throw on any ERP fetch * failure; every returned row must be one of the given candidates. */ selectWorkOrderOperationsToDelete?( candidates: MMReceiveWorkOrderOperation[] ): Promise; } /** * Interface for standard process methods that call ERP connectors back to create or update labor tickets. * * Timestamp fields on `MMReceiveLaborTicket` objects have already been converted by the SDKfrom MM's * UTC API responses into the ERP's local timezone, based on the company's timezone setting. */ export interface IERPLaborTicketHandler { /** * Optionally recover an existing ERP labor ticket ID for an MM ticket before * creating a new ERP row. * * Return `{ action: RecoverLaborTicketIdAction.RECOVER, laborTicketId }` when a row exists, * `{ action: RecoverLaborTicketIdAction.CREATE }` or `null` when none exists, * `{ action: RecoverLaborTicketIdAction.DEFER }` to skip create for this cycle (retry later). * Legacy `string` / `null` returns are also accepted (`string` = recover). * * Duplicate prevention requires the connector to implement this and return a * correct id; the SDK does not validate ids against the ERP. */ recoverLaborTicketId?( laborTicket: MMReceiveLaborTicket ): Promise; /** * `warnings` (optional, both methods): non-fatal, operator-readable messages the ERP * raised while accepting the transaction (e.g. Epicor BPM InfoMessages). They never * fail the operation. The NATS listener returns them on its reply as a top-level * `warnings: string[]` (omitted when there are none); the polling path logs them. * If the MM write that follows the ERP call fails, the NATS reply is an error and * carries no warnings: the operator sees the failure instead. * * Warnings sit beside the ticket for create but on the returned ticket for update. * The asymmetry is deliberate: it keeps both return types backward compatible, so * connectors that never raise warnings need no changes. Do not "fix" it into a * shared result object; that breaks every existing connector. */ createLaborTicketInERP( laborTicket: MMReceiveLaborTicket ): Promise<{ laborTicket: MMReceiveLaborTicket; erpUid: string; warnings?: string[] }>; updateLaborTicketInERP( laborTicket: MMReceiveLaborTicket ): Promise; } /** * ERP-specific handling for timesheet rows exported from MM (e.g. employee clock in/out). * * A row that throws is delivered again on later cycles, and a person's later rows wait behind it, * so a retry must be safe after a partial failure. A row that succeeded is not delivered again, * except in the rare case where the checkpoint save to MM fails and the container is recreated * before the next save; tolerating repeats where the ERP allows it keeps that case harmless. */ export interface IERPTimesheetHandler { processTimesheetInERP(timesheet: MMReceiveTimesheet): Promise; }