toga-ai 1.0.1185 → 1.0.1186

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.
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-23
9
+ updated: 2026-10-28
10
10
  owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish", "jcardinal"]
11
11
  files:
12
12
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
@@ -206,6 +206,7 @@ behaviour — it bites any write to a nested child, not one feature. Worked exam
206
206
  - **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
207
207
  - **An identifier that is unique only inside a parent (e.g. `bundles.number` per catalog) links the WRONG record, silently.** The match uses only the identifier fields sent, so `bundle: {number: "001"}` hits another catalog's `001`. Send `uuid` (or add the scoping identifier) resolved in the caller. Seen on Prudential India REQs — see [bundle catalog resolution](../../../clients/prudential/features/service-request-bundle-catalog-resolution.md).
208
208
  - **⚠ A nested `unit` with only an `item` (no serial/asset tag) REUSES one existing unit.** `ServiceRequests_Units.unitId` is `MATCH_UPSERT` and `Units.itemId` is an identifier, so `{unit:{item:{uuid}}}` matches the first unit of that item — every SR links the same unit. For serial-less requests attach a **bundle** instead (`ServiceRequests_Bundles.bundleId` is `MATCH`; send `{bundle:{uuid}}`). See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md).
209
+ - **Nested `unit {serialNumber, item:{partNumber}}` matches on serial AND item together** (both identifiers sent). Same serial + different partNumber → a **second** Unit; an unknown partNumber → a new bare Item; serial with no item → 1048 `itemId` cannot be null. Verified beta `Client_Adyen` 2026-10-28 — see [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md).
209
210
  - **Parent FKs that can CREATE a Contact** (prod `Core.RecordFields.childPolicy`, 2026-10-16): `MATCH_CREATE` — `Users.contactId`, `PurchaseOrders.contactId`; `MATCH_UPSERT` — `Tickets.contactId`, `Tickets.reportedByContactId`, `Units.contactId`, `UnitNotes.contactId`, `Entitlements.contactId`, `ServiceRequests.requestedForContactId`, `ContactPhoneNumbers.contactId`. A nested contact created through any of these must carry a `customer` — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md).
210
211
  - **⚠ Every identifier field SENT joins the match.** `contacts.customerId` (1651) is `isIdentifier = 1`, so `{c_wjeUserId, customer:{uuid}}` matches on c_wjeUserId AND customerId. A contact with a NULL/different customer does not match → `MATCH_CREATE`/`MATCH_UPSERT` **creates a duplicate**. Backfill the field before code starts sending it.
211
212
  - **Nested-write ACL** (`V2.php` `getForeignKeyValue` ~L9790-10469): no record-level ACL on the nested record. Checked = top-level record create (e.g. users, Core.Records 2) + FIELD write for every field sent on the nested record (e.g. contacts, 37), using the caller's `id.client.roles`. A missing field grant fails **EV-12** (400, "lack write permissions"), never a silent drop.
@@ -214,7 +215,7 @@ behaviour — it bites any write to a nested child, not one feature. Worked exam
214
215
  - **⚠ A `prePost` that adds a key to a `{uuid}`-only nested reference breaks the link.** `getForeignKeyValue` (~L7453) forces `CHILD_POLICY__MATCH` only when the object has exactly one key = a non-empty `uuid`; a 2nd key drops to the field's own child policy. Skip uuid-only objects in hooks — see [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md).
215
216
 
216
217
  ## Change history
217
- - 2026-10-23 — Gotcha: a prePost adding keys to a `{uuid}`-only nested object loses the forced MATCH (TRUE-82978). (rgirish)
218
+ - 2026-10-28 — Gotcha: nested unit matches on serial AND item; new partNumber mints a bare Item (TRUE-83365). (rgirish)- 2026-10-23 — Gotcha: a prePost adding keys to a `{uuid}`-only nested object loses the forced MATCH (TRUE-82978). (rgirish)
218
219
  - 2026-10-22 — POST response ignores `fields` (full record at default depth; `depth=-1` = uuid only) (TRUE-82919). (jcardinal)
