toga-ai 1.0.722 → 1.0.724
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/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +32 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +3 -2
- package/knowledge/2.0/apps/_underscore/features/calculated-sql-fields.md +9 -1
- package/knowledge/2.0/apps/_underscore/features/carrier-shipping-labels.md +61 -1
- package/knowledge/2.0/apps/_underscore/features/shipping-carriers-and-accounts.md +169 -0
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +35 -2
- package/knowledge/2.0/apps/toga2-supply/features/fulfill-and-ship.md +27 -1
- package/knowledge/2.0/standards/backend-php.md +52 -1
- package/knowledge/clients/growrk/INDEX.md +1 -0
- package/knowledge/clients/growrk/features/shipping-carrier-cleanup.md +81 -0
- package/knowledge/clients/growrk/profile.md +10 -1
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +17 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php
|
|
23
23
|
related:
|
|
24
24
|
- ../architecture.md
|
|
25
|
+
- ../../../2.0/apps/_underscore/features/shipping-carriers-and-accounts.md
|
|
25
26
|
- forecast2-netsuite-reconciliation.md
|
|
26
27
|
- ../workflows/onboarding-client-to-netsuite-togasupply-sync.md
|
|
27
28
|
- ../../library/features/toga2-api-client-and-bridge.md
|
|
@@ -336,6 +337,27 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
336
337
|
|
|
337
338
|
## Gotchas / known issues
|
|
338
339
|
|
|
340
|
+
- **⚠ A lookup seeded as a BY-PRODUCT of another list will miss records — this created runaway
|
|
341
|
+
duplicate `ShippingCarriers` rows (fixed 2026-09-01, TRUE-81576).**
|
|
342
|
+
`$lookupShippingCarrierByClientUuidAndShippingCarrierNameUpper` was built only inside the
|
|
343
|
+
`/shipping-methods` loop, harvesting each method's parent carrier, so **a carrier with ZERO
|
|
344
|
+
shipping methods never entered the lookup**. The invoice sync's `!isset($lookup[$name])` check in
|
|
345
|
+
`library/app/api/toga2.php` (~L3194, fed by `App_Misc::getCarrierFromTrackingNumber`) then created
|
|
346
|
+
it **again every run** — and the new row also had no methods, so it stayed invisible. Unbounded
|
|
347
|
+
growth, one row per 5-minute sync. **The tell:** only **USPS and DHL** duplicated (17x / 6x on
|
|
348
|
+
GroWrk) — the two carriers with zero methods; FedEx (7 methods) and UPS (4) had exactly one row
|
|
349
|
+
each. Now seeded directly from a paged `GET /shipping-carriers`
|
|
350
|
+
(`MAX_RECORDS_PER_PAGE_TO_FETCH_FROM_TOGA_API` = 1000), blank names skipped. Full write-up:
|
|
351
|
+
[Shipping Carriers & Carrier Account Numbers](../../../2.0/apps/_underscore/features/shipping-carriers-and-accounts.md).
|
|
352
|
+
Same lesson as the manufacturer/catalog bug below: **initialise and seed a lookup from its own
|
|
353
|
+
source, not from a neighbouring loop.**
|
|
354
|
+
- **⚠ A blank-named carrier matched EVERYTHING** — the matcher in `library/app/api/toga2.php` uses
|
|
355
|
+
`stripos($name, $carrierName) === 0`, and under PHP 8 an **empty needle returns `int(0)`**. The
|
|
356
|
+
placeholder became a catch-all and, having no account numbers, was invisible in the UI. Blank
|
|
357
|
+
names are now skipped **and** the creation branch reuses the existing blank placeholder instead of
|
|
358
|
+
POSTing a new `name => ''` carrier each run. **Those two changes must ship together** — skipping
|
|
359
|
+
blank names alone would make the placeholder permanently unmatchable and increase creation.
|
|
360
|
+
|
|
339
361
|
- **A client with NEITHER a manufacturer NOR a catalog on its items used to crash the whole run
|
|
340
362
|
(fixed 2026-08-12).** `$lookupItemByClientUuidAndPartNumberUpper[$uuidClient]` and
|
|
341
363
|
`$lookupAllCatalogItemsByClientUuidAndPartNumberUpper[$uuidClient]` were only keyed **inside** the
|
|
@@ -821,6 +843,15 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
821
843
|
|
|
822
844
|
## Change history
|
|
823
845
|
|
|
846
|
+
- 2026-09-01 — **TRUE-81576: stopped runaway duplicate `ShippingCarriers` creation (worker + library).**
|
|
847
|
+
`common_sync_togasupply.php` now seeds the carrier lookup from its own paged `GET /shipping-carriers`
|
|
848
|
+
loop instead of harvesting carriers out of the `/shipping-methods` loop — a carrier with zero
|
|
849
|
+
shipping methods was absent from the lookup, so the invoice sync recreated it every run (GroWrk had
|
|
850
|
+
17 USPS + 6 DHL duplicates; NYCHH 100 carriers, Compass 98). In `library/app/api/toga2.php` the
|
|
851
|
+
carrier matcher now **skips blank names** (an empty `stripos` needle returns `int(0)` and matched
|
|
852
|
+
every ship method) and the creation branch **reuses the existing blank placeholder** rather than
|
|
853
|
+
POSTing another empty-named carrier. Committed on branch `TRUE-81576`, PRs open. (mhammontree)
|
|
854
|
+
|
|
824
855
|
- 2026-08-31 — **Item-receipt partial-receipt NULL-quantity fix (shared, `library`).**
|
|
825
856
|
`syncItemReceiptFromNetsuite` now **skips line items whose `quantity` is null** (unreceived PO lines on a
|
|
826
857
|
partial receipt) in both passes over `$nsItemReceipt->itemList->item` (payload-build ~L3376 + unit/serial
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
11
11
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
12
12
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
13
|
-
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
13
|
+
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/PurchaseOrder.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
14
14
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
15
15
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
|
|
16
16
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
|
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
42
42
|
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
|
|
43
43
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
44
|
+
| [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | `ShippingCarriers` is a **per-client** table (one row per carrier, per client DB) that the NetSuite syncs create on demand and that Fulfill & Ship reads to buil | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ShippingMethod.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, api2/Component/Api/V2/V2.php |
|
|
44
45
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
45
46
|
| [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | A resolver capability that makes a surface `FIELD` element carry its **input contract** — max length, validator name, and whether the caller may write it — inst | _underscore/Model/Core/Surface.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/AclFieldPermission.php, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, api2/Component/Api/V2/V2.php |
|
|
46
47
|
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Client/Language.php, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-12a - NavigationSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-12a - NavigationRoleGrants.sql, toga25-supply/surface-layer-guide.md |
|
|
47
48
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
48
|
-
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, _underscore/Model.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql |
|
|
49
|
+
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, _underscore/Model.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql |
|
|
49
50
|
| [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
|
|
50
51
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Client_Quad/2026-08-26a - BackfillQuadUnitLevelTrackingSingleTn.sql, dbchanges2/Client_Quad/2026-08-26b - BackfillQuadItemLevelTrackingSingleTn.sql, dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql |
|
|
51
52
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | _underscore/Model/Client/ServiceRequest.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql |
|
|
@@ -6,10 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [snaredla, jcardinal, bala]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model.php
|
|
13
|
+
- _underscore/Model/Client/PurchaseOrder.php
|
|
13
14
|
- _underscore/Model/Client/SalesOrder.php
|
|
14
15
|
- _underscore/Model/Client/ServiceRequest.php
|
|
15
16
|
- _underscore/Model/Elite/SalesOrder.php
|
|
@@ -210,6 +211,13 @@ characters — that is the standard of evidence for touching a field that runs o
|
|
|
210
211
|
[save-cascade stored-field deadlocks](./model-save-parent-cascade-stored-field-deadlock.md).
|
|
211
212
|
|
|
212
213
|
## Change history
|
|
214
|
+
- 2026-09-01 — Added `_shipTo` to the base `_Model_Client_PurchaseOrder` (`public $_shipTo =
|
|
215
|
+
self::FIELD_SQL;` + static `_shipTo($table = self::TABLE)` returning a correlated subquery
|
|
216
|
+
`SELECT Addresses.addressee FROM Addresses WHERE Addresses.id = $table.shipToAddressId`),
|
|
217
|
+
paralleling `SalesOrders._shipTo`. Registered here via a **`Core.RecordFields`** row (id 2555,
|
|
218
|
+
recordId 17 `purchase-orders`, type STRING, mirroring `SalesOrders._shipTo` id 626) —
|
|
219
|
+
i.e. the Core-registration path, not the `CustomRecordFields`-only path this doc shows is also
|
|
220
|
+
sufficient. Additive and shared: it renders only where a view references it. (jcardinal)
|
|
213
221
|
- 2026-08-26 - Documented **how to override a calculated field for one client / sub-client**: api2
|
|
214
222
|
resolves the model class by `str_replace('_Model_Client_', '_Model_' . $clientIdentifier . '_', ...)`
|
|
215
223
|
(`Component/Api/V2/V2.php`), and because the expression is invoked as
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [mhammontree, tcox, bala]
|
|
11
11
|
files:
|
|
12
12
|
- test/@Mark/true-80824-fedex-inflate-test.php
|
|
@@ -28,6 +28,8 @@ files:
|
|
|
28
28
|
- _underscore/Cloud.php
|
|
29
29
|
related:
|
|
30
30
|
- ../architecture.md
|
|
31
|
+
- shipping-carriers-and-accounts.md
|
|
32
|
+
- address-validation.md
|
|
31
33
|
- field-storage-row-hydration.md
|
|
32
34
|
- recursive-item-fulfillments.md
|
|
33
35
|
- ../../toga2-supply/features/fulfill-and-ship.md
|
|
@@ -336,6 +338,54 @@ framework-level note is pending on the `_underscore` architecture doc.
|
|
|
336
338
|
**unconditionally**, so a *dirty read* (reading a storage field after assigning but before saving)
|
|
337
339
|
still discards the unsaved in-memory value.
|
|
338
340
|
|
|
341
|
+
## Account association: why a valid credential still fails (resolved 2026-09-01, TRUE-81576)
|
|
342
|
+
|
|
343
|
+
**Carrier API credentials are GLOBAL; account numbers are PER-CLIENT.** `_Config::fedex(...)` and
|
|
344
|
+
`_Config::ups(...)` read one platform-wide config group (`[fedex]` / `[ups]` in
|
|
345
|
+
`api2/Config/production.ini`) and **neither carries an account number** — that comes only from the
|
|
346
|
+
client's `ShippingCarrierAccountNumbers` row. So **a client's own carrier account works only if the
|
|
347
|
+
carrier has authorised TOGA's API credentials to bill it.** See
|
|
348
|
+
[Shipping Carriers & Carrier Account Numbers](shipping-carriers-and-accounts.md).
|
|
349
|
+
|
|
350
|
+
**The long-running FedEx `SHIPMENT.ACCOUNTNUMBER.UNAUTHORIZED` failure (`ship/v1/shipments`) was an
|
|
351
|
+
account-association problem, not a code bug.** The FedEx API developer account is
|
|
352
|
+
**`jcardinal@goagilant.com`**, whose credentials live in **1Password** — that is where Fulfill &
|
|
353
|
+
Ship's authorised account ID is configured. Using that account ID **succeeded in production**.
|
|
354
|
+
(Location only — no credential values or account numbers belong in this doc.)
|
|
355
|
+
|
|
356
|
+
### ⚠ Address-validation success is NOT evidence the Ship API works
|
|
357
|
+
|
|
358
|
+
The `/addresses/validate` USPS→FedEx→UPS cascade can succeed while `ship/v1/shipments` rejects the
|
|
359
|
+
account: **different carrier API surfaces, separate entitlements.** This is exactly what made a ship
|
|
360
|
+
path that had **never worked in production** look like a regression, and it cost significant
|
|
361
|
+
investigation time. Supporting detail: the config carries **separate
|
|
362
|
+
`tracking_client_id` / `tracking_client_secret`** from the ship credentials, implying tracking and
|
|
363
|
+
shipping are provisioned as **different FedEx projects** — an account attached to the tracking
|
|
364
|
+
project but not the ship project produces exactly this error.
|
|
365
|
+
|
|
366
|
+
### Two diagnostic shortcuts worth remembering
|
|
367
|
+
|
|
368
|
+
- **Reaching a FedEx BUSINESS-level error proves the OAuth token was issued.**
|
|
369
|
+
`Fedex::getAccessToken()` throws `'Invalid FedEx API Response'` on **any non-200** from
|
|
370
|
+
`/oauth/token`, so a business error means the **credentials are valid** and only the account link
|
|
371
|
+
is missing.
|
|
372
|
+
- **UPS's equivalent is `"Missing or invalid shipper number"`.** That string exists **nowhere in our
|
|
373
|
+
codebase** (verified by grep), so it is UPS's own message and proves the account **was
|
|
374
|
+
transmitted**. Our own guard reads *"A carrier account number is required to generate a shipping
|
|
375
|
+
label."* (`Model/Client/ItemFulfillment.php` ~L794.)
|
|
376
|
+
|
|
377
|
+
### Where carrier error history actually lives
|
|
378
|
+
|
|
379
|
+
- **FedEx has NO outbound log row** — the ship call runs `$api->setLogging(false)`
|
|
380
|
+
(`Fedex.php` ~L280), deliberate because a successful ship response carries the full base64 label
|
|
381
|
+
(megabytes).
|
|
382
|
+
- The **inbound** `/v2/item-fulfillments/fedexShipmentApi` call **is** recorded in `Logs.Api`
|
|
383
|
+
(`responseCode` 200, with the carrier error inside `responsePayload`) — **that is the only complete
|
|
384
|
+
history of label attempts.**
|
|
385
|
+
- **UPS outbound IS logged** (`direction = OUT`, `hostname LIKE '%ups.com%'`).
|
|
386
|
+
- A carrier failure returns **HTTP 200 with an `error` key in `data`**, so it will **not** show up in
|
|
387
|
+
a 5xx search.
|
|
388
|
+
|
|
339
389
|
## Gotchas / known issues
|
|
340
390
|
|
|
341
391
|
- **⚠ `[ups] label_format` must be set in EVERY environment's api2 config.** Without it **production
|
|
@@ -444,6 +494,16 @@ still discards the unsaved in-memory value.
|
|
|
444
494
|
bill) — expect a hard error rather than a silently-wrong bill in those cases.
|
|
445
495
|
|
|
446
496
|
## Change history
|
|
497
|
+
- 2026-09-01 — TRUE-81576: **resolved the long-standing FedEx `SHIPMENT.ACCOUNTNUMBER.UNAUTHORIZED`** —
|
|
498
|
+
it was an **account-association** problem, not code. The authorised account ID belongs to the FedEx
|
|
499
|
+
developer account `jcardinal@goagilant.com` (credentials in **1Password**); using it succeeded in
|
|
500
|
+
production. Recorded the architecture behind it (carrier **credentials are global**, **account
|
|
501
|
+
numbers per-client** in `ShippingCarrierAccountNumbers`), the trap that **address-validation success
|
|
502
|
+
proves nothing about the Ship API** (separate entitlements; separate `tracking_client_*` creds imply
|
|
503
|
+
separate FedEx projects), the two diagnostic shortcuts (a FedEx *business* error proves the OAuth
|
|
504
|
+
token issued; UPS's "Missing or invalid shipper number" is UPS's own string, not ours), and where
|
|
505
|
+
carrier error history actually lives (FedEx: inbound `Logs.Api` only; UPS: outbound logged; failures
|
|
506
|
+
return HTTP 200). TRUE-80824's gzip-decode fix is what made this error readable at all. (mhammontree)
|
|
447
507
|
- 2026-08-24 — Added **batch tracking-status polling** to the carrier libs for the platform-wide
|
|
448
508
|
[tracking-status refresh](../../worker2/features/tracking-status-refresh.md):
|
|
449
509
|
`Fedex::getStatuses()` (~30/call) and `Usps::getStatuses()` (~10/call), reusing the single-shipment
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: [mhammontree]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
13
|
+
- _underscore/Component/Library/Carriers/Fedex/Fedex.php
|
|
14
|
+
- _underscore/Component/Library/Carriers/Ups/Ups.php
|
|
15
|
+
- _underscore/Model/Client/ItemFulfillment.php
|
|
16
|
+
- _underscore/Model/Client/ShippingMethod.php
|
|
17
|
+
- library/app/api/toga2.php
|
|
18
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
19
|
+
- toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts
|
|
20
|
+
- api2/Component/Api/V2/V2.php
|
|
21
|
+
related:
|
|
22
|
+
- carrier-shipping-labels.md
|
|
23
|
+
- address-validation.md
|
|
24
|
+
- ../../toga2-supply/features/fulfill-and-ship.md
|
|
25
|
+
- ../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
|
|
26
|
+
- ../../../clients/growrk/features/shipping-carrier-cleanup.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Summary
|
|
30
|
+
|
|
31
|
+
`ShippingCarriers` is a **per-client** table (one row per carrier, per client DB) that the
|
|
32
|
+
NetSuite syncs create on demand and that Fulfill & Ship reads to build its Carrier dropdown.
|
|
33
|
+
Two independent code paths create carrier rows — the 1.0 worker sync
|
|
34
|
+
(`library/app/api/toga2.php`) and the 2.0 NetSuite sales-order trait
|
|
35
|
+
(`_underscore/Trait/Netsuite/SalesOrder.php`) — and both got the "does it already exist?"
|
|
36
|
+
question wrong in different ways, which produced **runaway duplicate rows** and a
|
|
37
|
+
**blank-named catch-all carrier** in production. TRUE-81576 fixed both.
|
|
38
|
+
|
|
39
|
+
## Data model
|
|
40
|
+
|
|
41
|
+
- **`ShippingCarriers`** — `id`, `uuid`, `name`. Rows are auto-created by the syncs.
|
|
42
|
+
- **`ShippingMethods.shippingCarrierId`** — the carrier's ship methods (`code` = the carrier
|
|
43
|
+
service code, e.g. UPS `03` Ground / `01` Next Day Air).
|
|
44
|
+
- **`ShippingCarrierAccountNumbers`** — `id`, `uuid`, `shippingCarrierId` (FK), `name`,
|
|
45
|
+
`accountNumber`. **This is the only place a per-client carrier account lives.**
|
|
46
|
+
- `ShippingCarriers.defaultShippingCarrierAccountNumberId` — the client's default account.
|
|
47
|
+
|
|
48
|
+
### Credentials are GLOBAL; account numbers are PER-CLIENT
|
|
49
|
+
|
|
50
|
+
Both carrier clients read **one platform-wide config group** — `_Config::fedex(...)` /
|
|
51
|
+
`_Config::ups(...)`, defined as `[fedex]` and `[ups]` in `api2/Config/production.ini` — and
|
|
52
|
+
**neither group contains an account number**. The shipper/account number comes **only** from
|
|
53
|
+
`ShippingCarrierAccountNumbers`. Consequence: **a client's own carrier account only works if the
|
|
54
|
+
carrier has authorised TOGA's API credentials to bill it.** A perfectly valid account number that
|
|
55
|
+
is not linked to our API app fails at ship time. This is the architectural reason behind both the
|
|
56
|
+
FedEx and UPS account failures documented in [carrier shipping labels](carrier-shipping-labels.md).
|
|
57
|
+
|
|
58
|
+
### No account number means invisible in the UI
|
|
59
|
+
|
|
60
|
+
The Fulfill & Ship **Carrier dropdown only lists carriers with at least one
|
|
61
|
+
`ShippingCarrierAccountNumbers` row**. A carrier with no account row is unselectable no matter how
|
|
62
|
+
it is configured. (GroWrk has FedEx and UPS accounts and no USPS account, so USPS cannot be
|
|
63
|
+
picked there.)
|
|
64
|
+
|
|
65
|
+
## Runaway duplicate carriers — root cause (fixed TRUE-81576)
|
|
66
|
+
|
|
67
|
+
`worker/crons/toga2/netsuite/common_sync_togasupply.php` built
|
|
68
|
+
`$lookupShippingCarrierByClientUuidAndShippingCarrierNameUpper` **only as a by-product of its
|
|
69
|
+
`/shipping-methods` loop**, harvesting each method's parent carrier. **A carrier with ZERO
|
|
70
|
+
shipping methods therefore never entered the lookup.** The invoice sync's
|
|
71
|
+
`!isset($lookup[$name])` check in `library/app/api/toga2.php` (~L3194, fed by
|
|
72
|
+
`App_Misc::getCarrierFromTrackingNumber`) then created the carrier **again** — and the new row
|
|
73
|
+
also had no methods, so it stayed invisible to the next run. **One extra row per sync, forever.**
|
|
74
|
+
|
|
75
|
+
**The diagnostic that cracked it:** the duplicates were **only USPS and DHL** (17x and 6x on
|
|
76
|
+
GroWrk) — exactly the two carriers with **zero** shipping methods. FedEx (7 methods) and UPS
|
|
77
|
+
(4 methods) had **exactly one row each** and were never duplicated. That asymmetry disproved an
|
|
78
|
+
earlier pagination theory and pointed straight at the lookup's source. Scale found in the sweep:
|
|
79
|
+
GroWrk 28 carriers, NYC Health & Hospitals 100, Compass 98, Quad 11, AIG/Rate/Elite 4.
|
|
80
|
+
|
|
81
|
+
**Fix:** a paged `GET /shipping-carriers` loop that **seeds the lookup directly**, using the same
|
|
82
|
+
`for ($page = 1, $nextPage = 0; ...)` idiom and `MAX_RECORDS_PER_PAGE_TO_FETCH_FROM_TOGA_API`
|
|
83
|
+
(1000) as the neighbouring lookups. Blank names are skipped — they are unusable as keys.
|
|
84
|
+
|
|
85
|
+
## A blank-named carrier is a WILDCARD (PHP stripos with an empty needle)
|
|
86
|
+
|
|
87
|
+
The carrier matcher keys on `stripos($nsShippingMethodName, $carrierName) === 0`. Under PHP 8 an
|
|
88
|
+
**empty needle returns `int(0)`** — verified directly:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
php -r 'var_dump(stripos("UPS Ground", ""));' // int(0)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
So a carrier row with a blank `name` **matched every ship-method name** and became a catch-all.
|
|
95
|
+
Compounding it: a blank-named carrier has no account numbers, and the UI filters the dropdown to
|
|
96
|
+
carriers with at least one account — so every method swallowed by the placeholder became
|
|
97
|
+
**unreachable in the UI**. Blank names are now **skipped in both matchers**
|
|
98
|
+
(`Trait/Netsuite/SalesOrder.php` and `library/app/api/toga2.php`). The added `(string)` cast also
|
|
99
|
+
removes a PHP 8.1 null-to-non-nullable deprecation on a null `->name`.
|
|
100
|
+
|
|
101
|
+
### The skip and the placeholder-reuse MUST ship together
|
|
102
|
+
|
|
103
|
+
The creation branch used to `POST` a **new** carrier with `name => ''` every time nothing matched,
|
|
104
|
+
instead of reusing the placeholder it created on the previous run. It now reuses the existing
|
|
105
|
+
blank placeholder. **Skipping blank names WITHOUT the reuse would leave the placeholder
|
|
106
|
+
permanently unmatchable and INCREASE row creation.**
|
|
107
|
+
|
|
108
|
+
### Resolve the placeholder BEFORE the shipping-method lookup
|
|
109
|
+
|
|
110
|
+
(Found in review by php-reviewer.) *Where* the blank-placeholder fallback is resolved changes
|
|
111
|
+
matching semantics. Resolving it **after** the shipping-method lookup leaves `$shippingCarrier`
|
|
112
|
+
null through that lookup, which (a) drops the `ShippingCarriers` join added by
|
|
113
|
+
`if ($shippingCarrier)` and (b) short-circuits the uuid comparison via
|
|
114
|
+
`is_null($shippingCarrier) || ...`. The lookup then degrades into a **carrier-blind global name
|
|
115
|
+
match** that can bind this NetSuite ship-method id onto a *real* carrier's identically-named
|
|
116
|
+
method (an unmapped "Ground" landing on **UPS Ground**). Resolve the placeholder **before** the
|
|
117
|
+
method lookup so the query stays scoped exactly as before. The `$nsShippingMethodName`
|
|
118
|
+
carrier-prefix strip is unaffected either way (`strlen('')` = 0 was already a no-op).
|
|
119
|
+
|
|
120
|
+
## An unpaginated V2 list GET SILENTLY TRUNCATES
|
|
121
|
+
|
|
122
|
+
`api2/Component/Api/V2/V2.php`: `DEFAULT_RECORDS_PER_PAGE = 25`, `MAX_RECORDS_PER_PAGE = 10000`,
|
|
123
|
+
and results come back **ordered by uuid**. There is **no error and no warning** — records simply
|
|
124
|
+
are not in the response.
|
|
125
|
+
|
|
126
|
+
**Concrete impact:** GroWrk had 30 carriers; **UPS** (uuid `db26a116...`) sorted *after* the
|
|
127
|
+
page-1 cutoff (`d513f1ff...`) and was dropped. UPS was therefore unselectable in the Fulfill &
|
|
128
|
+
Ship carrier dropdown **and** invisible to the 2.0 sync's carrier matcher — which also meant the
|
|
129
|
+
UPS service codes populated under TRUE-80824 (`03` Ground / `01` Next Day Air) were never
|
|
130
|
+
reachable. Compass (98) and NYCHH (100) are 4x over the default and were affected the same way.
|
|
131
|
+
|
|
132
|
+
**Fix:** `recordsPerPage => 1000` on the `_underscore` carrier lookup; `recordsPerPage: 1000` plus
|
|
133
|
+
`sort: ["name"]` on toga2-supply's `getShippingCarriers()`.
|
|
134
|
+
|
|
135
|
+
**Convention note (toga2-supply):** the `sort` option is a **flat array of field-name strings**,
|
|
136
|
+
with a `-` prefix for descending (e.g. `sort: ["-dateOrder"]`) — **not** objects like
|
|
137
|
+
`[{ name: "ASC" }]`. `ExtendedOptionsType` is `any`, so **tsc will not catch a wrong shape.**
|
|
138
|
+
|
|
139
|
+
## Delete-safety — every column that references a carrier
|
|
140
|
+
|
|
141
|
+
Before deleting a `ShippingCarriers` row in a client DB, check **all** of:
|
|
142
|
+
|
|
143
|
+
- `TrackingNumbers.shippingCarrierId` (**direct**)
|
|
144
|
+
- `TrackingNumbers.shippingCarrierAccountNumberId`
|
|
145
|
+
- `InvoiceTrackingNumbers.shippingCarrierId`
|
|
146
|
+
- `ShippingMethods.shippingCarrierId`
|
|
147
|
+
- `ShippingCarriers.defaultShippingCarrierAccountNumberId`
|
|
148
|
+
|
|
149
|
+
**`TrackingNumbers` reaches a carrier BOTH directly and via `shippingMethodId`, so a single-path
|
|
150
|
+
check is not sufficient.** Rows with any reference must be repointed to the canonical carrier
|
|
151
|
+
first. See the [GroWrk carrier cleanup](../../../clients/growrk/features/shipping-carrier-cleanup.md).
|
|
152
|
+
|
|
153
|
+
## Gotchas / known issues
|
|
154
|
+
|
|
155
|
+
- **Cleaning duplicates before the fix is deployed is wasted work** — the syncs refill the rows on
|
|
156
|
+
the next run (every 5 minutes). Deploy first, clean second.
|
|
157
|
+
- Compass (98 carriers) and NYC Health & Hospitals (100) are **not** cleaned yet — separate tasks.
|
|
158
|
+
- The `[fedex]` config group contains a key literally named **`_`** with a 32-character value.
|
|
159
|
+
Purpose unknown; no reader has been traced.
|
|
160
|
+
|
|
161
|
+
## Change history
|
|
162
|
+
- 2026-09-01 — TRUE-81576: root-caused and fixed **runaway duplicate `ShippingCarriers`** (the 1.0
|
|
163
|
+
sync lookup was seeded only from `/shipping-methods`, so a method-less carrier was recreated
|
|
164
|
+
every run) by seeding from a paged `GET /shipping-carriers`; **skipped blank carrier names in
|
|
165
|
+
both matchers** (an empty `stripos` needle matches everything) and made the creation branch
|
|
166
|
+
**reuse** the blank placeholder instead of POSTing a new one; resolved the placeholder **before**
|
|
167
|
+
the shipping-method lookup to keep that query carrier-scoped; and set `recordsPerPage` on both
|
|
168
|
+
carrier list GETs after an unpaginated 25-row page silently dropped GroWrk's UPS row.
|
|
169
|
+
(mhammontree)
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [bala, apeterson]
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: [bala, apeterson, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/TableView.php
|
|
13
13
|
- _underscore/Model.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
18
18
|
- dbchanges2/Client_CompassCanada/2026-08-27a - ItemFulfillmentTableViewsRebuild.sql
|
|
19
19
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
20
|
+
- dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
|
|
20
21
|
related:
|
|
21
22
|
- ../../api2/features/tableview-field-metadata.md
|
|
22
23
|
- ./page-meta-context-field-settings.md
|
|
@@ -193,6 +194,28 @@ grants of any kind — `Records` **351** (`transfer-order-stages`) had none befo
|
|
|
193
194
|
`Client_Nychh/2026-08-28a`, so the Status column of a brand-new view would have vanished silently.
|
|
194
195
|
Check the joined record's grants before assuming an existing record is grant-covered.
|
|
195
196
|
|
|
197
|
+
## A third mode: an INNER join to an EMPTY FK returns an EMPTY GRID — not an error
|
|
198
|
+
|
|
199
|
+
The two `meta()` failures above both throw. There is a third, quieter mode that is **not an
|
|
200
|
+
error at all**: a view loads, the header/meta looks right, and the data grid shows **zero rows**.
|
|
201
|
+
|
|
202
|
+
Cause: an **INNER** `TableViewJoins` on a foreign-key column that is **unpopulated for that
|
|
203
|
+
tenant** drops every row, because an inner join with no matching parent row returns nothing.
|
|
204
|
+
Confirmed 2026-09-01 on the NYCHH `purchase-orders` view: `Client_Nychh.PurchaseOrders` had **0 of
|
|
205
|
+
175 rows** with a populated `locationId`, so an INNER `Locations` join
|
|
206
|
+
(`Locations.id = PurchaseOrders.locationId`) emptied the whole grid. The meta API looked correct;
|
|
207
|
+
the data API just returned 0 results.
|
|
208
|
+
|
|
209
|
+
- **The blast radius is per tenant.** The same view can serve fine for a client whose FK is
|
|
210
|
+
populated and go empty for one whose FK is not — so it reads as "the grid is broken for this
|
|
211
|
+
client only", exactly like an ACL gap, but the join type is the cause.
|
|
212
|
+
- **Fix / avoid:** for an **optional / possibly-empty** FK, use **LEFT OUTER** (rows survive with
|
|
213
|
+
nulls in the joined columns) or omit the column entirely. Only use INNER when the FK is
|
|
214
|
+
guaranteed populated for the tenant.
|
|
215
|
+
- **Check before you add:** verify FK population in the target tenant
|
|
216
|
+
(`SELECT COUNT(*) ... WHERE <fk> IS NOT NULL`) before choosing INNER. NYCHH's `purchase-orders`
|
|
217
|
+
ultimately **dropped** the Location (and Ship To) column rather than switch to OUTER.
|
|
218
|
+
|
|
196
219
|
## Context RecordFieldSettings are OPTIONAL for a joined column to render
|
|
197
220
|
|
|
198
221
|
`meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
|
|
@@ -251,9 +274,19 @@ Core-level reference ids used (identical across all clients and environments): *
|
|
|
251
274
|
read the missing log row as "the request never happened".
|
|
252
275
|
- **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
|
|
253
276
|
is what breaks chained joins.
|
|
277
|
+
- **An INNER join to an EMPTY FK empties the whole grid with NO error.** Third failure mode: an
|
|
278
|
+
inner join on an FK that is unpopulated for the tenant drops every row (meta looks fine, data
|
|
279
|
+
returns 0). Use LEFT OUTER for an optional FK, or verify FK population before choosing INNER.
|
|
254
280
|
|
|
255
281
|
## Change history
|
|
256
282
|
|
|
283
|
+
- 2026-09-01 — Added a **third `meta()` failure mode that is not an error**: an **INNER** join to a
|
|
284
|
+
foreign key that is **unpopulated for the tenant** returns an **empty grid** (meta correct, data
|
|
285
|
+
0 rows), because an inner join with no matching parent row drops everything. Found on NYCHH's
|
|
286
|
+
`purchase-orders` view — `Client_Nychh.PurchaseOrders` had 0 of 175 rows with a populated
|
|
287
|
+
`locationId`, so an INNER `Locations` join emptied the grid. Rule: use LEFT OUTER (or omit the
|
|
288
|
+
column) for an optional/possibly-empty FK, and verify FK population in the target tenant before
|
|
289
|
+
using INNER. (jcardinal)
|
|
257
290
|
- 2026-08-28 — Separated the **two distinct ways `meta()` 500s**, after Compass Canada's
|
|
258
291
|
`item-fulfillments-for-sales-order(-item)s` views returned 500 while Compass USA's served fine.
|
|
259
292
|
(a) A join pointing at a **deleted `Core.Records` / `Core.RecordFields` row** throws from
|
|
@@ -6,7 +6,7 @@ project: TOGa Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-01
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- _underscore/Component/Library/Carriers/Ups/Ups.php
|
|
57
57
|
related:
|
|
58
58
|
- ../architecture.md
|
|
59
|
+
- ../../_underscore/features/shipping-carriers-and-accounts.md
|
|
59
60
|
- ../../_underscore/features/carrier-shipping-labels.md
|
|
60
61
|
- ../../_underscore/features/recursive-item-fulfillments.md
|
|
61
62
|
- ../../_underscore/features/netsuite-salesorder-address-phone-sync.md
|
|
@@ -402,6 +403,24 @@ the call 403s and returns no label.
|
|
|
402
403
|
|
|
403
404
|
## Gotchas / known issues
|
|
404
405
|
|
|
406
|
+
- **⚠ The Carrier dropdown silently lost UPS because the list GET was UNPAGINATED (fixed
|
|
407
|
+
2026-09-01, TRUE-81576).** api2's `DEFAULT_RECORDS_PER_PAGE` is **25** and list results come back
|
|
408
|
+
**ordered by uuid** — no error, no warning, records just are not there. GroWrk had 30 carriers and
|
|
409
|
+
**UPS** (uuid `db26a116...`) sorted after the page-1 cutoff (`d513f1ff...`), so UPS was unselectable
|
|
410
|
+
in the dropdown **and** invisible to the 2.0 sync's carrier matcher, which also made the TRUE-80824
|
|
411
|
+
UPS service codes (`03` Ground / `01` Next Day Air) unreachable. `getShippingCarriers()` now sends
|
|
412
|
+
**`recordsPerPage: 1000`** and **`sort: ["name"]`**. Compass (98) and NYCHH (100) were affected too.
|
|
413
|
+
- **⚠ The `sort` option is a FLAT ARRAY OF FIELD-NAME STRINGS**, `-` prefix for descending
|
|
414
|
+
(`sort: ["-dateOrder"]`) — **not** objects like `[{ name: "ASC" }]`. `ExtendedOptionsType` is `any`,
|
|
415
|
+
so **tsc will not catch a wrong shape**; it just silently does nothing.
|
|
416
|
+
- **⚠ The Carrier dropdown only lists carriers that have at least one
|
|
417
|
+
`ShippingCarrierAccountNumbers` row.** A carrier with no account number is invisible regardless of
|
|
418
|
+
configuration — GroWrk has FedEx and UPS accounts but no USPS account, so USPS cannot be selected.
|
|
419
|
+
- **⚠ Address validation succeeding tells you NOTHING about the Ship API.** `/addresses/validate`
|
|
420
|
+
(USPS→FedEx→UPS) and `ship/v1/shipments` are different carrier surfaces with separate entitlements;
|
|
421
|
+
a green address check next to a failing label buy is normal, not a contradiction. See
|
|
422
|
+
[carrier shipping labels](../../_underscore/features/carrier-shipping-labels.md).
|
|
423
|
+
|
|
405
424
|
- **⚠⚠ A config-driven validator MUST skip DISPLAY-ONLY fields — this caused a TOTAL production
|
|
406
425
|
submit outage (2026-07-29 → 2026-07-31, all clients).** `DUMMYUPDATESHIPMENTFIELDS.json` contains a
|
|
407
426
|
**display-only section header** (id 31, "Address") with **`valueKey: ""` and `isRequired: true`**.
|
|
@@ -820,6 +839,13 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
|
|
|
820
839
|
for Compass and is not yet in prod.
|
|
821
840
|
|
|
822
841
|
## Change history
|
|
842
|
+
- 2026-09-01 — TRUE-81576: **UPS reappeared in the Carrier dropdown.** `getShippingCarriers()` was an
|
|
843
|
+
unpaginated list GET, so api2's 25-row uuid-ordered default page dropped GroWrk's UPS carrier
|
|
844
|
+
entirely; now sends `recordsPerPage: 1000` and `sort: ["name"]`. Also documented that the dropdown
|
|
845
|
+
only shows carriers with a `ShippingCarrierAccountNumbers` row, that the `sort` option is a flat
|
|
846
|
+
array of field-name strings (typed `any`, so tsc will not catch a wrong shape), and that address
|
|
847
|
+
validation succeeding is not evidence the Ship API works. Verified in production alongside the
|
|
848
|
+
GroWrk carrier cleanup. (mhammontree)
|
|
823
849
|
- 2026-08-11 — TRUE-80824: **Reference 1 / Reference 2 / Phone # are now PREFILLED from NetSuite**
|
|
824
850
|
(still user-overridable, still not persisted) — **correcting the earlier SME note that they are
|
|
825
851
|
never prefilled**. Confirmed sources against production: Ref1 = `SalesOrders.number`; Ref2 =
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-09-01
|
|
9
9
|
owners: [jcardinal, mhammontree, dfranks, bala]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
@@ -547,6 +547,11 @@ clients/compass-usa/features/stranded-approval-reassignment.md
|
|
|
547
547
|
* A whole-file ending flip turns a one-line change into a full-file rewrite in `git diff`, burying the real change, breaking `git blame`, and inviting merge conflicts.
|
|
548
548
|
* **Check before committing:** if `git diff --stat` shows a tiny edit as a near-total rewrite, the endings flipped. Restore them — e.g. for an LF file: `tr -d '\r' < file > file.tmp && mv file.tmp file` — then re-stage.
|
|
549
549
|
|
|
550
|
+
#### These repos use CRLF line endings — normalise on programmatic writes
|
|
551
|
+
|
|
552
|
+
A scripted or tool-driven edit that writes LF into a CRLF file produces **mixed line endings** and a
|
|
553
|
+
diff that appears to touch the whole file. Any script that rewrites a source file must preserve CRLF.
|
|
554
|
+
|
|
550
555
|
#### **Braces**
|
|
551
556
|
|
|
552
557
|
* Opening braces for classes, methods and control structures (`if`, `else`, `for`, `while`, etc.) should go on the same line.
|
|
@@ -752,6 +757,22 @@ re-verify against `Controller/Index.php` before relying on it.
|
|
|
752
757
|
|
|
753
758
|
* Always use HTTPS to encrypt data in transit.
|
|
754
759
|
|
|
760
|
+
### NEVER ship a hardcoded-credential fallback — fail loudly instead
|
|
761
|
+
|
|
762
|
+
`_underscore/Component/Library/Carriers/Fedex/Fedex.php` `initialize()` carries a "production
|
|
763
|
+
fallback" branch with a **literal `client_id` and `client_secret`**. The sandbox branch above it is
|
|
764
|
+
dead code (`elseif (false && _Environment::isDebugMode())`). The hardcoded `client_id` was verified
|
|
765
|
+
programmatically to be **byte-identical** to the one in `api2/Config/production.ini` — i.e. a **live
|
|
766
|
+
production credential committed to git**.
|
|
767
|
+
|
|
768
|
+
Per the team security rule, a committed secret is compromised. Required actions:
|
|
769
|
+
1. Remove the fallback branch; throw when the `[fedex]` config group is unset.
|
|
770
|
+
2. Delete the dead `false && ...` sandbox branch.
|
|
771
|
+
3. Rotate the FedEx API key with FedEx.
|
|
772
|
+
|
|
773
|
+
Rule: **config absence is an error, not a reason to fall back to a baked-in value.** A missing
|
|
774
|
+
config group must throw at startup. Never commit a credential value, in any branch, dead or live.
|
|
775
|
+
|
|
755
776
|
## Performance Optimization
|
|
756
777
|
|
|
757
778
|
### Caching
|
|
@@ -870,6 +891,26 @@ client sending a shallow depth.
|
|
|
870
891
|
(TRUE-79533) — both read the same starved node. Relevant code: `api2/Component/Api/V2/V2.php`,
|
|
871
892
|
`api2/Controller/Index.php`.
|
|
872
893
|
|
|
894
|
+
### An unpaginated V2 list GET SILENTLY TRUNCATES — always send `recordsPerPage`
|
|
895
|
+
|
|
896
|
+
`api2/Component/Api/V2/V2.php`: `DEFAULT_RECORDS_PER_PAGE = 25`, `MAX_RECORDS_PER_PAGE = 10000`.
|
|
897
|
+
Omitting `recordsPerPage` returns only the first 25 rows with **no error and no warning** — the rest
|
|
898
|
+
simply are not there, and the caller cannot tell. **No meaningful ordering is guaranteed**: a
|
|
899
|
+
production response was observed coming back in uuid order, so which rows survive truncation is
|
|
900
|
+
effectively arbitrary and can change as data is added.
|
|
901
|
+
|
|
902
|
+
- Any list GET used to **build a lookup or populate a dropdown** must set `recordsPerPage`
|
|
903
|
+
explicitly (1000 is the established value; `MAX_RECORDS_PER_PAGE_TO_FETCH_FROM_TOGA_API` in the
|
|
904
|
+
1.0 sync) **or** loop pages with the `for ($page = 1, $nextPage = 0; ...)` idiom.
|
|
905
|
+
- Always sort by a **meaningful** field rather than relying on the default.
|
|
906
|
+
- Real cost: GroWrk's UPS carrier sorted past the page-1 cutoff and vanished from the Fulfill & Ship
|
|
907
|
+
carrier dropdown and from the NetSuite carrier matcher, which also made the UPS service codes
|
|
908
|
+
added under TRUE-80824 unreachable. Compass (98 carriers) and NYCHH (100) are 4× over the default.
|
|
909
|
+
|
|
910
|
+
**Frontend note (toga2-supply):** the `sort` option is a flat array of field-name strings with a `-`
|
|
911
|
+
prefix for descending (`sort: ["-dateOrder"]`), **not** objects. `ExtendedOptionsType` is `any`, so
|
|
912
|
+
tsc will not catch a wrong shape — it fails silently.
|
|
913
|
+
|
|
873
914
|
### API Versioning
|
|
874
915
|
|
|
875
916
|
* Implement a new version (ie: `/v1`, `/v2`, `/v3`) when a major API framework is introduced to manage the new API framework without breaking existing clients.
|
|
@@ -956,6 +997,16 @@ See: 2.0/apps/worker2/features/cross-account-aws-access.md
|
|
|
956
997
|
|
|
957
998
|
## Change history
|
|
958
999
|
|
|
1000
|
+
- 2026-09-01 — TRUE-81576. Three additions: (1) Security — never ship a hardcoded-credential
|
|
1001
|
+
fallback; the FedEx `initialize()` "production fallback" holds a live `client_id`/`client_secret`
|
|
1002
|
+
byte-identical to `api2/Config/production.ini`, so it must throw on missing `[fedex]` config, the
|
|
1003
|
+
dead `false && ...` sandbox branch must go, and the key must be rotated. (2) API Design — an
|
|
1004
|
+
unpaginated V2 list GET silently truncates at `DEFAULT_RECORDS_PER_PAGE = 25` with no error and no
|
|
1005
|
+
guaranteed ordering; always send `recordsPerPage` (or page the loop) for lookups/dropdowns. Cost:
|
|
1006
|
+
GroWrk's UPS carrier fell off page 1 and disappeared from the Fulfill & Ship dropdown and the
|
|
1007
|
+
NetSuite matcher, stranding the TRUE-80824 service codes. Includes the toga2-supply `sort`
|
|
1008
|
+
flat-string-array note (`ExtendedOptionsType` is `any`, so tsc catches nothing). (3) Code
|
|
1009
|
+
Formatting — programmatic writes must preserve CRLF. (mhammontree)
|
|
959
1010
|
- 2026-08-29 — Added a fail-loud cross-reference under **PHP Error Handling**: import/sync/cron
|
|
960
1011
|
code that reconciles external data must never catch-and-continue or log-and-skip a record;
|
|
961
1012
|
points to the full policy and the NetSuite → TOGa Supply sync data-loss incident recorded in
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [GroWrk — shipping-carrier cleanup, UPS visibility, and the NetSuite Fulfill-and-Ship punch-out URL](features/shipping-carrier-cleanup.md) | 2.0 | GroWrk (`Core.Clients` id **33**, DB `Client_Growrk`) is where the runaway `ShippingCarriers` duplication was found and fixed (TRUE-81576). | _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts |
|
|
5
6
|
| [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
7
|
| [GroWrk NetSuite-module custom fields — the never-executed 2026-05-28 commit (Units + ServiceRequests 500s)](features/units-netsuite-custom-fields.md) | 2.0 | GroWrk is on the dbchanges2 **`netsuite`** module (`dbchanges2/Client_Growrk/_modules.txt`), so its Units carry the module's seven `c_` custom fields. | _underscore/Trait/Netsuite/Unit.php, _underscore/Model/Growrk/Unit.php, _underscore/Model/Growrk/ItemFulfillment.php, dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
7
8
|
| [GroWrk](profile.md) | 2.0 | GroWrk is a 2.0 client using TOGA for inventory and order management. | |
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GroWrk — shipping-carrier cleanup, UPS visibility, and the NetSuite Fulfill-and-Ship punch-out URL"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: growrk
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-01
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Trait/Netsuite/SalesOrder.php
|
|
13
|
+
- library/app/api/toga2.php
|
|
14
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
15
|
+
- toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts
|
|
16
|
+
related:
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/shipping-carriers-and-accounts.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/carrier-shipping-labels.md
|
|
20
|
+
- ../../../2.0/apps/toga2-supply/features/fulfill-and-ship.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
GroWrk (`Core.Clients` id **33**, DB `Client_Growrk`) is where the runaway
|
|
26
|
+
`ShippingCarriers` duplication was found and fixed (TRUE-81576). GroWrk carried **28** carrier
|
|
27
|
+
rows — 17 duplicate USPS and 6 duplicate DHL, all with zero shipping methods. The mechanics are
|
|
28
|
+
shared and live in
|
|
29
|
+
[Shipping Carriers & Carrier Account Numbers](../../../2.0/apps/_underscore/features/shipping-carriers-and-accounts.md);
|
|
30
|
+
this doc records what was done to GroWrk's **data** and what is still parked.
|
|
31
|
+
|
|
32
|
+
## What was done (2026-09-01, production, by hand)
|
|
33
|
+
|
|
34
|
+
**20 fully-orphaned carrier rows** (zero shipping methods, zero account numbers, zero tracking
|
|
35
|
+
references) were **deleted manually in production**, taking GroWrk from **28 carriers to 8**.
|
|
36
|
+
Immediately after, **UPS became visible and selectable** in the Fulfill & Ship carrier dropdown,
|
|
37
|
+
and the NetSuite phone prefill from TRUE-80824 was confirmed working in the same test.
|
|
38
|
+
|
|
39
|
+
**Decision (Mark): treated as a one-off — no `dbchanges2` migration was written.** The code fixes
|
|
40
|
+
prevent recurrence, so a migration would only re-run a cleanup that cannot happen again. **Note
|
|
41
|
+
this leaves production ahead of `dbchanges2` for that table.**
|
|
42
|
+
|
|
43
|
+
## Parked follow-ups (not done in this ticket)
|
|
44
|
+
|
|
45
|
+
- **Carrier ids 15, 17 and 20 could NOT be deleted.** Each still has **one direct
|
|
46
|
+
`TrackingNumbers.shippingCarrierId` reference**. Those rows must be repointed to the canonical
|
|
47
|
+
**USPS (id 3)** first. (Remember `TrackingNumbers` reaches a carrier both directly and through
|
|
48
|
+
`shippingMethodId` — check both paths.)
|
|
49
|
+
- **GroWrk UPS shipping methods ids 11 and 12** ("Overnight Standard" / "Overnight Priority")
|
|
50
|
+
still have **NULL `code`**, and both are **FedEx product names sitting on UPS rows**.
|
|
51
|
+
- **Compass (98 carriers) and NYC Health & Hospitals (100) are not cleaned** — separate tasks, and
|
|
52
|
+
the cleanup must happen **after** this deploy or the rows refill.
|
|
53
|
+
|
|
54
|
+
## Known issue — the NetSuite "Fulfill and Ship" button opens spglobal.togasupply.com
|
|
55
|
+
|
|
56
|
+
**Not caused by anything in our repos.** Ruled out during this session:
|
|
57
|
+
|
|
58
|
+
- **No Fulfill & Ship SuiteScript exists anywhere in the workspace.** The only NetSuite user
|
|
59
|
+
scripts present are `NetSuite_Prudential_Bridge.js`, two forecast exports, and test-folder junk.
|
|
60
|
+
- **No PHP builds a `/shipment-items` or `/edit-shipment` URL.**
|
|
61
|
+
- **`Core.Domains` is correct for GroWrk** — id **400**, `clientId` 33, `appId` 1,
|
|
62
|
+
`environmentId` 1 → `https://growrk.togasupply.com`.
|
|
63
|
+
|
|
64
|
+
The URL is therefore built **inside NetSuite's File Cabinet**. Leading hypothesis: **S&P Global**
|
|
65
|
+
is `Core.Clients` id **4** with `Domains` ids 7-9 and was one of the earliest TOGa Supply tenants,
|
|
66
|
+
so the punch-out was likely built for them first and the target is **hardcoded or read from a
|
|
67
|
+
script parameter that is unset for the GroWrk subsidiary**. Where to look: the script on the Sales
|
|
68
|
+
Order / Item Fulfillment form, its script parameters, and any subsidiary-to-URL custom record.
|
|
69
|
+
|
|
70
|
+
## Data drift
|
|
71
|
+
|
|
72
|
+
`Core.Domains` id **403** (`growrk.alpha.togasupply.com`) has **NULL `clientId` AND NULL `appId`**,
|
|
73
|
+
unlike every other GroWrk row. Harmless in production; **it will misbehave on alpha.**
|
|
74
|
+
|
|
75
|
+
## Change history
|
|
76
|
+
- 2026-09-01 — TRUE-81576: deleted **20 orphaned `ShippingCarriers` rows** in production by hand
|
|
77
|
+
(28 → 8); UPS became selectable in Fulfill & Ship and the TRUE-80824 phone prefill was confirmed.
|
|
78
|
+
Recorded the parked items (carrier ids 15/17/20 blocked by direct `TrackingNumbers` references;
|
|
79
|
+
UPS methods 11/12 with NULL codes and FedEx names), the NetSuite punch-out URL pointing at
|
|
80
|
+
`spglobal.togasupply.com` (built in NetSuite, not our code), and the NULL `clientId`/`appId` drift
|
|
81
|
+
on `Core.Domains` id 403. Deliberately **no dbchanges2 migration** — one-off cleanup. (mhammontree)
|
|
@@ -13,12 +13,13 @@ project: _Underscore
|
|
|
13
13
|
client: growrk
|
|
14
14
|
type: profile
|
|
15
15
|
status: active
|
|
16
|
-
updated: 2026-
|
|
16
|
+
updated: 2026-09-01
|
|
17
17
|
owners: ["rgirish", "mhammontree", "bala", "jcardinal"]
|
|
18
18
|
files: []
|
|
19
19
|
related:
|
|
20
20
|
- clients/growrk/features/transfer-order-flow.md
|
|
21
21
|
- clients/growrk/features/units-netsuite-custom-fields.md
|
|
22
|
+
- clients/growrk/features/shipping-carrier-cleanup.md
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
## Summary
|
|
@@ -38,6 +39,14 @@ module (`dbchanges2/Client_Growrk/_modules.txt`).
|
|
|
38
39
|
|
|
39
40
|
## Gotchas
|
|
40
41
|
|
|
42
|
+
- **Carrier rows were duplicating every sync and UPS was invisible in Fulfill & Ship** (fixed
|
|
43
|
+
TRUE-81576, 2026-09-01). 20 orphaned `ShippingCarriers` rows were deleted **by hand in production**
|
|
44
|
+
(28 → 8) with **no dbchanges2 migration**, so production is ahead of `dbchanges2` for that table.
|
|
45
|
+
Carrier ids **15/17/20** are still blocked by direct `TrackingNumbers` references. Details:
|
|
46
|
+
[shipping-carrier cleanup](features/shipping-carrier-cleanup.md).
|
|
47
|
+
- **`Core.Domains` id 403 (`growrk.alpha.togasupply.com`) has NULL `clientId` AND NULL `appId`** —
|
|
48
|
+
real data drift; harmless in production, will misbehave on alpha.
|
|
49
|
+
|
|
41
50
|
- **`_Model_Growrk_Unit`'s `c_` columns must physically exist on `Client_Growrk.Units` — in EVERY
|
|
42
51
|
environment, production included.** The model body is empty: it is only
|
|
43
52
|
`extends _Model_Client_Unit` + `use _Trait_Netsuite_Unit`, and **the trait** declares all seven
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
| [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
|
|
7
7
|
| [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
8
8
|
| [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/PurchaseOrderItem.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
|
-
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
9
|
+
| [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
|
|
@@ -16,9 +16,10 @@ project: _Underscore
|
|
|
16
16
|
client: nychh
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-
|
|
19
|
+
updated: 2026-09-01
|
|
20
20
|
owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
|
|
21
21
|
files:
|
|
22
|
+
- dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
|
|
22
23
|
- dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
|
|
23
24
|
- dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql
|
|
24
25
|
- dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
|
|
@@ -48,6 +49,19 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
48
49
|
run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
|
|
49
50
|
|
|
50
51
|
## Key features (this client)
|
|
52
|
+
- **`purchase-orders` table view was rebased off the wrong record (2026-09-01).** The view was
|
|
53
|
+
based on **SalesOrders (Core record 14)** but had to be **PurchaseOrders (record 17)** — it was
|
|
54
|
+
mislabeled. `Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql` flips
|
|
55
|
+
`TableViews.recordId` 14→17, drops the SalesOrders→bridge→PurchaseOrders join chain, removes the
|
|
56
|
+
"Sales Order number" column (`SalesOrders.number`, RecordFields 49), and repoints the rest to PO
|
|
57
|
+
fields — final columns **number (base field 87), dateOrder (86), total (`_total` 1429)**. Location
|
|
58
|
+
and Ship To were intentionally **omitted**: PO `locationId` is empty for this tenant, so an INNER
|
|
59
|
+
Locations join would empty the grid (see the third failure mode in
|
|
60
|
+
[tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md)). The migration also
|
|
61
|
+
seeds a **Developer-role (role 2)** record ACL grant chain (`AclRecordPermissions` →
|
|
62
|
+
`AclLogicGroups` → `AclLogicGroupExpressions`, reusing record 17's `all` expression) and reads
|
|
63
|
+
`AclFieldPermissions` for the displayed fields, resolving `roleId` by role **name**. Written
|
|
64
|
+
idempotent / uuid-keyed; applied to **prod and client-sandbox**.
|
|
51
65
|
- **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
|
|
52
66
|
and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
|
|
53
67
|
Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
|
|
@@ -112,6 +126,8 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
112
126
|
[NYCHH PO Number direction](./features/po-number-upstream-direction.md).
|
|
113
127
|
|
|
114
128
|
## Notes
|
|
129
|
+
- **Role ids in `Client_Nychh` (verified 2026-09-01):** `1 = Base`, `2 = Developer`, `3 = API`.
|
|
130
|
+
Role ids differ per client DB — resolve by **name** in migrations, never hard-code these.
|
|
115
131
|
- **PO Number is upstream-sourced** (`PurchaseOrders_SalesOrders`), which is why the downstream
|
|
116
132
|
SO→PO route and the shared `_purchaseOrders` field came back empty. Details, and the
|
|
117
133
|
now-closed-on-`_beta` fix: [NYCHH PO Number](./features/po-number-upstream-direction.md).
|
package/package.json
CHANGED