toga-ai 1.0.809 → 1.0.811
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 +60 -1
- package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md +19 -2
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +16 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +98 -2
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +72 -2
- package/knowledge/clients/compass-canada/profile.md +8 -1
- package/knowledge/clients/compass-usa/features/item-catalogs-and-duplicate-items.md +15 -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-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
@@ -1270,6 +1270,50 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
1270
1270
|
Note `SalesOrderItems.lineNumber` is a **`smallint`**, so the parked base must stay within smallint
|
|
1271
1271
|
range for the client's line counts (headroom 1000 is safe at observed volumes).
|
|
1272
1272
|
|
|
1273
|
+
- **⚠ A NetSuite-removed line that still holds an UPSTREAM customer-PO link blocked the renumber and
|
|
1274
|
+
re-froze Compass SALES_ORDERS (fixed 2026-09-15, `library` only, Compass-gated).** Compass USA's
|
|
1275
|
+
SALES_ORDERS was stuck at `1-RUNNING` (`Logs.Issue` 749, clientId 2) throwing *"unresolvable
|
|
1276
|
+
NetSuite<->DB lineNumber conflict … reconcile the orphaned/undeletable row"* whenever a
|
|
1277
|
+
NetSuite-removed line sat on a `lineNumber` a surviving line had to move onto. Root cause: the
|
|
1278
|
+
**existing** stale-line prune (`toga2.php` ~L1582) runs **AFTER** the renumber **and** skips any
|
|
1279
|
+
line carrying a PO link (it cannot delete a PO-linked line), so those removed-but-PO-linked lines
|
|
1280
|
+
survived and occupied a needed target number — and the renumber threw first anyway.
|
|
1281
|
+
- **Fix — a Compass-gated prune BEFORE the renumber** in `syncSalesOrderFromNetsuite`. For each SO
|
|
1282
|
+
line whose `c_netsuiteLineUniqueKey` is **non-empty** and **absent** from the current NetSuite
|
|
1283
|
+
line set (a line NetSuite removed) that was **never fulfilled and never invoiced**: delete its
|
|
1284
|
+
Agilant-side SO↔PO **bridge LINK** rows in **both** directions (routes
|
|
1285
|
+
`/purchase-order-item-sales-order-items/{uuid}` and `/sales-order-item-purchase-order-items/{uuid}`),
|
|
1286
|
+
then DELETE the `SalesOrderItem` (`/sales-order-items/{uuid}`), then drop it from the in-memory
|
|
1287
|
+
`$salesOrderItemsInToga`. It **NEVER** deletes/modifies the `PurchaseOrder`/`PurchaseOrderItem`
|
|
1288
|
+
itself (the Compass **customer** order is off-limits) and never touches another order record.
|
|
1289
|
+
- **Guardrails (owner decision 2026-09-15; cto AGREE; php-reviewer clean; cso SAFE TO SHIP):**
|
|
1290
|
+
(a) non-empty key **absent from NetSuite** only — an empty key was never matched to NetSuite, so
|
|
1291
|
+
treating it as "removed" is a false positive; (b) never a shipped/invoiced line (checked against
|
|
1292
|
+
the fulfillment/invoice maps built ~L990-1032); (c) **Compass-gated** (`CLIENT_UUID_COMPASS`);
|
|
1293
|
+
(d) **re-check that one line's fulfillment/invoice status immediately before the deletes** to
|
|
1294
|
+
close a TOCTOU gap (the maps are a start-of-run snapshot); (e) the final `SalesOrderItem` DELETE
|
|
1295
|
+
stays **throwing** (fail-loud) if any unhandled attachment remains — mirrors the PO-line cleanup
|
|
1296
|
+
in `syncPurchaseOrderFromNetsuite` (~L2793-2806) and the mirror-NetSuite decision (2026-09-02).
|
|
1297
|
+
- **Every FK child of `Client_Compass.SalesOrderItems` blocks a delete.** All are `NO ACTION` or
|
|
1298
|
+
`RESTRICT`, and InnoDB treats `NO ACTION` as `RESTRICT`: `ItemFulfillmentItems`, `InvoiceItems`,
|
|
1299
|
+
`PurchaseOrderItems_SalesOrderItems`, `SalesOrderItems_PurchaseOrderItems`,
|
|
1300
|
+
`SalesOrderItems_CommittedUnits`, `SalesOrderItems_Tags`, `SalesOrderItems`
|
|
1301
|
+
(`parentSalesOrderItemId` kit children), `SalesOrderItems_TransferOrderItems`,
|
|
1302
|
+
`TransferOrderItems_SalesOrderItems`. This is why the prune must clear the bridge links first and
|
|
1303
|
+
only ever touches never-fulfilled/never-invoiced lines.
|
|
1304
|
+
- **Proven (evidence):** NetSuite sales order 271929 (internal id 6926956), customer "6096 ODP
|
|
1305
|
+
Veyer (B2B)"; NetSuite reorganized it to 9 lines and removed 6 kitting lines (lineUniqueKeys
|
|
1306
|
+
20024694/695/696/702/703/704) that never shipped and were never invoiced but each still held an
|
|
1307
|
+
upstream customer-PO link — those 6 blocked the renumber.
|
|
1308
|
+
- **⚠ OPEN, NOT FIXED — a DUPLICATE `SalesOrders` record on one NetSuite internal id.** `SalesOrders`
|
|
1309
|
+
number **467435086001** shares NetSuite internal id **6926956** with order 271929 (two TOGa
|
|
1310
|
+
records for one NetSuite sales order; 467435086001 has 12 keyless lines with downstream vendor-PO
|
|
1311
|
+
links, none shipped). The main SO lookup in `syncSalesOrderFromNetsuite` (~L946-974) filters
|
|
1312
|
+
Compass by `locationId 615286` / `customerId 3`, which currently narrows to 271929, so the prune
|
|
1313
|
+
only ever touches 271929 — but **two records on one NetSuite id is an anomaly that could make the
|
|
1314
|
+
lookup bind to the wrong record on other orders.** Needs its own ticket. Deploy: `library` only;
|
|
1315
|
+
the worker redeploys to pick it up (worker clones library on deploy).
|
|
1316
|
+
|
|
1273
1317
|
- **⚠ Canon + Endeavor Health ITEM_RECEIPTS froze because `fetchPurchaseOrderById` never populated the
|
|
1274
1318
|
PO vendor (deployed/verified 2026-08-29).** The PO shim's `->entity` was always null (its SuiteQL
|
|
1275
1319
|
SELECT omitted `entity`, unlike `listPurchaseOrders`), so the fail-loud missing-entity guard in
|
|
@@ -1581,6 +1625,21 @@ library (or vice versa) crashes GroWrk and Adyen on their next sync run.
|
|
|
1581
1625
|
|
|
1582
1626
|
## Change history
|
|
1583
1627
|
|
|
1628
|
+
- 2026-09-15 — **Compass SALES_ORDERS unfrozen: a Compass-gated prune runs BEFORE the renumber.**
|
|
1629
|
+
`syncSalesOrderFromNetsuite` was throwing *"unresolvable NetSuite<->DB lineNumber conflict"*
|
|
1630
|
+
(`Logs.Issue` 749, clientId 2) because a NetSuite-removed line that still held an upstream
|
|
1631
|
+
customer-PO link survived the existing stale-line prune (which runs after the renumber and skips
|
|
1632
|
+
PO-linked lines) and occupied a `lineNumber` a surviving line needed. Added a prune BEFORE the
|
|
1633
|
+
renumber that, for each never-fulfilled/never-invoiced line whose non-empty `c_netsuiteLineUniqueKey`
|
|
1634
|
+
is absent from NetSuite, deletes its SO↔PO bridge links in both directions then the `SalesOrderItem`
|
|
1635
|
+
— never touching the `PurchaseOrder`/`PurchaseOrderItem` (Compass customer order). Guardrails:
|
|
1636
|
+
absent-from-NetSuite + non-empty key only, never shipped/invoiced, Compass-gated, immediate
|
|
1637
|
+
TOCTOU re-check, final DELETE stays fail-loud. Recorded that every FK child of
|
|
1638
|
+
`Client_Compass.SalesOrderItems` is `NO ACTION`/`RESTRICT` (all block a delete), and an OPEN
|
|
1639
|
+
duplicate-`SalesOrders`-record anomaly (467435086001 shares NetSuite internal id 6926956 with
|
|
1640
|
+
271929). Proven on NetSuite SO 271929 (6 removed kitting lines blocked the renumber). `library`
|
|
1641
|
+
only, no SQL; worker redeploys to pick it up. cto AGREE; php-reviewer clean; cso SAFE TO SHIP.
|
|
1642
|
+
(jcardinal)
|
|
1584
1643
|
- 2026-09-13 — **ITEM_FULFILLMENTS unfrozen for NYCHH: five stacked fulfillment-reconcile bugs fixed
|
|
1585
1644
|
(library, deployed + verified in prod).** (1) **Unit duplication from a truncated nested GET** — the
|
|
1586
1645
|
existing-units dedup map was built from the nested `itemFulfillmentItemUnits` of one
|
package/knowledge/2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
CHANGED
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [apeterson, bala]
|
|
9
|
+
updated: 2026-09-15
|
|
10
|
+
owners: [apeterson, bala, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/PurchaseOrders/SalesOrder.php
|
|
13
13
|
- _underscore/Model/Client/SalesOrders/PurchaseOrder.php
|
|
@@ -40,6 +40,18 @@ one returns `200` with an empty array, which reads like a permission or data bug
|
|
|
40
40
|
Read the name as **"`<source>`\_`<thing created from it>`"**. The *first* table name is the
|
|
41
41
|
origin; the second is what was generated from it.
|
|
42
42
|
|
|
43
|
+
**The line-item bridges follow the same rule (same name order, same directions):**
|
|
44
|
+
|
|
45
|
+
| Table | Direction | Meaning | API route |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `PurchaseOrderItems_SalesOrderItems` | **UPSTREAM** | an SO line created **FROM** a customer PO line | `purchase-order-item-sales-order-items` |
|
|
48
|
+
| `SalesOrderItems_PurchaseOrderItems` | **DOWNSTREAM** | a vendor PO line created **FROM** an SO line | `sales-order-item-purchase-order-items` |
|
|
49
|
+
|
|
50
|
+
The 1.0 Compass NetSuite sync deletes these **item-level** link rows in **both** directions when it
|
|
51
|
+
prunes a NetSuite-removed SO line — see
|
|
52
|
+
[NetSuite → TOGa Supply per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)
|
|
53
|
+
(2026-09-15).
|
|
54
|
+
|
|
43
55
|
## How it works
|
|
44
56
|
|
|
45
57
|
- Both `Records` rows are `aclDatabase = CLIENT`, `childPolicy = MATCH_UPSERT`, so both are
|
|
@@ -122,6 +134,11 @@ everywhere. Present is not populated.
|
|
|
122
134
|
collapses many POs without multiplying rows.
|
|
123
135
|
|
|
124
136
|
## Change history
|
|
137
|
+
- 2026-09-15 — Added the **line-item** bridge parallel: `PurchaseOrderItems_SalesOrderItems`
|
|
138
|
+
(upstream, route `purchase-order-item-sales-order-items`) and `SalesOrderItems_PurchaseOrderItems`
|
|
139
|
+
(downstream, route `sales-order-item-purchase-order-items`) follow the same name-order rule as the
|
|
140
|
+
header bridges. Confirmed while building the Compass 1.0 sync's pre-renumber prune, which deletes
|
|
141
|
+
both item-level link directions for a NetSuite-removed SO line. (jcardinal)
|
|
125
142
|
- 2026-08-26 — Added the **write** side: the two directions are populated by two different
|
|
126
143
|
pipelines (2.0 `_Trait_Netsuite_SalesOrder` writes **upstream** customer POs from `otherRefNum`;
|
|
127
144
|
the 1.0 `syncPurchaseOrderFromNetsuite` cron writes **downstream** vendor POs), and the api2 child
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -234,6 +234,16 @@ production `Core.RecordFields` for `Items`:
|
|
|
234
234
|
|---|---|---|---|
|
|
235
235
|
| **107** | `Items.manufacturerId` | `MATCH_UPSERT` | **creates** the manufacturer — get-or-create by name works |
|
|
236
236
|
| **269** | `Items.assetTypeId` | `MATCH` | **fails the entire item write** |
|
|
237
|
+
| **914** | `Items.catalogId` | `MATCH` | **matches** the existing catalog — and can never create one |
|
|
238
|
+
|
|
239
|
+
Row 914 is the benign shape of `MATCH`, and it is worth knowing why it is benign: the match set is
|
|
240
|
+
built from **`isIdentifier`**, and the `catalogs` record carries `isIdentifier = 1` on **`id`,
|
|
241
|
+
`uuid` AND `name`** (verified on production `Core.RecordFields`, 2026-09-15). So
|
|
242
|
+
`catalog: {name: 'Compass'}` resolves to the existing `Catalogs` row by name and **cannot mint a
|
|
243
|
+
new catalog** — which is what makes a name-keyed catalog safe to seed as a per-tenant default
|
|
244
|
+
(see [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)). The identifier set
|
|
245
|
+
is per-record data, not a code constant: check `isIdentifier` before assuming a business key is
|
|
246
|
+
matchable.
|
|
237
247
|
|
|
238
248
|
The 1.0 NetSuite importer's `getCreateItem()` contains **both** calls. `getCreateManufacturer()`
|
|
239
249
|
sends `manufacturer: {name}` and has always worked; copying that shape for `assetType: {name}`
|
|
@@ -442,6 +452,11 @@ only one tenant's copy has the wrong flag.
|
|
|
442
452
|
|
|
443
453
|
## Change history
|
|
444
454
|
|
|
455
|
+
- 2026-09-15 — Added **`Items.catalogId` (recordFieldId 914, `MATCH`)** to the childPolicy table as
|
|
456
|
+
the *benign* `MATCH` case, and recorded why: the `catalogs` record has `isIdentifier = 1` on
|
|
457
|
+
**`id`, `uuid` and `name`** (prod `Core.RecordFields`), so `catalog: {name: '<catalog>'}` always
|
|
458
|
+
links the existing row and can never create a catalog. That is what lets a tenant ship a
|
|
459
|
+
name-keyed catalog as an item-create default. (bala)
|
|
445
460
|
- 2026-09-08 — ⚠ Recorded the **third EV-12 flavor: the field is declared in PHP but
|
|
446
461
|
`isIdentifier = 0` in the tenant's `CustomRecordFields`.** `V2.php` (~L7471-7530) builds
|
|
447
462
|
`$recordIdentifiers` purely from DB metadata (`Core.RecordFields` + `Client_<X>.CustomRecordFields`
|
|
@@ -6,9 +6,12 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [jcardinal, apeterson, tcox]
|
|
9
|
+
updated: 2026-09-15
|
|
10
|
+
owners: [jcardinal, apeterson, tcox, bala]
|
|
11
11
|
files:
|
|
12
|
+
- dbchanges2/Core/2026-09-15a - ItemCreateDefaultsSurfaceSeed.sql
|
|
13
|
+
- dbchanges2/Client_Compass/2026-09-15a - ItemCreateCatalogDefault.sql
|
|
14
|
+
- dbchanges2/Client_CompassCanada/2026-09-15a - ItemCreateCatalogDefault.sql
|
|
12
15
|
- dbchanges2/Client_Compass/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql
|
|
13
16
|
- dbchanges2/Client_CompassCanada/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql
|
|
14
17
|
- dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql
|
|
@@ -1372,7 +1375,100 @@ environment and take the **highest** `MAX(id)+1`, never the one in front of you,
|
|
|
1372
1375
|
`Core.SurfaceElements.id` = **233**, next `Core.Messages.id` = **299**. Both consuming files were
|
|
1373
1376
|
still unrun at the time, so this is a reservation, not a deployed state.
|
|
1374
1377
|
|
|
1378
|
+
## A marker element can carry per-tenant WRITE defaults, not just visibility (`item-record-create-defaults`, Core 61, 2026-09-15)
|
|
1379
|
+
|
|
1380
|
+
The marker-element pattern (a hidden element whose `config` is the payload) had so far only carried
|
|
1381
|
+
**visibility / mode** flags. `Core/2026-09-15a - ItemCreateDefaultsSurfaceSeed.sql` uses it to carry
|
|
1382
|
+
**default field VALUES for a create form**: surface **`item-record-create-defaults`** (type
|
|
1383
|
+
`SECTION`, `config.cardType = 'createDefaults'`) with one element, `isVisible = 0`, whose config is
|
|
1384
|
+
|
|
1385
|
+
```json
|
|
1386
|
+
{"role":"createDefaults","values":{}}
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
Core seeds `values` **empty** — the true-neutral default — and each tenant opts in with a single
|
|
1390
|
+
`SurfaceOverrides` **`CONFIG`** row. The consuming app spreads `values` into its `POST` body before
|
|
1391
|
+
the form fields, so a default never beats real user input. FE side:
|
|
1392
|
+
[surface-frontend](../../toga25-supply/features/surface-frontend.md). This shipped to fix items
|
|
1393
|
+
created in toga25-supply being written with `Items.catalogId = NULL` (28 rows in prod
|
|
1394
|
+
`Client_Compass`, 6 in `Client_CompassCanada`).
|
|
1395
|
+
|
|
1396
|
+
**Why `name` and not `uuid` in the seeded value.** `Items.catalogId` is `Core.RecordFields` **914**
|
|
1397
|
+
with `childPolicy = MATCH`, and the `catalogs` record has `isIdentifier = 1` on `id`, `uuid` **and**
|
|
1398
|
+
`name` — so a name matches the existing row and **can never create a catalog**
|
|
1399
|
+
([nested-relationship writes](../../api2/features/nested-relationship-writes.md)). A name also keeps
|
|
1400
|
+
the seed readable and tenant-portable, where a uuid would not be.
|
|
1401
|
+
|
|
1402
|
+
**The two tenants spell the catalog differently, and the seeds use the STORED spelling:**
|
|
1403
|
+
|
|
1404
|
+
| Tenant | Seeded value | `Catalogs` reality |
|
|
1405
|
+
|---|---|---|
|
|
1406
|
+
| `Client_Compass` | `{"catalog":{"name":"Compass"}}` | id 1 **`Compass`** (also id 2 `Agilant`, id 3 `Office Depot`) |
|
|
1407
|
+
| `Client_CompassCanada` | `{"catalog":{"name":"compass"}}` | id 1 **`compass`** — the only row |
|
|
1408
|
+
|
|
1409
|
+
Both `name` columns are case-insensitive (`utf8mb4_unicode_ci` on Compass,
|
|
1410
|
+
`utf8mb4_0900_ai_ci` on Canada), so either spelling would in fact match; matching the stored
|
|
1411
|
+
spelling is a readability rule, not a correctness one. Catalog background:
|
|
1412
|
+
[Compass item catalogs](../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
1413
|
+
|
|
1414
|
+
### Both client files restate `role` — the CONFIG-replaces-wholesale rule, hit again
|
|
1415
|
+
|
|
1416
|
+
A `CONFIG` override **replaces the element's config object entirely** (`Surface.php:839`, see
|
|
1417
|
+
[the load-bearing rule](#-a-config-override-replaces-config-wholesale--this-is-the-load-bearing-rule-of-the-whole-design)),
|
|
1418
|
+
so each client row must ship **`role` AND `values`**:
|
|
1419
|
+
|
|
1420
|
+
```json
|
|
1421
|
+
{"role":"createDefaults","values":{"catalog":{"name":"Compass"}}}
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
Send only `values` and the element loses its `role`, the adapter stops finding the marker, and the
|
|
1425
|
+
default silently disappears with no error anywhere. This is now the **second** feature to hit it
|
|
1426
|
+
(after the Talos suggestion chips) — treat "restate every key the FE matches on" as the default
|
|
1427
|
+
authoring step for any CONFIG override, not a special case.
|
|
1428
|
+
|
|
1429
|
+
### The client files hardcode the Core ids — and the 2026-07-20d precedent is NOT copyable
|
|
1430
|
+
|
|
1431
|
+
`Client_Compass/2026-09-15a` and `Client_CompassCanada/2026-09-15a` inline the Core `surfaceId` /
|
|
1432
|
+
`surfaceElementId` read from Core **after** the Core file was run, and guard re-runs with
|
|
1433
|
+
`SELECT … FROM DUAL WHERE NOT EXISTS (…)` against the **client table only** — no `Core.*` read at
|
|
1434
|
+
all.
|
|
1435
|
+
|
|
1436
|
+
**Confirmed again by a real failure this session:** querying `Client_Compass` with a join to `Core`
|
|
1437
|
+
returns `OperationalError (1049, "Unknown database 'Core'")` on the `prod-client` cluster. The
|
|
1438
|
+
nearest existing example, `Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql`, resolves
|
|
1439
|
+
its element id with `FROM Core.SurfaceElements JOIN Core.Surfaces` — **do not copy it.** It is one of
|
|
1440
|
+
the ~51 stale cross-cluster files described above; it landed in prod by hand transposition, not by
|
|
1441
|
+
running as written.
|
|
1442
|
+
|
|
1443
|
+
### Ids consumed — and beta must be RE-READ, not reused
|
|
1444
|
+
|
|
1445
|
+
`Surfaces`/`SurfaceElements` ids differ sharply per environment: before this work prod Core was at
|
|
1446
|
+
`Surfaces` **60** / `SurfaceElements` **233**, while dev-sandbox (beta) was at **23** / **65**. The
|
|
1447
|
+
developer ran the Core file **on PROD**, taking **surface 61** and **element 234**, and the two
|
|
1448
|
+
client files hardcode those prod ids.
|
|
1449
|
+
|
|
1450
|
+
> ⚠ **Beta has not had the Core file run.** Running the client files on beta as written would point
|
|
1451
|
+
> the overrides at ids that mean something else (or nothing) there. Re-read the beta ids after
|
|
1452
|
+
> running the Core file and swap them in before running the client files.
|
|
1453
|
+
|
|
1454
|
+
**Handoff published (verify before your own run):** next prod `Core.Surfaces.id` = **62**, next prod
|
|
1455
|
+
`Core.SurfaceElements.id` = **235**.
|
|
1456
|
+
|
|
1375
1457
|
## Change history
|
|
1458
|
+
- 2026-09-15 — Added the **`item-record-create-defaults`** seed trio (`Core/2026-09-15a` +
|
|
1459
|
+
`Client_Compass` / `Client_CompassCanada` `2026-09-15a - ItemCreateCatalogDefault.sql`), which
|
|
1460
|
+
extends the marker-element pattern from visibility flags to **per-tenant create-form default
|
|
1461
|
+
VALUES** (`config {"role":"createDefaults","values":{}}`, Core seeds `values` empty, each tenant
|
|
1462
|
+
overrides). Shipped to stop toga25-supply writing `Items.catalogId = NULL`. Recorded: the value is
|
|
1463
|
+
seeded by catalog **name** because `RecordFields` 914 is `MATCH` over a record with
|
|
1464
|
+
`isIdentifier` on `name` (so it can never mint a catalog); the two tenants store the name with
|
|
1465
|
+
different case (`Compass` vs `compass`, both CI collations); both client files must restate
|
|
1466
|
+
**`role`** because a CONFIG override replaces the config wholesale (second feature to hit that);
|
|
1467
|
+
and the client files hardcode Core ids with a client-only `NOT EXISTS` guard — re-confirmed by a
|
|
1468
|
+
live `1049 Unknown database 'Core'` on `prod-client`, which also makes the older
|
|
1469
|
+
`2026-07-20d - ItemRecordVendorItemsEnable.sql` a NON-copyable precedent. Prod consumed
|
|
1470
|
+
**Surface 61 / SurfaceElement 234** (beta is at 23 / 65 and has NOT run the Core file — re-read
|
|
1471
|
+
its ids first); next prod ids = **62** / **235**. (bala)
|
|
1376
1472
|
- 2026-09-04 — Added the **restrict-to-persona opt-in pair** (`Client_Compass/2026-09-04a` +
|
|
1377
1473
|
`Client_CompassCanada/2026-09-04a - ItemRecordRestrictToPersonaVisible.sql`): client-wide
|
|
1378
1474
|
`IS_VISIBLE`+`IS_ENABLED` overrides on `surfaceId` **19** / `surfaceElementId` **57**, literal Core
|
|
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [jcardinal, apeterson, tcox, rgirish]
|
|
9
|
+
updated: 2026-09-15
|
|
10
|
+
owners: [jcardinal, apeterson, tcox, rgirish, bala]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts
|
|
13
13
|
- toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts
|
|
@@ -71,6 +71,7 @@ files:
|
|
|
71
71
|
- toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts
|
|
72
72
|
- toga25-supply/src/fieldsConfig/index.ts
|
|
73
73
|
- toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts
|
|
74
|
+
- toga25-supply/src/layout/ItemRecordModalLayout/hooks/useItemCreateForm.tsx
|
|
74
75
|
- toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts
|
|
75
76
|
- toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx
|
|
76
77
|
- toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx
|
|
@@ -402,6 +403,64 @@ a `computeValueKey`, grep the consuming view's registry for that literal string
|
|
|
402
403
|
> makes the field visible and therefore **crashes the modal**; the FE alone changes nothing visible.
|
|
403
404
|
> Same shape as the approvals-gate ship-together dependency.
|
|
404
405
|
|
|
406
|
+
## Item CREATE defaults also come from Surface — and a missing default wrote NULL `catalogId` (2026-09-15)
|
|
407
|
+
|
|
408
|
+
The item modal's **create** payload now takes its per-tenant defaults from the Surface layer instead
|
|
409
|
+
of a client branch in code. This started as a data bug: `useItemCreateForm` built the `POST /items`
|
|
410
|
+
body with **no catalog at all**, so every item created in this app was saved with
|
|
411
|
+
`Items.catalogId = NULL`. The old `toga2-supply` `CreateItemModal.tsx` had always sent
|
|
412
|
+
`catalog: { name: "Compass" }` — which is why only the 2.5 app produced NULL rows, and why nobody
|
|
413
|
+
saw it in the older app. **Measured on prod 2026-09-15:** `Client_Compass.Items` held **28** rows
|
|
414
|
+
with a NULL `catalogId` (newest 2026-09-14) against 1,509 on catalog 1, and
|
|
415
|
+
`Client_CompassCanada.Items` **6** NULL against 221. A NULL-catalog item **never reaches the
|
|
416
|
+
storefront** — see
|
|
417
|
+
[Compass item catalogs](../../../clients/compass-usa/features/item-catalogs-and-duplicate-items.md).
|
|
418
|
+
|
|
419
|
+
**How it works now** — the same marker-element shape as `item-record-vendor-items`, read by a new
|
|
420
|
+
adapter:
|
|
421
|
+
|
|
422
|
+
- New Core surface **`item-record-create-defaults`** (type `SECTION`, `surface.config.cardType =
|
|
423
|
+
'createDefaults'`) carrying **one non-rendered marker element** with
|
|
424
|
+
`config = {"role":"createDefaults","values":{}}` and `isVisible = 0`.
|
|
425
|
+
- The slug was added to **`ITEM_RECORD_SURFACE_SLUGS`**, so it rides the group fetch the record
|
|
426
|
+
modal already makes — no new request.
|
|
427
|
+
- **`surfaceBundleToItemCreateDefaults()`** (`helpers/surfaceBundleToItemFields.ts`, exported via
|
|
428
|
+
`helpers/index.ts`) finds the marker by `config.role === 'createDefaults'` and returns its
|
|
429
|
+
`config.values` object.
|
|
430
|
+
- `useItemCreateForm` **spreads the defaults FIRST** into the `POST /items` body, so a real form
|
|
431
|
+
field always wins over a default. Order is the whole contract here.
|
|
432
|
+
- A tenant opts in with one `SurfaceOverrides` CONFIG row. Compass seeds
|
|
433
|
+
`{"catalog":{"name":"Compass"}}`; Compass Canada seeds `{"catalog":{"name":"compass"}}` — the
|
|
434
|
+
spelling differs per tenant, see
|
|
435
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
436
|
+
|
|
437
|
+
**Why this is the right seam:** a tenant default is presentation/tenant config, so it belongs in the
|
|
438
|
+
surface layer, not in a hostname or client-slug switch in the create form. Adding the next default
|
|
439
|
+
(vendor, inventory type, anything) is now a `values` key plus a seed row — zero FE change.
|
|
440
|
+
|
|
441
|
+
### Adding a slug to a group fetch is SAFE on unseeded clients and environments
|
|
442
|
+
|
|
443
|
+
`_buildBundle` returns `{ surface: null, elements: [] }` when `$surface->load()` fails, so a slug
|
|
444
|
+
that has not been seeded in a tenant (or a whole environment where the Core file has not run) simply
|
|
445
|
+
resolves to an empty bundle. The adapter then returns `{}` and the create payload is byte-identical
|
|
446
|
+
to before. Verified against `Model/Core/Surface.php` — see
|
|
447
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md). **So the FE can ship before the
|
|
448
|
+
SQL runs anywhere** — the reverse of the restrict-to-persona deploy-order rule above, because this
|
|
449
|
+
element renders nothing.
|
|
450
|
+
|
|
451
|
+
### Sending `catalog` by NAME does not trip the Compass POST interceptor
|
|
452
|
+
|
|
453
|
+
`_Model_Compass_Item::postPost` throws *"Could not load catalog uuid"* when it receives a catalog
|
|
454
|
+
with no uuid, which looks like a reason to send a uuid instead of a name. It is not. api2 invokes the
|
|
455
|
+
POST payload interceptor with **`$outData` — the created record, not the submitted body**
|
|
456
|
+
(`V2.php:6019`), and the created record always carries the resolved catalog uuid. Compass has an
|
|
457
|
+
active POST/POST interceptor on `recordId 21` (Items); Compass Canada has one too, but there is no
|
|
458
|
+
`_Model_CompassCanada_Item`, so it falls back to `_Model_Client_Item::postPost`, which returns early
|
|
459
|
+
unless `isFulfillable` was sent. Background:
|
|
460
|
+
[API payload interceptors](../../api2/features/api-payload-interceptors.md). The name→row match
|
|
461
|
+
itself is safe because `Items.catalogId` is a `MATCH` field over a record whose `name` is an
|
|
462
|
+
identifier — [nested-relationship writes](../../api2/features/nested-relationship-writes.md).
|
|
463
|
+
|
|
405
464
|
## SalesOrders approval-decision modal (approve + deny) — migrated via an OVERLAY adapter seam
|
|
406
465
|
|
|
407
466
|
The SalesOrder approve/deny decision modal's config now sources its display fields from Surface,
|
|
@@ -1100,6 +1159,17 @@ claim is about the `navigation-*` **flags** being inert — still true — not a
|
|
|
1100
1159
|
unused).
|
|
1101
1160
|
|
|
1102
1161
|
## Change history
|
|
1162
|
+
- 2026-09-15 — **Item create defaults moved to the Surface layer, fixing NULL `catalogId` on every
|
|
1163
|
+
item created in this app.** `useItemCreateForm` sent no catalog at all (prod: 28 NULL-catalog rows
|
|
1164
|
+
in `Client_Compass`, 6 in `Client_CompassCanada`; the older `toga2-supply` modal had always sent
|
|
1165
|
+
`catalog: {name:"Compass"}`), and a NULL-catalog item never reaches the storefront. New Core
|
|
1166
|
+
surface `item-record-create-defaults` carries a hidden `config.role='createDefaults'` marker whose
|
|
1167
|
+
`config.values` each tenant overrides; the slug rides the existing `ITEM_RECORD_SURFACE_SLUGS`
|
|
1168
|
+
group fetch, `surfaceBundleToItemCreateDefaults()` adapts it, and the create form spreads it
|
|
1169
|
+
**first** so form fields still win. Recorded two rules that make this cheap: an unseeded slug
|
|
1170
|
+
resolves to an empty bundle (so FE can ship before the SQL, unlike the restrict-to-persona case),
|
|
1171
|
+
and sending `catalog` by **name** cannot trip `_Model_Compass_Item::postPost` because api2 hands a
|
|
1172
|
+
POST interceptor the created record, not the request body. (bala)
|
|
1103
1173
|
- 2026-09-04 — Fixed the item modal's **More Info → "Restrict to Persona"** field missing in VIEW for
|
|
1104
1174
|
Compass USA + Canada. Two causes, both now rules here: (1) the item modal's VIEW is Surface-driven
|
|
1105
1175
|
while EDIT is still JSON, so a Core-default-OFF element with no client `SurfaceOverride` vanishes
|
|
@@ -17,7 +17,7 @@ project: _Underscore
|
|
|
17
17
|
client: compass-canada
|
|
18
18
|
type: profile
|
|
19
19
|
status: active
|
|
20
|
-
updated: 2026-09-
|
|
20
|
+
updated: 2026-09-15
|
|
21
21
|
owners: [jcardinal, bala, tcox, apeterson, ajean]
|
|
22
22
|
files: []
|
|
23
23
|
related:
|
|
@@ -61,6 +61,13 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
61
61
|
resolution + client constants that all 8 Canada crons depend on. `library` is in `apps` for that
|
|
62
62
|
reason.
|
|
63
63
|
|
|
64
|
+
- **Catalogs: one row, lowercase.** `Client_CompassCanada.Catalogs` holds a **single** row, id 1
|
|
65
|
+
name **`compass`** (lowercase) — not the `Compass` / `Agilant` / `Office Depot` trio Compass USA
|
|
66
|
+
has ([item catalogs](../compass-usa/features/item-catalogs-and-duplicate-items.md)). The column
|
|
67
|
+
collation is case-insensitive, but write the stored spelling when seeding a value that references
|
|
68
|
+
it by name. Items created in `toga25-supply` were saved with a NULL `catalogId` until 2026-09-15
|
|
69
|
+
(6 rows here); the fix seeds the catalog as a Surface create-default per tenant.
|
|
70
|
+
|
|
64
71
|
## Vendors & integrations
|
|
65
72
|
- **Grand & Toy (G&T)** — primary hardware vendor. SOs flow toga → MITS → PO to G&T; G&T sends
|
|
66
73
|
back ASNs. ASN ingestion (email CSV + the auto-created ItemFulfillment chain + bilingual
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["bala", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -214,6 +214,15 @@ children" rule:
|
|
|
214
214
|
- **⚠ A NULL `catalogId` is a real state** (27 items in prod, 22 ODP `VendorItems` point at such
|
|
215
215
|
items). Anything joining through `Catalogs` **drops those rows silently** — an inner join to
|
|
216
216
|
`Catalogs` is itself a filter.
|
|
217
|
+
**Where the newest NULL rows came from is now known, and it is fixed (2026-09-15):** the
|
|
218
|
+
`toga25-supply` item **create** form posted no catalog at all, so every item created in the 2.5 app
|
|
219
|
+
was born with `catalogId = NULL` — **28** such rows in `Client_Compass` (newest 2026-09-14) and
|
|
220
|
+
**6** in `Client_CompassCanada`, against 1,509 / 221 on catalog 1. A NULL-catalog item never
|
|
221
|
+
reaches the storefront. The create form now takes `catalog: {name: 'Compass'}` from a seeded
|
|
222
|
+
Surface default instead of hardcoding it (the older `toga2-supply` modal always sent it, which is
|
|
223
|
+
why the NULL rows are all from the new app) — see
|
|
224
|
+
[surface-frontend](../../../2.0/apps/toga25-supply/features/surface-frontend.md). The existing NULL
|
|
225
|
+
rows still need a data repair; the leak is closed, the backlog is not.
|
|
217
226
|
- **⚠ `BundleItems.itemId` is a silent catalog-resolution consumer — the storefront never
|
|
218
227
|
re-catalogs it.** The `toga2-commerce` storefront relays whatever item `GET /v2/bundles/{uuid}`
|
|
219
228
|
returns; its `catalogId = 1` filter guards standalone item search only, never bundles. So a kit
|
|
@@ -241,6 +250,11 @@ children" rule:
|
|
|
241
250
|
[catalog-fold reversal](../workflows/catalog-fold-reversal.md).
|
|
242
251
|
|
|
243
252
|
## Change history
|
|
253
|
+
- 2026-09-15 — Identified the **source of the newest NULL-`catalogId` items**: the `toga25-supply`
|
|
254
|
+
create form sent no catalog, so every item created in the 2.5 app was born catalog-less (**28** rows
|
|
255
|
+
in `Client_Compass`, newest 2026-09-14; **6** in `Client_CompassCanada`), and such an item never
|
|
256
|
+
reaches the storefront. The leak is closed — the create payload now takes its catalog from a seeded
|
|
257
|
+
Surface default — but the existing NULL rows are still unrepaired. (bala)
|
|
244
258
|
- 2026-09-02 — **Narrowed the same-day uniqueness resolution below.** `Client_Compass.Items` holds
|
|
245
259
|
`MD7F4LL/A-S` **twice** — id 2760 (`title` set, `isActive = 0`, the row all referencing bundles
|
|
246
260
|
use) and id 2784 (`title` NULL, `isActive = 1`, unused) — a pair from outside the fold population, so
|
package/package.json
CHANGED