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.
@@ -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)
@@ -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)
@@ -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)_ — 62 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
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)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.649",
3
+ "version": "1.0.650",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",