toga-ai 1.0.649 → 1.0.650
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/INDEX.md +2 -0
- package/knowledge/2.0/apps/_underscore/features/sales-order-po-number-sourcing.md +88 -0
- package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +72 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/workflows/verifying-a-migration-ran.md +58 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/profile.md +1 -0
- package/knowledge/clients/nychh/INDEX.md +1 -0
- package/knowledge/clients/nychh/features/po-number-upstream-direction.md +59 -0
- package/knowledge/clients/nychh/profile.md +7 -0
- package/knowledge/clients/quad/INDEX.md +1 -0
- package/knowledge/clients/quad/features/enter-po-details-vendor-selection-ev12.md +62 -0
- package/knowledge/clients/quad/profile.md +2 -0
- package/package.json +1 -1
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
+
| [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, 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 |
|
|
41
|
+
| [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 |
|
|
40
42
|
| [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 |
|
|
41
43
|
| [_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 |
|
|
42
44
|
| [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 |
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Compass/SalesOrder.php
|
|
14
|
+
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
15
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
16
|
+
- dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql
|
|
17
|
+
- dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql
|
|
18
|
+
- dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql
|
|
19
|
+
related:
|
|
20
|
+
- ./sales-order-purchase-order-bridge-direction.md
|
|
21
|
+
- ./calculated-sql-fields.md
|
|
22
|
+
- ./surface-resolver.md
|
|
23
|
+
- ../../toga25-supply/features/surface-frontend.md
|
|
24
|
+
- ../../dbchanges2/features/surface-layer-schema.md
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. As of
|
|
30
|
+
2026-08-25 every tenant renders it the same way:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Core.SurfaceElements id 7 (Order Details "PO Number", messages.label = salesOrder.field.poNumber)
|
|
34
|
+
→ config.valueKey = "_purchaseOrders"
|
|
35
|
+
→ FE adapter toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
36
|
+
→ renders the order's `_purchaseOrders` calculated field (comma-joined string)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**All tenant variation lives underneath, in the SQL behind `_purchaseOrders`** — not in the
|
|
40
|
+
surface, not in the front end. This supersedes the earlier (TRUE-81190) understanding that the
|
|
41
|
+
*display path* itself was tenant-split.
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
|
|
45
|
+
### The display binding
|
|
46
|
+
`SurfaceElements` id 7 carries `config = {"valueKey": "_purchaseOrders"}`. `isPersonaValue` is
|
|
47
|
+
deliberately **not** set: the value is a scalar comma-joined string, not a persona array.
|
|
48
|
+
|
|
49
|
+
`surfaceBundleToTenantFields.ts` exposes `PO_NUMBER_VALUE_KEY` and resolves it against the order
|
|
50
|
+
via `resolvePathInsensitive(valueKey)`, normalising an empty string to an em-dash.
|
|
51
|
+
|
|
52
|
+
### The tenant layer (`_purchaseOrders`)
|
|
53
|
+
| Tenant | Implementation | Direction read |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **Compass USA** | overrides in `_underscore/Model/Compass/SalesOrder.php:238` — a `UNION ALL` of its own vendor POs plus Office Depot's POs traversed SO→PO→SO→PO across the multi-tier chain | downstream **+ upstream** (only tenant reading upstream) |
|
|
56
|
+
| **Quad Graphics** | no override → base implementation | downstream only |
|
|
57
|
+
| **Compass Canada** | no `Model/Compasscanada/` directory exists at all → base implementation | downstream only |
|
|
58
|
+
| base (`_Model_Client_SalesOrder:348`) | the shared calculated field | **downstream only** (`SalesOrders_PurchaseOrders`) |
|
|
59
|
+
|
|
60
|
+
### `poSelection` is a different thing
|
|
61
|
+
The `poSelection` config on the `sales-order-record-actions` surface picks **which** linked PO the
|
|
62
|
+
*editable* flow treats as primary (Enter-PO modal prefill, `approveButtonRule`, completion state).
|
|
63
|
+
Core default is `"first"`; Quad is overridden to `"mostRecent"` by
|
|
64
|
+
`Client_Quad/2026-08-19a`. It reads the **downstream join**, not `_purchaseOrders`. Do not conflate
|
|
65
|
+
the two — changing one does not move the other.
|
|
66
|
+
|
|
67
|
+
## Gotchas
|
|
68
|
+
|
|
69
|
+
- **A surface-config change is only real if the FE actually honours the binding.** Before
|
|
70
|
+
2026-08-25 the FE constant *matched* the surface's `valueKey` and then **discarded** it,
|
|
71
|
+
substituting a hardcoded field path. The config was read and thrown away — so shipping the
|
|
72
|
+
migration alone would have silently done nothing. Whenever you repoint a `valueKey`, grep the FE
|
|
73
|
+
for the old literal and confirm it resolves *through* the config, not around it.
|
|
74
|
+
- **Surface seed `id`s are deterministic; surface `uuid`s are not.** Seeds insert with `UUID()`, so
|
|
75
|
+
the element uuid differs per environment. A uuid quoted in an older migration will **not** match
|
|
76
|
+
the uuid in a live payload. Key surface updates on the literal `id` (here, `id = 7`).
|
|
77
|
+
- **Downstream-only base implementation is a trap for upstream tenants.** On an upstream-only
|
|
78
|
+
tenant `_purchaseOrders` resolves cleanly (no `EV-8`) and returns `null` forever. See
|
|
79
|
+
[SO↔PO bridge direction](./sales-order-purchase-order-bridge-direction.md) and
|
|
80
|
+
[NYCHH PO Number](../../../clients/nychh/features/po-number-upstream-direction.md).
|
|
81
|
+
- Dropping `isPersonaValue` is intentional; re-adding it will make the FE try to treat the string
|
|
82
|
+
as a persona array.
|
|
83
|
+
|
|
84
|
+
## Change history
|
|
85
|
+
- 2026-08-25 — Repointed `SurfaceElements` id 7 to `_purchaseOrders` for all clients
|
|
86
|
+
(`Core/2026-08-25a`, reverting `Core/2026-07-23a`) and made the FE adapter honour the surface
|
|
87
|
+
`valueKey` instead of a hardcoded path; recorded the corrected per-tenant map, which supersedes
|
|
88
|
+
the tenant-split display understanding (apeterson)
|
package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/PurchaseOrders/SalesOrder.php
|
|
13
|
+
- _underscore/Model/Client/SalesOrders/PurchaseOrder.php
|
|
14
|
+
- _underscore/Model/Client/ItemFulfillment.php
|
|
15
|
+
- _underscore/Model/Client/SalesOrder.php
|
|
16
|
+
related:
|
|
17
|
+
- ./sales-order-po-number-sourcing.md
|
|
18
|
+
- ./recursive-item-fulfillments.md
|
|
19
|
+
- ./fulfillable-item-propagation.md
|
|
20
|
+
- ../../api2/features/v2-rest-query-contract.md
|
|
21
|
+
- ../../../clients/nychh/features/po-number-upstream-direction.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
There are **two** bridge tables linking sales orders and purchase orders, and they mean
|
|
27
|
+
**opposite things**. Both names are legal under the bridge-table naming convention (sibling
|
|
28
|
+
tables may be named in either order), and the two API routes differ only in word order — so
|
|
29
|
+
**nothing at the API surface tells you which direction you are querying**. Querying the wrong
|
|
30
|
+
one returns `200` with an empty array, which reads like a permission or data bug and is not.
|
|
31
|
+
|
|
32
|
+
| Table | Direction | Meaning | Model | API route | `Core.Records` id |
|
|
33
|
+
|---|---|---|---|---|---|
|
|
34
|
+
| `PurchaseOrders_SalesOrders` | **UPSTREAM** | a sales order created **FROM** a purchase order — the *customer's* PO | `_Model_Client_PurchaseOrders_SalesOrder` | `purchase-order-sales-orders` | 276 |
|
|
35
|
+
| `SalesOrders_PurchaseOrders` | **DOWNSTREAM** | a purchase order created **FROM** a sales order — a *vendor* PO | `_Model_Client_SalesOrders_PurchaseOrder` | `sales-order-purchase-orders` | 275 |
|
|
36
|
+
|
|
37
|
+
Read the name as **"`<source>`\_`<thing created from it>`"**. The *first* table name is the
|
|
38
|
+
origin; the second is what was generated from it.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
- Both `Records` rows are `aclDatabase = CLIENT`, `childPolicy = MATCH_UPSERT`, so both are
|
|
43
|
+
per-tenant data and both accept nested writes that match-or-create the link row.
|
|
44
|
+
- Which direction a tenant actually populates depends on how their orders enter the platform:
|
|
45
|
+
- Customer sends TOGa a PO which becomes a sales order → **upstream** rows.
|
|
46
|
+
- TOGa raises vendor POs to fulfil a sales order → **downstream** rows.
|
|
47
|
+
- A multi-tier tenant (Compass USA) has **both**, chained SO→PO→SO→PO.
|
|
48
|
+
- The only in-code statement of the distinction today is a docblock at
|
|
49
|
+
`_underscore/Model/Client/ItemFulfillment.php:136-140`.
|
|
50
|
+
- The base `_purchaseOrders` calculated field on `_Model_Client_SalesOrder` (line ~348) reads the
|
|
51
|
+
**downstream** table only. See
|
|
52
|
+
[sales-order PO Number sourcing](./sales-order-po-number-sourcing.md).
|
|
53
|
+
|
|
54
|
+
## Gotchas
|
|
55
|
+
|
|
56
|
+
- **An empty array is the symptom.** `200` / `totalRecordCount 0` with **no** `EZ-*` or `EV-*`
|
|
57
|
+
message is the signature of *"the join matched nothing"* — i.e. wrong direction — not ACL and
|
|
58
|
+
not a missing migration. A permission failure would be `EZ-1`; an unknown field `EV-8`/`EV-9`.
|
|
59
|
+
- **Check both directions before debugging anything else.** Count rows in
|
|
60
|
+
`PurchaseOrders_SalesOrders` and `SalesOrders_PurchaseOrders` for the order. If upstream = 1 and
|
|
61
|
+
downstream = 0, the caller is on the wrong route. This diagnosis is one query and saves hours.
|
|
62
|
+
- **The routes are near-homographs.** `sales-order-purchase-orders` vs
|
|
63
|
+
`purchase-order-sales-orders`. Read them aloud before wiring a front-end call.
|
|
64
|
+
- **Downstream-only assumptions are baked into shared code.** Any tenant whose POs are purely
|
|
65
|
+
upstream (verified: NYCHH; suspected: Compass Canada, which has no `Model/Compasscanada/`
|
|
66
|
+
overrides at all) silently gets `null`/empty from the shared downstream-reading helpers rather
|
|
67
|
+
than an error. Upstream-only tenants need an explicit upstream counterpart field.
|
|
68
|
+
|
|
69
|
+
## Change history
|
|
70
|
+
- 2026-08-25 — Documented the two-direction bridge after an NYCHH "empty array" debug that was
|
|
71
|
+
purely a wrong-direction query; recorded route/model/record-id mapping and the empty-vs-EZ
|
|
72
|
+
diagnostic signature (apeterson)
|
|
@@ -8,3 +8,4 @@
|
|
|
8
8
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.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/_modules.txt |
|
|
9
9
|
| [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
|
|
10
10
|
| [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Aig/_modules.txt, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql |
|
|
11
|
+
| [Verifying whether a dbchanges2 migration actually ran in an environment](workflows/verifying-a-migration-ran.md) | dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered from the **rows the file would have produced**. | Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Verifying whether a dbchanges2 migration actually ran in an environment"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
|
|
13
|
+
- Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql
|
|
14
|
+
related:
|
|
15
|
+
- ../architecture.md
|
|
16
|
+
- ../features/surface-layer-schema.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered
|
|
22
|
+
from the **rows the file would have produced**. This procedure gets a reliable answer, and names
|
|
23
|
+
the two ways the naive answer is wrong.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **Preferred — match the hardcoded uuid.** If the migration inserts a literal v4 uuid, select on
|
|
28
|
+
it. Present ⇒ this file ran. This is the only check that identifies the *file* rather than the
|
|
29
|
+
*effect*.
|
|
30
|
+
2. **Fallback — match the structural signature.** When the uuid column is unavailable or the row
|
|
31
|
+
was seeded with `UUID()`, select on the **exact column-value combination** the file writes.
|
|
32
|
+
Worked example — `Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql` was confirmed on
|
|
33
|
+
`Client_Nychh` by a `TableViewJoins` row with `sortOrder = 23`, `joinRecordId = 15`,
|
|
34
|
+
`joinOnRecordFieldId = 60`, `parentRecordFieldId = 368`, `type = OUTER`, on the tableview slug
|
|
35
|
+
`units-for-items-for-purchase-orders`.
|
|
36
|
+
3. **For a guarded file, find its distinguishing side-effect.** Pick something in the file that a
|
|
37
|
+
pre-existing row would *not* already have — typically the ACL grant pair it adds. Worked example
|
|
38
|
+
— `Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql` was confirmed on `Client_Nychh` by the
|
|
39
|
+
grant pair **exactly Base + API, `isWritable = 0`**; Compass already had `_purchaseOrders` as
|
|
40
|
+
`CustomRecordFields` id 77 under a *different* uuid, so the field row alone proved nothing.
|
|
41
|
+
4. **State the conclusion as "the effect is present in env X"**, not "the file ran", unless step 1
|
|
42
|
+
succeeded.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- **A guarded `INSERT ... WHERE NOT EXISTS` is a NO-OP where the row already exists under a
|
|
47
|
+
different uuid.** Therefore: *row absent* ≠ "the file did not run", and *row present* ≠ "this
|
|
48
|
+
file created it". Both directions of the naive inference are unsound.
|
|
49
|
+
- Seed rows created with `UUID()` differ per environment, so cross-environment uuid comparison is
|
|
50
|
+
meaningless. Compare **ids and structure**, not uuids. (Same reason surface migrations key on the
|
|
51
|
+
literal `id` — see [Surface Layer Schema](../features/surface-layer-schema.md).)
|
|
52
|
+
- Fan-out folders (`Client/`) run per tenant, so a file can be present in one client schema and
|
|
53
|
+
genuinely absent in another. Always verify against the specific schema, not "the environment".
|
|
54
|
+
|
|
55
|
+
## Change history
|
|
56
|
+
- 2026-08-25 — Captured the uuid-first / structural-signature-fallback procedure and the
|
|
57
|
+
guarded-insert unsoundness, after confirming `Client/2026-08-11b` and `Client/2026-08-13` on
|
|
58
|
+
`Client_Nychh` in sandbox-client (apeterson)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,10 +18,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 66 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 56 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 9 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 7 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -22,6 +22,7 @@ updated: 2026-08-25
|
|
|
22
22
|
owners: [jcardinal, bala, tcox, apeterson, dfranks]
|
|
23
23
|
files: []
|
|
24
24
|
related:
|
|
25
|
+
- ../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
25
26
|
- features/persona-model-and-levy-gating.md
|
|
26
27
|
- features/people-file-user-lifecycle.md
|
|
27
28
|
- workflows/persona-refactor-migration.md
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [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, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql |
|
|
5
6
|
| [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 |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: nychh
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/SalesOrder.php
|
|
13
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts
|
|
14
|
+
- dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
|
|
15
|
+
related:
|
|
16
|
+
- ../../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
17
|
+
- ../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
18
|
+
- ../profile.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live
|
|
24
|
+
in the **upstream** table `PurchaseOrders_SalesOrders` (route `purchase-order-sales-orders`,
|
|
25
|
+
record 276). They have **no** downstream `SalesOrders_PurchaseOrders` rows.
|
|
26
|
+
|
|
27
|
+
This produced a symptom that looked like a bug and was not: the sales-order modal's PO Number came
|
|
28
|
+
back as an empty array while the sales-orders **list** column showed a PO number for the same
|
|
29
|
+
order. The front end was calling the **downstream** route; the list column's table-view join uses
|
|
30
|
+
the **upstream** table.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
- Verified on `Client_Nychh` (sandbox-client) for the affected order: **upstream = 1 row,
|
|
35
|
+
downstream = 0 rows**.
|
|
36
|
+
- The API returned `200` with `totalRecordCount 0` and **no** `EZ-*`/`EV-*` messages — the
|
|
37
|
+
signature of a join matching nothing, not of a permission or field-resolution failure.
|
|
38
|
+
- **ACL was clean and was ruled out:** `AclRecordPermissions` on records **275 and 276** both grant
|
|
39
|
+
`roleId` 1 and 3 full CRUD in `Client_Nychh`; the JWT carried client roles `[1,2]`. This is *not*
|
|
40
|
+
the Compass Canada-style role mis-seed.
|
|
41
|
+
- Both relevant migrations were confirmed to have run on `Client_Nychh`
|
|
42
|
+
(`Client/2026-08-11b`, `Client/2026-08-13`) — see
|
|
43
|
+
[verifying a migration ran](../../../2.0/apps/dbchanges2/workflows/verifying-a-migration-ran.md).
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
- 🚨 **Open consequence.** The shared `_purchaseOrders` calculated field reads the **downstream**
|
|
48
|
+
table (`_underscore/Model/Client/SalesOrder.php:348`). Now that the modal's PO Number binds to
|
|
49
|
+
`_purchaseOrders`, NYCHH resolves it without an `EV-8` but gets **`null` forever**. NYCHH needs an
|
|
50
|
+
**upstream counterpart** — either an upstream-aware override of `_purchaseOrders` for this tenant
|
|
51
|
+
or an upstream variant field. **No tenant precedent exists for upstream-only:** Compass USA reads
|
|
52
|
+
both, Quad and Compass Canada are downstream-only.
|
|
53
|
+
- Do not "fix" this by adding downstream rows. The direction is correct for NYCHH's business model;
|
|
54
|
+
the shared field is what is incomplete.
|
|
55
|
+
|
|
56
|
+
## Change history
|
|
57
|
+
- 2026-08-25 — Diagnosed the empty-array PO Number as a wrong-direction query (upstream data,
|
|
58
|
+
downstream route); ruled out ACL and migrations; recorded the still-open `_purchaseOrders`
|
|
59
|
+
downstream-only gap for this tenant (apeterson)
|
|
@@ -23,6 +23,8 @@ files:
|
|
|
23
23
|
related:
|
|
24
24
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
25
25
|
- ../../2.0/apps/_underscore/features/tableview-joins.md
|
|
26
|
+
- ./features/po-number-upstream-direction.md
|
|
27
|
+
- ../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
|
|
26
28
|
---
|
|
27
29
|
|
|
28
30
|
## Summary
|
|
@@ -52,6 +54,11 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
|
|
|
52
54
|
tag added after the ItemShip never bumps the fulfillment sync. See
|
|
53
55
|
[NYCHH Asset-Tag Backfill](../../2.0/apps/worker2/features/nychh-asset-tag-backfill.md).
|
|
54
56
|
|
|
57
|
+
- **PO links are UPSTREAM (`PurchaseOrders_SalesOrders`), not downstream.** The downstream
|
|
58
|
+
`sales-order-purchase-orders` route returns an empty array for NYCHH by design, and the shared
|
|
59
|
+
`_purchaseOrders` field (downstream-only) resolves to `null` for this tenant — open gap. See
|
|
60
|
+
[NYCHH PO Number direction](./features/po-number-upstream-direction.md).
|
|
61
|
+
|
|
55
62
|
## Notes
|
|
56
63
|
- Tracking data: record 318 (item-level) is currently empty for this client; their tracking
|
|
57
64
|
populates the IF/shipment level (record 317). The rebuilt views use 318 (per the Compass
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [Quad Enter PO Details — single-vendor-by-country selection and the EV-12 `vendorItem` failure](features/enter-po-details-vendor-selection-ev12.md) | 2.0 | Adding a PO number for Quad through **Enter PO Details** can `400` with **`EV-12` "unable to find a unique match"** on field **`vendorItem`**. | _underscore/Model/Quad/PurchaseOrder.php |
|
|
5
6
|
| [Quad: PO + ASN email importer (import_po_and_asn.php)](features/po-asn-email-import.md) | 1.0 | Quad Graphics' supplier emails Purchase Order and Advance Shipping Notice CSVs into a monitored Microsoft 365 mailbox. | worker/crons/toga2/quad/import_po_and_asn.php |
|
|
6
7
|
| [Quad Graphics](profile.md) | 2.0 | Quad Graphics is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Quad`. | |
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Quad Enter PO Details — single-vendor-by-country selection and the EV-12 `vendorItem` failure"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: quad
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Quad/PurchaseOrder.php
|
|
13
|
+
related:
|
|
14
|
+
- ../../../2.0/apps/api2/features/v2-api-error-codes.md
|
|
15
|
+
- ../../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
16
|
+
- ../../../2.0/standards/backend-php.md
|
|
17
|
+
- ../profile.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
Adding a PO number for Quad through **Enter PO Details** can `400` with
|
|
23
|
+
**`EV-12` "unable to find a unique match"** on field **`vendorItem`**. The failure is a **code
|
|
24
|
+
defect plus a country-scoped catalog coverage gap** — not a bad data reset and not ACL. Quad's
|
|
25
|
+
`Client_Quad` data was verified healthy (592 `VendorItems`, 18 active vendors, 15 with a complete
|
|
26
|
+
`Vendors → Addresses → States → Countries` chain).
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
1. The FE payload contains **no items**. Quad's `prePost` interceptor
|
|
31
|
+
(`_underscore/Model/Quad/PurchaseOrder.php:9` → `populatePurchaseOrderFromSalesOrder`) builds
|
|
32
|
+
them from the sales order's lines.
|
|
33
|
+
2. `getVendorByCountry` (line ~186) picks **one vendor for the whole PO**: first the vendor covering
|
|
34
|
+
the **most** of the order's items *in the ship-to country*, else **any** vendor in that country.
|
|
35
|
+
3. Line ~79 then restricts the `VendorItems` join to that **single** vendor.
|
|
36
|
+
4. When the chosen vendor has no `VendorItems` row for a line, lines ~127-139 fall back to sending a
|
|
37
|
+
**descriptor** (`vendor` + `item` + `vendorPartNumber`).
|
|
38
|
+
|
|
39
|
+
## Gotchas
|
|
40
|
+
|
|
41
|
+
- 🚨 **The descriptor fallback cannot work — its comment is wrong.** The comment claims the API can
|
|
42
|
+
"create/match" the descriptor. The field's child policy is **`MATCH`**, which only *looks up* and
|
|
43
|
+
**never creates**. Zero matches (or 2+) ⇒ `EV-12`. This is the primary defect.
|
|
44
|
+
- **Real-world instance:** order ships to Poland; picked vendor **Bechtle Direct Polska** (PL,
|
|
45
|
+
correct); item **MGDN4HN/A** is mapped **only** to General Technologies (IN, India). No Polish
|
|
46
|
+
supplier for the part ⇒ `EV-12`. Reproduced on a second order with a different vendor/item pair —
|
|
47
|
+
it is **systemic**, not one-off.
|
|
48
|
+
- 🚨 **Secondary defect — nondeterministic vendor choice.** `getVendorByCountry`'s ranking query is
|
|
49
|
+
`ORDER BY vendorItemCount DESC LIMIT 1` with **no tie-break column**. On tied counts the winner is
|
|
50
|
+
arbitrary and can **change after a database reload** (row order/ids move). This is exactly the
|
|
51
|
+
hazard in [backend-php standards](../../../2.0/standards/backend-php.md) ("a `LIMIT 1` on a
|
|
52
|
+
non-unique key MUST have a deterministic `ORDER BY`"). Fix is to append `, v.id`.
|
|
53
|
+
- **A vendor with a broken address link is invisible to vendor selection.** The queries require the
|
|
54
|
+
full `Vendors → Addresses → States → Countries` chain; a vendor missing any hop is silently
|
|
55
|
+
skipped and the PO is silently handed to a different vendor.
|
|
56
|
+
- **Recommended remediation (not yet applied):** raise a useful error naming the chosen vendor and
|
|
57
|
+
the unmapped item instead of an opaque `EV-12`, and add the deterministic tie-break.
|
|
58
|
+
|
|
59
|
+
## Change history
|
|
60
|
+
- 2026-08-25 — Diagnosed EV-12 on `vendorItem` as the `MATCH`-policy descriptor fallback that can
|
|
61
|
+
never create, driven by single-vendor-by-country selection against a country-scoped catalog gap;
|
|
62
|
+
also recorded the missing `LIMIT 1` tie-break in `getVendorByCountry` (apeterson)
|
|
@@ -20,6 +20,8 @@ owners: ["jcardinal", "bala", "apeterson", "ajean"]
|
|
|
20
20
|
files: []
|
|
21
21
|
related:
|
|
22
22
|
- ./features/po-asn-email-import.md
|
|
23
|
+
- ./features/enter-po-details-vendor-selection-ev12.md
|
|
24
|
+
- ../../2.0/apps/_underscore/features/sales-order-po-number-sourcing.md
|
|
23
25
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
24
26
|
- ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
25
27
|
- ../../2.0/apps/_underscore/features/surface-resolver.md
|
package/package.json
CHANGED