toga-ai 1.0.148 → 1.0.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +5 -2
- package/knowledge/INDEX.md +2 -1
- package/knowledge/clients/growrk/INDEX.md +6 -0
- package/knowledge/clients/growrk/features/transfer-order-flow.md +119 -0
- package/knowledge/clients/growrk/profile.md +22 -0
- package/knowledge/clients/nycdoe/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +46 -2
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-06-19
|
|
10
|
+
owners: [jcardinal, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
13
13
|
- _underscore/Model/Client/ItemFulfillmentItem.php
|
|
@@ -101,15 +101,18 @@ inheritance. Verified against prod `Client_Compass` chains (≥3 levels deep).
|
|
|
101
101
|
post-delete hooks never fire; a standalone downstream DELETE doesn't immediately remove its
|
|
102
102
|
upstream mirror. A later reconciling PUT cleans up stale records via set comparison. Full
|
|
103
103
|
delete handling would need a `preDelete` mechanism + `PRE/DELETE` interceptor rows.
|
|
104
|
+
- **Transfer-order fulfillments are explicitly excluded from the upstream walk.** `reconcileUpstreamLevel()` returns `null` immediately when the downstream IF has `transferOrderId` set (no `salesOrderId`). This is intentional for standalone TOs (GroWrk v1). When SO→TO→SO chain scenarios are needed, Phase 5 of the GroWrk transfer order plan extends this method to walk `TransferOrders_SalesOrders` bridge tables — see `clients/growrk/features/transfer-order-flow.md`.
|
|
104
105
|
- **Out of scope:** NetSuite sync of upstream IFs; transfer-order fulfillments
|
|
105
106
|
(`transferOrderId`) are skipped.
|
|
106
107
|
- **Performance:** every child write re-runs a full upstream walk (read-heavy, but each
|
|
107
108
|
upstream record is written at most once — idempotent). Fine for normal fulfillment sizes.
|
|
108
109
|
|
|
109
110
|
## Change history
|
|
111
|
+
- 2026-06-19 — Documented explicit TO exclusion in reconcileUpstreamLevel; cross-linked GroWrk transfer order plan. (rgirish)
|
|
110
112
|
- 2026-06-08 — Documented the Recursive Item Fulfillments engine (interceptor-driven upstream fulfillment mirroring, bundle scaling, reconcile loop). (jcardinal)
|
|
111
113
|
|
|
112
114
|
## Related docs
|
|
115
|
+
- GroWrk transfer order flow plan: `clients/growrk/features/transfer-order-flow.md`.
|
|
113
116
|
- `_underscore` architecture (interceptors, `internalApiRequest`, `_Model` layer).
|
|
114
117
|
- `api2` architecture (V2 metadata engine that fires these interceptors; the
|
|
115
118
|
commit-logs / rollback-data-on-failure transaction invariant the throws rely on).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -14,7 +14,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
14
14
|
|
|
15
15
|
## 2.0 framework
|
|
16
16
|
|
|
17
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
17
|
+
- **_underscore** (_Underscore) _(framework core)_ — 9 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
18
18
|
- **worker2** (Worker) — 8 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
19
19
|
- **api2** (API) — 4 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
20
20
|
- **dbchanges2** (Database Changes) _(framework core)_ — 1 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -38,6 +38,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
38
38
|
- **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
|
|
39
39
|
- **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
|
|
40
40
|
- **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
|
|
41
|
+
- **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
|
|
41
42
|
- **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
|
|
42
43
|
- **Office Depot** (`office-depot`) → [clients/office-depot/INDEX.md](clients/office-depot/INDEX.md)
|
|
43
44
|
- **Prudential Financial** (`prudential`) → [clients/prudential/INDEX.md](clients/prudential/INDEX.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Client: GroWrk `growrk`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)](features/transfer-order-flow.md) | 2.0 | GroWrk moves inventory between their own locations (warehouses, distribution points). | _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemReceipt.php, _underscore/Model/Client/ItemReceiptItem.php, _underscore/Trait/Netsuite/TransferOrder.php, dbchanges2/Client_Growrk/, dbchanges2/Client/ |
|
|
6
|
+
| [GroWrk](profile.md) | 2.0 | GroWrk is a 2.0 client using TOGA for inventory and order management. | |
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Transfer Order Flow (NetSuite SalesOrder workaround → dedicated TransferOrders)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: growrk
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-19
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/TransferOrder.php
|
|
13
|
+
- _underscore/Model/Client/TransferOrderItem.php
|
|
14
|
+
- _underscore/Model/Client/ItemFulfillment.php
|
|
15
|
+
- _underscore/Model/Client/ItemReceipt.php
|
|
16
|
+
- _underscore/Model/Client/ItemReceiptItem.php
|
|
17
|
+
- _underscore/Trait/Netsuite/TransferOrder.php
|
|
18
|
+
- dbchanges2/Client_Growrk/
|
|
19
|
+
- dbchanges2/Client/
|
|
20
|
+
related:
|
|
21
|
+
- 2.0/apps/_underscore/features/recursive-item-fulfillments.md
|
|
22
|
+
- 2.0/apps/worker2/architecture.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
GroWrk moves inventory between their own locations (warehouses, distribution points). Historically this was done by creating zero-dollar SalesOrders with `holdInvoice=true` in NetSuite — polluting sales reporting, distorting demand planning, and creating audit confusion. This feature introduces a dedicated `TransferOrders` record type in the TOGA database. NetSuite continues to receive transfer orders as SalesOrders with `holdInvoice=true` (no NS-side change); the distinction is made and stored only on the TOGA side.
|
|
28
|
+
|
|
29
|
+
## Key files / entry points
|
|
30
|
+
|
|
31
|
+
| File | Role |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `_underscore/Model/Client/TransferOrder.php` | TO model — origin/destination locations, number, serviceRequestId, purchaseOrderId, c_netsuiteInternalSalesOrderId |
|
|
34
|
+
| `_underscore/Model/Client/TransferOrderItem.php` | TO line items — itemId, quantity, lineNumber |
|
|
35
|
+
| `_underscore/Trait/Netsuite/TransferOrder.php` | NS sync trait — **currently a stub** (only declares c_netsuiteInternalSalesOrderId); `syncNetsuiteTransferOrder()` not yet implemented |
|
|
36
|
+
| `_underscore/Model/Client/ItemFulfillment.php` | Outbound closure — `transferOrderId` FK already present; `salesOrderId` must be made nullable |
|
|
37
|
+
| `_underscore/Model/Client/ItemReceipt.php` | Inbound closure — `transferOrderId` FK already present |
|
|
38
|
+
| `dbchanges2/Client_Growrk/` | Client-specific schema fixes (dtCreated/dtUpdated/isActive on TransferOrders, nullable XOR columns) |
|
|
39
|
+
| `dbchanges2/Client/` | All-client bridge tables (8 new SO↔TO and PO↔TO bridge tables) |
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
### Identification signal
|
|
44
|
+
A NetSuite SalesOrder is a TransferOrder if and only if: `holdInvoice === true` AND `total === 0`. Both conditions must hold — `holdInvoice` alone is not sufficient.
|
|
45
|
+
|
|
46
|
+
### Two-stage closure
|
|
47
|
+
- **Outbound:** An `ItemFulfillment` with `transferOrderId` set (not `salesOrderId`) closes the outbound side — items leave the origin location.
|
|
48
|
+
- **Inbound:** An `ItemReceipt` with `transferOrderId` set (not `purchaseOrderId`) closes the inbound side — items arrive at the destination location.
|
|
49
|
+
|
|
50
|
+
### Chain positions
|
|
51
|
+
Transfer orders can sit in several positions in the order chain:
|
|
52
|
+
1. **Standalone TO** — pure location-to-location move, no SO/PO involved
|
|
53
|
+
2. **SO → TO** — customer SO triggers an internal TO (outbound from origin)
|
|
54
|
+
3. **PO → TO** — vendor PO received via TO (move to warehouse)
|
|
55
|
+
4. **SO → TO → SO** — customer-facing SO, internal move, downstream SO to fulfill
|
|
56
|
+
|
|
57
|
+
## Data model
|
|
58
|
+
|
|
59
|
+
### TransferOrders (prod — `Client_Growrk`)
|
|
60
|
+
| Column | Type | Notes |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| id | int PK | |
|
|
63
|
+
| uuid | char(36) | |
|
|
64
|
+
| originLocationId | int FK | NOT NULL |
|
|
65
|
+
| destinationLocationId | int FK | NOT NULL |
|
|
66
|
+
| dateOrder | date | nullable |
|
|
67
|
+
| number | varchar | currently varchar — generate as `TO-<tranId>` from NS in sync trait |
|
|
68
|
+
| serviceRequestId | int FK | nullable |
|
|
69
|
+
| purchaseOrderId | int FK | nullable |
|
|
70
|
+
| customerIdentificationNumber | varchar | nullable |
|
|
71
|
+
| c_netsuiteInternalSalesOrderId | varchar | NS SalesOrder internal ID |
|
|
72
|
+
| **dtCreated** | datetime | **MISSING — must add in Phase 1** |
|
|
73
|
+
| **dtUpdated** | datetime | **MISSING — must add in Phase 1** |
|
|
74
|
+
| **isActive** | tinyint | **MISSING — must add in Phase 1** |
|
|
75
|
+
|
|
76
|
+
### Columns that need schema fixes before first use
|
|
77
|
+
- `ItemFulfillments.salesOrderId` — currently NOT NULL; must be made nullable (XOR with transferOrderId)
|
|
78
|
+
- `ItemFulfillmentItems.salesOrderItemId` — currently NOT NULL; must be made nullable
|
|
79
|
+
- `ItemReceiptItems.purchaseOrderItemId` — currently NOT NULL; must be made nullable (XOR with transferOrderItemId)
|
|
80
|
+
|
|
81
|
+
### New bridge tables (all 8 in `dbchanges2/Client/` — applies to all clients)
|
|
82
|
+
| Table | FK A | FK B |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| SalesOrders_TransferOrders | salesOrderId | transferOrderId |
|
|
85
|
+
| TransferOrders_SalesOrders | transferOrderId | salesOrderId |
|
|
86
|
+
| PurchaseOrders_TransferOrders | purchaseOrderId | transferOrderId |
|
|
87
|
+
| TransferOrders_PurchaseOrders | transferOrderId | purchaseOrderId |
|
|
88
|
+
| SalesOrderItems_TransferOrderItems | salesOrderItemId | transferOrderItemId |
|
|
89
|
+
| TransferOrderItems_SalesOrderItems | transferOrderItemId | salesOrderItemId |
|
|
90
|
+
| PurchaseOrderItems_TransferOrderItems | purchaseOrderItemId | transferOrderItemId |
|
|
91
|
+
| TransferOrderItems_PurchaseOrderItems | transferOrderItemId | purchaseOrderItemId |
|
|
92
|
+
|
|
93
|
+
`Core.Records` already has id=312 (`_Model_Client_TransferOrder`) and id=313 (`_Model_Client_TransferOrderItem`) registered.
|
|
94
|
+
|
|
95
|
+
## Implementation plan (5 phases — team review pending)
|
|
96
|
+
|
|
97
|
+
Full plan saved at `/Applications/AMPPS/www/transfer-order-plan.md`.
|
|
98
|
+
|
|
99
|
+
| Phase | Work | Status |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| 1 | Schema fixes (dtCreated/dtUpdated/isActive, nullable XOR columns, 8 bridge tables, Core RecordFields) | Pending |
|
|
102
|
+
| 2 | PHP model updates (TransferOrder.php fields, 8 bridge model stubs) | Pending |
|
|
103
|
+
| 3 | _Trait_Netsuite_TransferOrder sync impl + ItemReceipt TO path | Pending |
|
|
104
|
+
| 4 | worker2 _Worker_Netsuite_TransferOrder action class | Pending |
|
|
105
|
+
| 5 | Upstream IF chain extension (SO→TO→SO walk) | Deferred |
|
|
106
|
+
|
|
107
|
+
## Gotchas / known issues
|
|
108
|
+
|
|
109
|
+
- **`dtCreated`/`dtUpdated` MISSING from `TransferOrders` in prod.** Any `_Model_Client_TransferOrder::save()` call will fail silently until Phase 1 runs. This is a hard blocker — nothing else works until the schema fix lands.
|
|
110
|
+
- **XOR is application-layer only.** MySQL does not enforce that exactly one of `salesOrderId`/`transferOrderId` is set on `ItemFulfillments`. The sync trait and any POST interceptor must validate this explicitly.
|
|
111
|
+
- **`_Trait_Netsuite_TransferOrder` is a stub.** It declares only `$c_netsuiteInternalSalesOrderId` — there is no `syncNetsuiteTransferOrder()` method yet.
|
|
112
|
+
- **Upstream IF chain explicitly skips TOs today.** `ItemFulfillment::reconcileUpstreamLevel()` returns null when `transferOrderId` is set — the comment reads "no salesOrderId (transfer order) → return null (out of scope)". Phase 5 will extend this.
|
|
113
|
+
- **Lazy transaction gotcha.** `_Database::register()` auto-starts a lazy transaction since Apr 2026. The worker must call `_Database::transactionCommit(DB_CLIENT)` before any SQS dispatch or the TO insert rolls back silently.
|
|
114
|
+
- **Bridge table scope confirmed as all-clients.** The 8 bridge tables go in `dbchanges2/Client/` (not `Client_Growrk/`). Schema fixes (dtCreated etc.) stay in `Client_Growrk/` — other clients don't need them.
|
|
115
|
+
- **NS identification: both conditions required.** `holdInvoice=true` alone is not a safe signal — must also confirm `total === 0`.
|
|
116
|
+
|
|
117
|
+
## Change history
|
|
118
|
+
|
|
119
|
+
- 2026-06-19 — Designed full transfer order flow; confirmed DB state in prod; produced 5-phase implementation plan pending team review (rgirish)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GroWrk"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- _underscore
|
|
6
|
+
- api2
|
|
7
|
+
- worker2
|
|
8
|
+
- dbchanges2
|
|
9
|
+
project: _Underscore
|
|
10
|
+
client: growrk
|
|
11
|
+
type: profile
|
|
12
|
+
status: active
|
|
13
|
+
updated: 2026-06-19
|
|
14
|
+
owners: ["rgirish"]
|
|
15
|
+
files: []
|
|
16
|
+
related:
|
|
17
|
+
- clients/growrk/features/transfer-order-flow.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
GroWrk is a 2.0 client using TOGA for inventory and order management. Their primary integration involves inventory movements between locations (warehouses, distribution points). NetSuite is the ERP. The key engineering challenge is that GroWrk historically used zero-dollar SalesOrders with holdInvoice=true in NetSuite as a workaround to represent internal inventory transfers — work is underway to replace this with a dedicated TransferOrders record type in the TOGA DB while keeping NetSuite unchanged.
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
-
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/edi.php |
|
|
5
|
+
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php |
|
|
6
6
|
| [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-19
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/import_asn.php
|
|
@@ -29,6 +29,10 @@ files:
|
|
|
29
29
|
- library/app/api/nycdoe.php
|
|
30
30
|
- library/app/api/nycdoev2.php
|
|
31
31
|
- library/app/asnprocessor/manufacturer.php
|
|
32
|
+
- library/app/asnprocessor/apple.php
|
|
33
|
+
- library/app/asnprocessor/lenovo.php
|
|
34
|
+
- library/app/asnprocessor/lexmark.php
|
|
35
|
+
- library/app/asnprocessor/acer.php
|
|
32
36
|
- library/app/edi.php
|
|
33
37
|
related:
|
|
34
38
|
- ../profile.md
|
|
@@ -147,10 +151,49 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
147
151
|
4. **The NetSuite freeze:** `netSuiteInternalSalesOrderId IS NOT NULL` ⇒ `qtyOrder` locked.
|
|
148
152
|
Growth = new item row; decrease = discrepancy alert, never a reduction.
|
|
149
153
|
5. **One unit per non-serialized item row** — a 50-cable line is 1 unit and 1 repair order.
|
|
150
|
-
6. **The
|
|
154
|
+
6. **The replacement PO suffix forces a brand-new ASN/MSO/RO chain** for replacements —
|
|
155
|
+
but the suffix is a **per-manufacturer convention with NO standard format**, and even a
|
|
156
|
+
single manufacturer is inconsistent. Observed in prod: `-RPL`, `-REPL`, `-RPLE`, `-N`,
|
|
157
|
+
`_N` (underscore), and a space variant (` RPL`). ServiceNow itself documents NO `-RPL`
|
|
158
|
+
convention — its native replacement model is field-based on the *original* RITM
|
|
159
|
+
(`device_replaced`/`replaced_serial_number`/`replaced_asset_tag`); the suffix comes from
|
|
160
|
+
the OEM/SFTP feeds. Replacement-of-replacement appears as `-RPL-RPL` and is structurally
|
|
161
|
+
fine **only if hyphenated consistently**. See the duplicate-fulfillment gotcha below.
|
|
151
162
|
|
|
152
163
|
## Gotchas / known issues
|
|
153
164
|
|
|
165
|
+
- **⚠ OPEN BUG (investigated 2026-06-19, not yet fixed) — manufacturer PO-suffix format
|
|
166
|
+
variance causes DUPLICATE fulfillment.** Stage 2's serialized dedupe is scoped to the
|
|
167
|
+
*exact* PO string (`lookupAsnUnitIdByAsnPartNumberAndSerialNumber[vendorId][purchaseOrder]
|
|
168
|
+
[part][serial]`, `legacy_process_asn_queue.php:272`), and the PO flows **verbatim** end to
|
|
169
|
+
end — never trimmed/normalized except Lenovo's `trim()` in `asnprocessor/lenovo.php`. So when
|
|
170
|
+
a manufacturer re-sends the *same physical unit* under a different suffix spelling
|
|
171
|
+
(e.g. Lenovo `WR260150152-RPL` then `WR260150152-RPLE`, same serial `V60095WK`), the two PO
|
|
172
|
+
strings differ → the serial guard misses → a full duplicate ASN→SO→MSO→RO chain is created
|
|
173
|
+
for one device (two NetSuite SOs, two repair orders, two onsite tickets).
|
|
174
|
+
- **Confirmed live case:** PO `WR260150152`, serial `V60095WK`, asset `DOE-LN1265478` →
|
|
175
|
+
repair orders **772308** (MSO 309909, via `-RPL`) and **801064** (MSO 318945, via `-RPLE`),
|
|
176
|
+
both `ORDER_ASSIGNED_AWAITING_SCHEDULING` (uncaught as of investigation), same TOGa Desk
|
|
177
|
+
`serialNumId 592519`.
|
|
178
|
+
- **Systemic, not a one-off:** ~14 same-base/same-serial duplicate pairs across **Lenovo AND
|
|
179
|
+
Lexmark**, spanning suffix forms `-RPL`/`-REPL`/`-RPLE`/`-N`/`_N`. A genuine replacement
|
|
180
|
+
carries a *new* serial, so identical-serial-across-base-and-suffix = true duplication.
|
|
181
|
+
- **The discriminator that would have caught it:** serial + assetTag + trackingNumber were
|
|
182
|
+
identical across copies. A serial-anchored dedupe (`vendorId + partNumber + serialNumber`,
|
|
183
|
+
ignoring PO suffix) collapses these; PO-scoped dedupe cannot.
|
|
184
|
+
- **Do NOT blindly canonicalize all suffixes to one token** — `-RPL-RPL` (legit
|
|
185
|
+
replacement-of-replacement) must stay distinct from `-RPL`. Confirm suffix semantics with
|
|
186
|
+
the SME before any fix.
|
|
187
|
+
- **Two ADJACENT anomalies surfaced by the same blast-radius query** (separate mechanisms,
|
|
188
|
+
not yet investigated): (a) a large Lexmark cluster pairing a **numeric order number with a
|
|
189
|
+
WR PO** for identical serials (e.g. `3100157`↔`WR240178018`) — same devices under two PO
|
|
190
|
+
identifiers; (b) **invalid/placeholder serials** becoming serialized units — serial `"NA"`
|
|
191
|
+
spans 25 POs→15 ROs (Lenovo `NA`→`''` normalization at `asnprocessor/lenovo.php:102` not
|
|
192
|
+
catching all), plus `"1"`, `"11"–"29"`.
|
|
193
|
+
- **Open thread for resume:** check RO active/cancelled status across the ~14 suffix-variance
|
|
194
|
+
pairs to size live double-truck-roll exposure; then trace the numeric-PO↔WR cluster.
|
|
195
|
+
Investigation was **read-only** — no code or data changes made.
|
|
196
|
+
|
|
154
197
|
- **The queue `dedupeKey` format is load-bearing.** A 2026 incident (WR260236464 duplicate
|
|
155
198
|
repair order) was caused by dedupeKey format drift (4-part keys with serial vs 3-part
|
|
156
199
|
without) letting old rows re-import; Stage 2 then created a sibling item row (frozen
|
|
@@ -187,6 +230,7 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
|
|
|
187
230
|
of the consumer query; `php -l` every touched file.
|
|
188
231
|
|
|
189
232
|
## Change history
|
|
233
|
+
- 2026-06-19 — Investigated (read-only) the replacement-PO-suffix duplicate-fulfillment bug: PO-scoped serial dedupe defeated by per-manufacturer suffix-format variance (`-RPL`/`-REPL`/`-RPLE`/`-N`/`_N`); confirmed live case WR260150152/V60095WK (ROs 772308+801064) and ~14 systemic pairs across Lenovo+Lexmark. Refined business rule #6; added OPEN-BUG gotcha + adjacent anomalies (numeric-PO↔WR cluster, "NA"/integer serials). No code changes. (mhammontree)
|
|
190
234
|
- 2026-06-10 — Documented the NYC DOE ServiceNow/ASN integration (ticket mirror, dual ASN ingestion, NetSuite freeze pipeline, EDI surface, dedupeKey gotchas). (mhammontree)
|
|
191
235
|
|
|
192
236
|
## Related docs
|
package/package.json
CHANGED