219
220
  - 2026-10-16 — Safe contact+email/phone create pattern (child arrays, depth=2, then PUT primary `{uuid}`); why primary FKs match by value (MATCH_UPSERT on identifier only); POST options incl. `fields=` reach nested children → EV-8 + rollback (TRUE-82887). (jcardinal)
220
221
  - 2026-10-16 — Corrected: a sent identifier field (contacts.customerId 1651) joins the match, so a mismatched contact is duplicated, not overwritten; nested-write ACL = top-level create + per-field write, EV-12 on a gap (TRUE-82841). (jcardinal)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-21
9
+ updated: 2026-10-28
10
10
  owners: ["bala", "mhammontree", "apeterson", "rgirish"]
11
11
  files:
12
12
  - dbchanges2/Client_Growrk/2026-05-28.sql
@@ -128,7 +128,7 @@ ALTER TABLE `Items` ADD COLUMN `isFulfillable` TINYINT UNSIGNED NULL DEFAULT NUL
128
128
 
129
129
  Column order does not affect correctness, but keeping the repaired tenant byte-identical to its peers is what makes the *next* `information_schema` diff return zero rows instead of noise.
130
130
 
131
- **Run it on beta first, verify, then production.** (Proposed 2026-09-08; not yet executed — the developer could not test in production at the time.)
131
+ **Run it on beta first, verify, then production.** (Adyen outcome 2026-10-28: prod already had it; dev-sandbox fixed. The full-wave audit found 8 more tables + 4 columns missing — see [Adyen profile](../../../../clients/adyen/profile.md).)
132
132
 
133
133
  #### Drift is not only columns — a metadata FLAG can be wrong while everything else is right
134
134
 
@@ -167,6 +167,11 @@ This is the write-side twin of the read-side rule above (*"the field named in th
167
167
  - **A LOCAL checkout that is many `dbchanges` behind looks exactly like a schema gap — it is not.** Every error in a 2026-09-21 local Compass session was an **existing file never applied** (`Client/2026-09-11a` → `SalesOrderItems.fulfillmentType`, the real cause of an order-submit 400 *"Unknown column 'fulfillmentType'"*; the `Logs_Client` blank-DB `Email`/`EmailAttachment` tables, cause of a 500 *"Table 'logs_compass.email' doesn't exist"*; the `Logs` blank-DB `Issue`/`Event` tables; `Client/2026-08-28b`, `2026-08-28f`, `2026-09-01b`, `2026-09-01c`, `2026-09-14a`, `_modules/netsuite/2026-09-11a`). **Grep `dbchanges2` for the column or table name before writing any SQL.**
168
168
  - **Diff a whole environment against prod in ONE pass** — missing tables *and* missing columns together — by comparing `SELECT CONCAT(TABLE_NAME, ':', COUNT(*)) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '<db>' GROUP BY TABLE_NAME` on both sides and `comm`-ing the sorted lists. Step 3's per-table diff is for one known traversal path; this is the catch-up sweep.
169
169
 
170
+ - **Only add what the `_underscore` model declares — other tenants carry dead leftovers.** A "missing vs other tenants" list includes legacy objects code no longer uses. Skip (verified 2026-10-28, TRUE-83365):
171
+ - `AdvanceShippingNoticeUnits` + `AdvanceShippingNoticeUnits_TrackingNumbers` — code uses `AdvanceShippingNoticeItemUnits`. Recreating them fails anyway: FK names `AdvanceShippingNoticeUnits_advanceShippingNoticeItemId` / `_unitId` already exist on `AdvanceShippingNoticeItemUnits`.
172
+ - `ItemFulfillmentPackages` — replaced by `ItemFulfillments_TrackingNumbers` (comment in `_underscore/Model/Client/ItemFulfillment.php` ~L745).
173
+ - `trackingNumberId` on `ItemFulfillmentItemUnits` / `ItemReceiptItems` / `ItemReceiptItemUnits`, and `TicketNotes.ticketNextActionId` — no model declares them.
174
+ - **`toga-db` MCP rejects `SHOW CREATE TABLE`** ("Forbidden keyword CREATE"). Rebuild the DDL from `information_schema` `COLUMNS`, `STATISTICS`, `KEY_COLUMN_USAGE`, `REFERENTIAL_CONSTRAINTS` and `TABLES` of a healthy tenant.
170
175
  - **Partial means arbitrary — never assume all-or-nothing.** The observed run landed **6 of 7** columns and **1 of 6** metadata rows from one file. "The migration ran" is not a fact about any individual statement in it.
171
176
  - **A `CustomRecordFields` row with no ACL grant is not automatically a defect.** Compare with the healthy client before "fixing" it — `Client_Growrk` recordId 312 (`c_netsuiteInternalSalesOrderId`, `TransferOrders`) has no grant, and `Client_Aig` is identical, so that is the normal cross-client state. Repairing it would be a change, not a fix.
172
177
  - **A client can be fine for months and then break on onboarding.** Before a client joins the module, its records resolve to the plain `_Model_Client_*` class with none of the `c_` fields, so the missing columns are unreachable. The 500s begin the day the client is switched onto the module-specific model — which is why the "it has always worked" evidence is misleading. Prove it with `Logs.Api`: successful 200s for the exact same request shape right up to the onboarding timestamp, 500s immediately after.
@@ -175,6 +180,7 @@ This is the write-side twin of the read-side rule above (*"the field named in th
175
180
 
176
181
  ## Change history
177
182
 
183
+ - 2026-10-28 — Gotchas: skip legacy leftovers other tenants still carry (only add model-declared objects); `toga-db` blocks `SHOW CREATE TABLE`, rebuild DDL from `information_schema`; Adyen full-wave audit outcome (TRUE-83365). (rgirish)
178
184
  - 2026-09-21 — Added the local-checkout case (behind on `dbchanges` reads as a schema gap; the fix is always an unapplied existing file — grep first) and the one-pass whole-schema `information_schema` column-count diff. (bala)
179
185
 
180
186
  ## Related
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: adyen
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-23
9
+ updated: 2026-10-28
10
10
  owners: ["rgirish"]
11
11
  files:
12
12
  - _underscore/Model/Adyen/ServiceRequest.php
@@ -26,7 +26,7 @@ Adyen's Reclaim SR → SO → NetSuite chain (same as Elite, but items come from
26
26
 
27
27
  ## Summary
28
28
 
29
- Adyen's API (`Apis` id 4, used by ServiceNow) posts Service Requests, all of type **"Reclaim"**, and **cannot send serial numbers**. Chain = Elite's: SR `postPost` → worker2 builds the Sales Order → SO `postPost` → NetSuite push. Adyen differences: items come from a seeded **"Reclaim" bundle** attached in `prePost`, the NetSuite PO # is meant to be the **employee name** (open — see Gotchas), and `prePost` fills the ship-to name from the contact.
29
+ Adyen's API (`Apis` id 4, used by ServiceNow) posts Service Requests, all of type **"Reclaim"**, usually **without serial numbers** (units started on beta 2026-10-27 — see below). Chain = Elite's: SR `postPost` → worker2 builds the Sales Order → SO `postPost` → NetSuite push. Adyen differences: items come from a seeded **"Reclaim" bundle** attached in `prePost`, the NetSuite PO # is meant to be the **employee name** (open — see Gotchas), and `prePost` fills the ship-to name from the contact.
30
30
 
31
31
  **Status (2026-10-23):** live in prod; prod `Client_Adyen.ApiPayloadInterceptors` rows 1, 2, 5 active; prod Core duplicates removed (see Gotchas). Ship-to name fix: `_underscore` PR #796 (TRUE-82978).
32
32
 
@@ -41,6 +41,14 @@ Adyen's API (`Apis` id 4, used by ServiceNow) posts Service Requests, all of typ
41
41
 
42
42
  **Why a bundle, not units:** a unit with only an item (no serial) matches an **existing** unit, so every SR would link the same unit — see [nested writes](../../../2.0/apps/api2/features/nested-relationship-writes.md). `ServiceRequests_Bundles.bundleId` is `MATCH` (links only).
43
43
 
44
+ ### When Adyen sends units (beta since 2026-10-27; verified 2026-10-28, TRUE-83365)
45
+ Payload: `serviceRequestUnits[].unit {serialNumber, item:{partNumber}}`. Unit and item are **find-or-create**: prod `Core.RecordFields` 252 `service-request-units.unitId` = `MATCH_UPSERT`, 238 `units.itemId` = `MATCH_UPSERT`, 108 `items.partNumber` `isIdentifier=1`; `Client_Adyen.Apis_RecordFields` has 0 override rows.
46
+ - Real beta call (SR100024 id 31, ref `TRUE-83365-TEST`): new serial `K64V0249WC` → Unit 9; unknown partNumber `Mac16,6` → new bare Item 13 (no description, no manufacturer).
47
+ - **Unit match = `serialNumber` AND `itemId`.** Same serial with a different partNumber → a **second** Unit.
48
+ - Unit with serial but **no item** → MySQL 1048 "Column 'itemId' cannot be null" (beta log 307).
49
+ - Every new partNumber value = a new Item. Adyen was told (2026-10-28) to send a consistent short part number (Apple model id or part number), never free text.
50
+ - Beta test data in dev-sandbox `Client_Adyen`: serial `ADYENTESTSERIAL001`, item `TEST-PN-ADYEN-001` (uuid `df75dfc0-8cfd-11f1-8d3b-0afffae7c6bf`).
51
+
44
52
  ### Decisions (PM, 2026-10-08)
45
53
  - Reclaim bundle = `SVC-FS-OFFBOARD-ADN` (NS 1579821) + **`Misc Laptop`** (NS 1545437), both $0. Keep Misc Laptop — **not** "Apple Macbook 14in" (NS 1625438).
46
54
  - NetSuite PO # (`otherrefnum`) = employee name, matching the hand-entered offboarding orders so ops can search NetSuite by name.
@@ -67,9 +75,12 @@ NS SOs: 290879, 292873, 292882, 292874, 292875, 292876, 288261, 292872. **290879
67
75
  - **Open — NetSuite `otherrefnum` on SR-generated SOs shows the SR number (e.g. SR100035), not the employee name**, though `queueNetsuiteCreate()` sets it and is on `_production` (662ff362). Not investigated; do not rely on name search in NetSuite for these SOs.
68
76
  - **Wrong `c_externalReferenceId` is usually Adyen's payload, not Toga.** Check `Logs_Adyen.Api` `requestPayload` for the POST first — see [profile → data gaps](../profile.md).
69
77
  - **Open — Adyen resending the same SR creates a duplicate.** `c_externalReferenceId` is not unique (SR100024 duplicates SR100021 — see [profile](../profile.md)).
78
+ - **⚠ OPEN decision — units suppress the Reclaim bundle.** `prePost` attaches the bundle only when there are **no units and no bundles**. An SR with units gets no bundle, so its SO loses `Misc Laptop` + `SVC-FS-OFFBOARD-ADN`. PM to decide (TRUE-83365).
79
+ - **Open (beta only) — SO job ran before the SR committed.** dev-sandbox `Core.WorkerJobs` 2675596 (`Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest`) ran the same second as the SR POST and failed "ServiceRequests.id=31 not found in Client_Adyen in Worker/Sync/ServiceRequest.php:68". Prod: last 5 Adyen runs OK (latest 2026-10-26). Not investigated.
80
+ - **Beta = dev-sandbox now** (was client-sandbox) — see [profile → Environments](../profile.md).
70
81
  - **Open — inbound-sync duplicate race** is a shared worker issue — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md#gotchas).
71
82
 
72
83
  ## Change history
73
- - 2026-10-23 — Fixed blank NetSuite ship-to name: `prePost` fills `deliverToAddress.addressee` from the contact (uuid-only address skipped); `otherRefNum` = employee name marked unverified (NS shows SR number) (TRUE-82978). (rgirish)
84
+ - 2026-10-28 — Adyen sends units on beta: unit + item find-or-create (serial AND item match), bare Items per new partNumber, 1048 without item; units skip the bundle (open); beta SO job ran before SR commit (open) (TRUE-83365). (rgirish)- 2026-10-23 — Fixed blank NetSuite ship-to name: `prePost` fills `deliverToAddress.addressee` from the contact (uuid-only address skipped); `otherRefNum` = employee name marked unverified (NS shows SR number) (TRUE-82978). (rgirish)
74
85
  - 2026-10-08 — NetSuite PO # = employee name (`buildEmployeeName`); Adyen map entry (location id 1, ship method 5); prod Core interceptor duplicates 15–17 deactivated; Misc Laptop kept (PM). (rgirish)
75
86
  - 2026-09-30 — Built: Reclaim bundle attach in `prePost`, Elite-style `postPost`, setup migration 2026-09-30a. (rgirish)
@@ -13,7 +13,7 @@ project: Worker
13
13
  client: adyen
14
14
  type: profile
15
15
  status: draft
16
- updated: 2026-10-23
16
+ updated: 2026-10-28
17
17
  owners: ["rgirish"]
18
18
  files:
19
19
  - worker/crons/toga2/netsuite/sync_togasupply_adyen.php
@@ -36,7 +36,7 @@ related:
36
36
  - ../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md
37
37
  ---
38
38
 
39
- Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply sync, status draft; open for the prod tracking-API test orders + SR↔NetSuite matching, the `isFulfillable` column gap, the beta=client-sandbox env trap, the ACL findings, and the Reclaim SR → SO → NetSuite chain.
39
+ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply sync, status draft; open for the prod tracking-API test orders + SR↔NetSuite matching, the schema drift (isFulfillable fixed; prod translation tables + 4 columns still missing), which env "beta" lands in, the ACL findings, and the Reclaim SR → SO → NetSuite chain.
40
40
 
41
41
  ## Summary
42
42
 
@@ -48,7 +48,7 @@ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply
48
48
  - **NetSuite → TOGa Supply sync.** Per-client thin wrapper over the shared engine — full mechanics on [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
49
49
  - **Transfer orders ENABLED** (`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS = true`). Adyen's NetSuite transfer orders arrive **as NetSuite sales orders**, classified by `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` — `holdInvoice === true` **AND** `total === 0`, same rule as GroWrk (see [GroWrk transfer order flow](../growrk/features/transfer-order-flow.md)). Mode is now declared explicitly in Adyen's launcher; `App_Api_Toga2::isTransferOrder()` throws if it is missing, so **`library` and `worker` must deploy together** for Adyen.
50
50
  - **IDs:** `Core.Clients` id **48**; NetSuite customer **31054** ("4950 Adyen"); Adyen API = `Apis` id 4.
51
- - **Reclaim SR → Sales Order → NetSuite**: same chain as Elite, items from a seeded "Reclaim" bundle (Adyen sends no serials); NetSuite PO # = employee name — see [Adyen reclaim flow](./features/service-request-salesorder-netsuite.md) (also holds the 8-order backlog map).
51
+ - **Reclaim SR → Sales Order → NetSuite**: same chain as Elite, items from a seeded "Reclaim" bundle when the SR has no units (Adyen began sending units on beta 2026-10-27 — open decision); NetSuite PO # = employee name — see [Adyen reclaim flow](./features/service-request-salesorder-netsuite.md) (also holds the 8-order backlog map).
52
52
  - **`Client_Adyen`** is one of the client DBs with surface-layer / ACL drift noted on the [ACL permission chain](../../2.0/apps/_underscore/features/acl-permission-chain.md) and [surface-layer schema](../../2.0/apps/dbchanges2/features/surface-layer-schema.md) docs.
53
53
 
54
54
  ## Gotchas
@@ -61,8 +61,8 @@ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply
61
61
  ### Environments — where `Client_Adyen` lives, and which one "beta" means
62
62
 
63
63
  - `toga_find_schema` returns the tenant DB in exactly **three** environments: **prod, dev-sandbox, client-sandbox**. There is **no `Client_Adyen` in `client-beta`**.
64
- - **⚠ Adyen's working beta calls hit `client-sandbox`, NOT `dev-sandbox`.** Calls go to `api.beta.togahub.com` with a JWT carrying `"env":"beta"`, which maps to the MCP environment **`client-sandbox`**. `Core.Domains` in client-sandbox shows `api.beta.togahub.com` with `env = 'beta'` and **`clientId = NULL`** — a shared API domain, so the tenant comes from the **JWT's client uuid**, not the hostname.
65
- - Comparing **dev-sandbox against prod** compares two environments the developer is not using — it produced **three consecutive wrong answers** in the 2026-09-08 session. Decode the token's `env` + `id.client.uuid` before any environment diff — recipe in [non-prod metadata drift, step 1](../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md#steps).
64
+ - **⚠ As of 2026-10-28, Adyen's beta calls land in `dev-sandbox`, NOT `client-sandbox`** (corrected — on 2026-09-08 they landed in client-sandbox). Calls go to `api.beta.togahub.com` with a JWT `"env":"beta"`; the domain is shared (`Core.Domains.clientId = NULL`), so the tenant comes from the **JWT's client uuid**. Proof (TRUE-83365): client-sandbox `Logs_Adyen.Api` 0 rows, `Client_Adyen.Items` 0 rows; dev-sandbox `Logs_Adyen.Api` 311 rows, last 2026-10-27 16:19:59.
65
+ - **Rule: the beta→env mapping moves. Before any env compare, find where the tenant's `Logs_<Client>.Api` rows actually are** (and decode the token's `env` + `id.client.uuid`) — recipe in [non-prod metadata drift, step 1](../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md#steps). Guessing the env gave **three wrong answers** in the 2026-09-08 session.
66
66
 
67
67
  ### Prod tracking-API test orders — linked to real NetSuite (2026-09-28)
68
68
 
@@ -90,19 +90,19 @@ Adyen tests the tracking API on **prod**. `2026-09-22a - AdyenApiTestSeed.sql` s
90
90
  4. **Bad `c_externalReferenceId` sent by Adyen (not a Toga bug, 2026-10-23, TRUE-82978).** SR100027 (Sam Mukherjee, `Logs_Adyen.Api` log 15707) = `"N/A"`; SR100028 (Monaem Billah, log 16033) = `"Employee ID 20042"`. No later write changed them. Monaem's earlier POST (log 11742, ref 48755605) made SR100026 (different address, no SO). SR100034 ref 766009 is 6 digits vs the usual 8 — unconfirmed. **Open:** waiting on Adyen for correct refs, then a `Client_Adyen` SQL fix. Proposed, not decided (PM's call): `prePost` rejects a Reclaim with empty/non-digit ref as `_Exception_Validation` 400.
91
91
  - Triage: read `Logs_Adyen.Api.requestPayload` for the SR's POST before assuming a Toga bug.
92
92
 
93
- ### Open defect — `Client_Adyen.Items` missing `isFulfillable` (prod AND beta)
93
+ ### Schema drift — `Client_Adyen` vs every other tenant
94
94
 
95
- - Root cause of the 2026-09-08 "works in sandbox, not in production" report; **NOT an ACL problem**. Adyen is the **only** tenant missing the column.
96
- - **Symptom:** `POST /v2/service-requests` returns **EV-12**, wrapping MySQL **`Error #: 1054 - Unknown column 'isFulfillable' in 'field list'`** on an `INSERT INTO Items`.
97
- - **Why an Items INSERT:** the Create-SR payload carries `serviceRequestUnits[].unit.item.partNumber`. When no item matches that partNumber, api2 **creates a new Item**, and that nested INSERT fails. `_underscore/Model/Client/Item.php:37` declares `public $isFulfillable = self::FIELD_BOOLEAN;` for **all** clients, so every Items INSERT includes the column.
98
- - **Fleet counts (2026-09-08):** prod **34 of 35** client DBs have it (Adyen missing); beta (client-sandbox) **29 of 30** (Adyen missing).
99
- - **Cause:** Adyen was provisioned/migrated before the column was added, and that migration's fan-out skipped this tenant. Same tenant is the outlier in *both* environments → provisioning gap, not a skipped environment run.
100
- - **⚠ Adyen may be missing OTHER columns from the same migration wave.** Nobody has audited beyond this one column. Do that before declaring Adyen healthy — see [client schema-drift audit](../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md).
101
- - **Proposed fix — NOT yet run** (could not test in production at the time). Shape/position copied from `Client_Compass`: `tinyint unsigned NULL DEFAULT NULL`, ordinal 24, between `isPoRequired` and `inventoryType`. Run on **beta first, verify, then prod**.
95
+ **`Items.isFulfillable` — RESOLVED in prod and dev-sandbox (2026-10-28).**
96
+ - Symptom was `POST /v2/service-requests` → **EV-12** wrapping MySQL **1054 Unknown column 'isFulfillable'** on a nested `INSERT INTO Items` (new `unit.item.partNumber` creates an Item; `_underscore/Model/Client/Item.php:37` declares the column for all clients). Not an ACL problem.
97
+ - Prod already had it (ordinal 24, after `isPoRequired`) — someone ran it before 2026-10-28, no repo file found. dev-sandbox got `ALTER TABLE Items ADD COLUMN isFulfillable TINYINT UNSIGNED NULL DEFAULT NULL AFTER isPoRequired` 2026-10-28, verified (TRUE-83365).
98
+ - Cause: Adyen was provisioned before that migration wave and its fan-out skipped this tenant.
102
99
 
103
- ```sql
104
- ALTER TABLE `Items` ADD COLUMN `isFulfillable` TINYINT UNSIGNED NULL DEFAULT NULL AFTER `isPoRequired`;
105
- ```
100
+ **Full audit (2026-10-28, TRUE-83365)** — `information_schema` diff vs all tenants (32 dev-sandbox, 37 prod). Missing in **both** prod and dev-sandbox Adyen:
101
+ - 8 tables: `AssortmentTranslations`, `BundleTranslations`, `FeatureTranslations`, `ItemCategoryFeatureGroupTranslations`, `ItemCategoryTranslations`, `ItemFeatureTranslations`, `NotificationTranslations`, `PersonaTranslations`.
102
+ - 4 model-declared columns: `Assortments.isActive`, `AssortmentItems.sortOrder`, `Entitlements.serviceAddressId`, `TicketStages.ticketTypeId`.
103
+ - DDL copied from dev-sandbox `Client_Quad`/`Client_True` and prod `Client_Elite`. Developer ran it on **dev-sandbox** `Client_Adyen` 2026-10-28, verified. (dev-sandbox `Client_Elite` was also missing `AssortmentItems.sortOrder`; added + verified same day.)
104
+ - **⚠ OPEN: PROD `Client_Adyen` still missing all of the above.** Needs a `dbchanges2/Client_Adyen/` catch-up file (not written in TRUE-83365 — the dbchanges2 checkout was on another ticket's branch).
105
+ - Leftover tables/columns other tenants carry but code no longer uses were skipped on purpose — list in [client schema-drift audit](../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md#gotchas).
106
106
 
107
107
  ### Verified: Adyen's PROD ACL is NOT missing anything (2026-09-08)
108
108
 
@@ -126,7 +126,7 @@ Listed only so nobody re-derives them and treats them as advice:
126
126
  2. `INSERT AclFieldPermissions` for recordFieldId 357, roleId 3 — same, **unverified**.
127
127
  3. `INSERT AclFieldPermissions` for recordFieldId 2087, roleId 1 into **prod** — **flatly wrong**; prod already has that row (id 3460). A misread query result caused it.
128
128
 
129
- The `AclRecordPermissions` prod-vs-dev-sandbox route-name diff from that session is **unverified for the same reason** (used dev-sandbox). Its `tracking-numbers` finding must be re-run against **client-sandbox** before anyone acts on it. **Only the `isFulfillable` `ALTER` above is a live recommendation.**
129
+ The `AclRecordPermissions` prod-vs-dev-sandbox route-name diff from that session is **unverified for the same reason** (used dev-sandbox). Its `tracking-numbers` finding must be re-run against the env beta actually lands in (see *Environments*) before anyone acts on it. (The beta-vs-prod ACL table above was measured 2026-09-08, when beta = client-sandbox.)
130
130
 
131
131
  ## Open questions
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1185",
3
+ "version": "1.0.1186",
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",