toga-ai 1.0.589 → 1.0.591
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +48 -1
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +110 -1
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md +101 -3
- package/knowledge/clients/aig/INDEX.md +1 -1
- package/knowledge/clients/aig/features/entitlement-intake.md +157 -1
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
33
33
|
| [NetSuite Sales Order sync — ship-to address, phone, and PO reference sourcing](features/netsuite-salesorder-address-phone-sync.md) | `_Trait_Netsuite_SalesOrder` is the **shared** sales-order importer composed into **22 client models** (every client on the dbchanges2 `netsuite` module). | _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Model.php, dbchanges2/_modules/netsuite/2026-08-10a - AddressPhoneNumberApiRoleAcl.sql |
|
|
34
34
|
| [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php, _underscore/Model/Client/TableView.php |
|
|
35
|
-
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
35
|
+
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/Model.php, _underscore/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
|
|
36
36
|
| [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 |
|
|
37
37
|
| [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 |
|
|
38
38
|
| [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 |
|
|
@@ -6,10 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-17
|
|
10
10
|
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Database.php
|
|
13
|
+
- _underscore/Model.php
|
|
13
14
|
- _underscore/Query.php
|
|
14
15
|
- _underscore/ApiRequest.php
|
|
15
16
|
- _underscore/Model/Client/Logs/Api.php
|
|
@@ -177,8 +178,54 @@ here — they live in `Config/*.ini`.)
|
|
|
177
178
|
(`db_logs`) — the laptop trap there is documented separately in the worker NetSuite bootstrap
|
|
178
179
|
notes.
|
|
179
180
|
|
|
181
|
+
## WITHDRAWN — the "alias-keyed `$_modelCache` cross-tenant leak" hypothesis (2026-08-17)
|
|
182
|
+
|
|
183
|
+
**There is no known cross-tenant leak. Do not cite this as a security concern.** An earlier entry in
|
|
184
|
+
this doc (same day, TRUE-80562) raised an *unproven* hypothesis that `_Database::$_modelCache`,
|
|
185
|
+
being keyed by the shared alias `'Client'`, was emitting one tenant's `c_` columns on another
|
|
186
|
+
tenant's `INSERT`. **That is not what happened.** The trigger was an AIG `Units` write failing MySQL
|
|
187
|
+
1054 on six `c_` columns; the real cause was a **missing migration on beta**, not a cache collision:
|
|
188
|
+
|
|
189
|
+
- `dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql` adds all six columns
|
|
190
|
+
(`c_itemDescription`, `c_lastModified`, `c_chargingBrick`, `c_deviceStatus`, `c_chargingCable`,
|
|
191
|
+
`c_grade`) **and** their `CustomRecordFields` rows to every client whose `_modules.txt` lists
|
|
192
|
+
`netsuite`. **`Client_Aig/_modules.txt` contains exactly `netsuite`** — AIG is opted in, so those
|
|
193
|
+
are **AIG's own fields**, not another tenant's.
|
|
194
|
+
- **Prod `Client_Aig.Units` has all 7 custom columns.** Beta simply never ran that module file — the
|
|
195
|
+
same execution gap as `Entitlements.serviceAddressId`. After the module migration was run manually
|
|
196
|
+
on beta `Client_Aig`, `Units` gained all 7 columns and the nested unit write succeeded.
|
|
197
|
+
|
|
198
|
+
**Methodology lesson — why the wrong conclusion was reached.** The model-class comparison that
|
|
199
|
+
produced the leak hypothesis was made against the **local `_underscore` checkout, which was on branch
|
|
200
|
+
`TRUE-80282`, while beta deploys `_sandbox-dev`.** Comparing beta *runtime* behavior against a local
|
|
201
|
+
checkout on a different branch is **not evidence**. api2's architecture doc already warns that
|
|
202
|
+
`_underscore` is cloned at build time from a moving branch
|
|
203
|
+
([environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md));
|
|
204
|
+
this is that hazard biting in practice. Before attributing a runtime field list to a model class,
|
|
205
|
+
confirm which branch the environment actually built from.
|
|
206
|
+
|
|
207
|
+
**The real generalizable fact** — the beta migration fan-out gap is **not limited to `Client/`; it
|
|
208
|
+
also hits `_modules/<module>/`** — is recorded in
|
|
209
|
+
[non-prod metadata drift repair](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
210
|
+
|
|
180
211
|
## Change history
|
|
181
212
|
|
|
213
|
+
- 2026-08-17 (later pass) — **WITHDRAWN, supersedes the entry below.** The alias-keyed
|
|
214
|
+
`$_modelCache` cross-tenant-leak hypothesis is **not supported** and is no longer a live security
|
|
215
|
+
concern. The six `c_` columns are **AIG's own**: `_modules/netsuite/2026-07-10a -
|
|
216
|
+
UnitInventoryFields.sql` adds them to every client whose `_modules.txt` lists `netsuite`, and
|
|
217
|
+
`Client_Aig/_modules.txt` contains exactly that; prod `Client_Aig.Units` has all 7 custom columns
|
|
218
|
+
and beta had simply never run the module file. Running it on beta fixed the write. The wrong
|
|
219
|
+
conclusion came from comparing beta runtime behavior against a **local `_underscore` checkout on
|
|
220
|
+
branch `TRUE-80282`** while beta deploys `_sandbox-dev` — the moving-branch hazard the api2
|
|
221
|
+
architecture doc already warns about. (mhammontree)
|
|
222
|
+
- 2026-08-17 — Recorded an **unproven open question** (TRUE-80562): an AIG nested `Units` write
|
|
223
|
+
emitted four *other* tenants' `c_` columns (1054 → EV-12) although AIG's schema, its
|
|
224
|
+
`CustomRecordFields` metadata, and its model class all declare exactly one custom field. Suspected
|
|
225
|
+
mechanism is the **alias-keyed `_Database::$_modelCache['Client'][$sql]`** collision; whether the
|
|
226
|
+
static survives across PHP-FPM requests is unknown, and that determines whether this is a
|
|
227
|
+
within-request artifact or a cross-tenant leak. Flagged for security review; no code changed.
|
|
228
|
+
(mhammontree)
|
|
182
229
|
- 2026-07-30 — Added four framework-level gotchas surfaced while building the all-client email
|
|
183
230
|
queue monitor: the log-DB name has the same never-string-build rule as the client DB;
|
|
184
231
|
`registerClientDatabases()` is unsafe for an all-client loop (INNER JOINs on client+archive
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-17
|
|
10
10
|
owners: ["bala", "mhammontree", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -94,6 +94,60 @@ fixing. Until then the caller-side workaround is a follow-up identifier-only `PU
|
|
|
94
94
|
self-heal that detects the null/mismatched echo (shipped on the BDR funnel side — see
|
|
95
95
|
web-funnel-app.md).
|
|
96
96
|
|
|
97
|
+
## ⚠ CREATE path: a nested singular FK is matched TENANT-WIDE by VALUE and RE-PARENTED
|
|
98
|
+
|
|
99
|
+
The forward-singular-FK back-reference machinery has a second, worse manifestation — on the
|
|
100
|
+
**CREATE** path, and the damage is a **stolen child row** rather than a null pointer.
|
|
101
|
+
|
|
102
|
+
A nested `primaryContactEmailAddress` / `primaryContactPhoneNumber` object is matched **across the
|
|
103
|
+
whole tenant by its VALUE** (`emailAddress` / `phoneNumber`) — **not** by `uuid` — and the matched
|
|
104
|
+
child's back-reference `contactId` is then **flipped to the newest writer**, silently detaching it
|
|
105
|
+
from its previous owner.
|
|
106
|
+
|
|
107
|
+
**Measured on beta `Client_Aig` 2026-08-17 (TRUE-80562).** Two sequential POSTs each created a new
|
|
108
|
+
Contact (**854** at 10:19, **856** at 10:41). Afterwards:
|
|
109
|
+
|
|
110
|
+
- Contacts **854 AND 856** both have `primaryContactEmailAddressId = 250` and
|
|
111
|
+
`primaryContactPhoneNumberId = 18`.
|
|
112
|
+
- `ContactEmailAddresses` row **250** has `contactId = 856`.
|
|
113
|
+
- So contact **854 still points at a child row that now belongs to contact 856** — 854's primary
|
|
114
|
+
email is effectively **detached**.
|
|
115
|
+
|
|
116
|
+
**Singular and collection paths DIVERGE — only the forward singular FK re-parents.** The collection
|
|
117
|
+
members were **not** stolen: the nested `contactEmailAddresses` entries produced rows **251/252**
|
|
118
|
+
owned by 854 and **separate new rows 255/256** owned by 856. Same payload, same request — different
|
|
119
|
+
behavior per path.
|
|
120
|
+
|
|
121
|
+
> **Production implication.** Any two contacts sharing an email address or phone number — spouses, a
|
|
122
|
+
> shared household line, a corporate address — **collapse onto one child row whose owner flips to
|
|
123
|
+
> whoever wrote last**. For AIG this compounds with the `c_aigCustomerId` integer overflow (314 of
|
|
124
|
+
> 353 prod contacts clamped to a single identifier value), so **AIG contact identity on prod is not
|
|
125
|
+
> reliably distinct**. See
|
|
126
|
+
> [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
|
|
127
|
+
|
|
128
|
+
Same back-reference machinery as the
|
|
129
|
+
[UPDATE-path back-reference injection](#update-path-back-reference-injection-defeats-the-uuid-forced-match-primary-pointer-bug)
|
|
130
|
+
above; treat them as one defect with two symptoms when a platform fix is scoped.
|
|
131
|
+
|
|
132
|
+
## Matching is "link-AND-UPDATE", not "link" — a matched row is mutated from the payload
|
|
133
|
+
|
|
134
|
+
A nested related object that **matches** an existing record does not merely link to it — the engine
|
|
135
|
+
also **writes the payload's fields onto that existing row**. So partner-supplied data can **mutate
|
|
136
|
+
shared catalog rows**.
|
|
137
|
+
|
|
138
|
+
**Measured (beta `Client_Aig`, 2026-08-17):** the nested `unit.item` matched `Client_Aig.Items`
|
|
139
|
+
id **1** by `partNumber`, and the payload's `modelNumber` was then written onto it — `Items` id 1
|
|
140
|
+
`modelNumber` went from `NULL` to a test value, `dtUpdated 2026-08-17 10:41:17`.
|
|
141
|
+
|
|
142
|
+
> **"Link vs. create" is really "link-and-update vs. create". Matching is not read-only.**
|
|
143
|
+
|
|
144
|
+
**Caveat on this example — do not over-read it.** The test payload put the **sale item** part number
|
|
145
|
+
in `unit.item`, whereas AIG's real payload sends the **device** part number there. Per the AIG data
|
|
146
|
+
model those are two different row kinds in the dual-purpose `Items` table (sale item =
|
|
147
|
+
`itemCategoryId` NULL; unit/device item = `itemCategoryId` set), so **real traffic would not mutate
|
|
148
|
+
this particular row** — but it would do exactly the same thing to the **device item's** row. The
|
|
149
|
+
behavior is general; the specific row in the measurement is an artifact of the test payload.
|
|
150
|
+
|
|
97
151
|
## Per-API overrides (`Apis_RecordFields`)
|
|
98
152
|
|
|
99
153
|
The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
|
|
@@ -115,6 +169,38 @@ columns matter:
|
|
|
115
169
|
> Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
|
|
116
170
|
> change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
|
|
117
171
|
|
|
172
|
+
### Fix it with a migration — a manual one-row data patch on non-prod DOES NOT SURVIVE
|
|
173
|
+
|
|
174
|
+
The TRUE-79978 beta fix was applied by hand as a single `UPDATE` on 2026-07-09. The **identical
|
|
175
|
+
EV-12 recurred on beta on 2026-07-21** (3 requests, `Logs_Aig.Api` ids 93 / 95 / 98) with the same
|
|
176
|
+
signature. Evidence the tenant was **reseeded** underneath the patch: those log rows report
|
|
177
|
+
`apiId 2` as *"AIG Integrations"*, while `Client_Aig.Apis` on beta now names id 2 *"Agilant"* — the
|
|
178
|
+
tenant data was rewritten after 2026-07-21, which is also where an incidental
|
|
179
|
+
`overrideIsIdentifier = 1` on one row came from (not a deliberate re-patch).
|
|
180
|
+
|
|
181
|
+
> **Rule: any `Apis_RecordFields` identifier correction must ship as a `dbchanges2/Client_<Tenant>/`
|
|
182
|
+
> migration.** A manual patch on beta / dev-sandbox evaporates on the next tenant reseed and the
|
|
183
|
+
> bug returns looking brand new. (TRUE-80562.)
|
|
184
|
+
|
|
185
|
+
**Scope rule when correcting these rows — only touch fields whose BASE flag is already 1.** Fix a
|
|
186
|
+
`NULL` `overrideIsIdentifier` **only** where `Core.RecordFields.isIdentifier = 1`. A `NULL` override
|
|
187
|
+
on a base-`0` field is a harmless no-op; setting it to `1` **adds** an identifier the platform never
|
|
188
|
+
had and changes nested match-vs-create behavior for that API. On beta `Client_Aig` there were 15
|
|
189
|
+
override rows (all `apiId 2`) but only **5** qualified: `recordFieldId` **1202**
|
|
190
|
+
(*Entitlement fulfillment types.Name*), **1216** (*Entitlement fulfillment methods.Name*), **1259**
|
|
191
|
+
(*Entitlement units.Unitid*), **1386** (*Entitlement coverage types.Name*), **1651**
|
|
192
|
+
(*Contacts.Customerid*).
|
|
193
|
+
|
|
194
|
+
**Expect a next landmine — fix the whole set, not the one field in the error.** Correcting only
|
|
195
|
+
1202 moved the same EV-12 onto `entitlementFulfillmentMethod` (1216), because
|
|
196
|
+
`_Model_Aig_Entitlement::prePost` injects **both** name-only objects. Audit every qualifying row in
|
|
197
|
+
one migration.
|
|
198
|
+
|
|
199
|
+
**Diagnostic fingerprint:** the EV-12's `searchableIdentifierFields` list is the record's identifier
|
|
200
|
+
set **minus the stripped field** — e.g. `["id","uuid","code"]` on a record whose identifiers are
|
|
201
|
+
Id/Uuid/Code/**Name** means `name` was stripped by an override row. Read that list first; it names
|
|
202
|
+
the culprit without any DB query.
|
|
203
|
+
|
|
118
204
|
**Build divergence warning.** The `Apis_RecordFields` override feature (reading
|
|
119
205
|
`overrideIsIdentifier`/`overrideChildPolicy`) was introduced 2026-03-20 (api2 commit `624146d`
|
|
120
206
|
"Cache custom fields and API override lookups"). Environments on **older** builds ignore the
|
|
@@ -174,6 +260,29 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
174
260
|
|
|
175
261
|
## Change history
|
|
176
262
|
|
|
263
|
+
- 2026-08-17 (later pass) — Two new measured behaviors from the TRUE-80562 verification run on beta
|
|
264
|
+
`Client_Aig`. (1) **CREATE-path re-parenting:** a nested forward singular FK
|
|
265
|
+
(`primaryContactEmailAddress` / `primaryContactPhoneNumber`) is matched **tenant-wide by VALUE**,
|
|
266
|
+
not by uuid, and the matched child's back-reference `contactId` is **flipped to the newest writer**
|
|
267
|
+
— Contacts 854 and 856 both point at `ContactEmailAddresses` 250, which now belongs to 856, so
|
|
268
|
+
854's primary email is detached. **Collection members are NOT stolen** (rows 251/252 stayed with
|
|
269
|
+
854; 856 got new rows 255/256), so the singular and collection paths diverge. Prod implication: any
|
|
270
|
+
two contacts sharing an email/phone collapse onto one child row, compounding the AIG
|
|
271
|
+
`c_aigCustomerId` overflow. Same back-reference machinery as the 2026-07-20 UPDATE-path bug, but on
|
|
272
|
+
CREATE and with a stolen child instead of a null pointer. (2) **Matching is link-AND-UPDATE:** a
|
|
273
|
+
matched nested object is **mutated from the payload** — `unit.item` matched `Items` id 1 by
|
|
274
|
+
`partNumber` and wrote the payload's `modelNumber` onto it, so partner data can mutate shared
|
|
275
|
+
catalog rows. (mhammontree)
|
|
276
|
+
- 2026-08-17 — TRUE-80562: the TRUE-79978 **manual one-row beta patch did not survive** — the same
|
|
277
|
+
EV-12 recurred on beta 2026-07-21 (`Logs_Aig.Api` ids 93/95/98) after a tenant reseed (log rows
|
|
278
|
+
call `apiId 2` "AIG Integrations"; beta `Client_Aig.Apis` now names it "Agilant"). Identifier
|
|
279
|
+
corrections must ship as a `dbchanges2/Client_<Tenant>/` migration. Added the **scope rule** (only
|
|
280
|
+
fix `NULL` overrides where `Core.RecordFields.isIdentifier = 1` — 5 of 15 AIG rows qualified:
|
|
281
|
+
1202/1216/1259/1386/1651), the **next-landmine** warning (fixing 1202 alone moves the EV-12 to
|
|
282
|
+
1216 because `prePost` injects both objects), and the **`searchableIdentifierFields` fingerprint**
|
|
283
|
+
(base identifier set minus the stripped field names the culprit without a query). Verified
|
|
284
|
+
2026-08-17: `POST /v2/entitlements` on beta returned 201 with both injected fields resolved.
|
|
285
|
+
(mhammontree)
|
|
177
286
|
- 2026-08-03 (later pass) — **Correction, supersedes the entry below on one point.** The bare-string
|
|
178
287
|
collection shape does **not** make the addresses "vanish"/blank the consumer's field: the empty
|
|
179
288
|
`array_column()` result makes the consumer's primary-email fallback fire, so the stored value is
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
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
|
-
| [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/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
|
|
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 |
|
|
@@ -6,14 +6,19 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-17
|
|
10
10
|
owners: ["mhammontree", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
|
|
13
13
|
- dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql
|
|
14
14
|
- dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql
|
|
15
|
+
- dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql
|
|
16
|
+
- dbchanges2/Client_Aig/_modules.txt
|
|
15
17
|
- dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
|
|
16
18
|
- api2/Config/beta.ini
|
|
19
|
+
- api2/Config/sandbox-dev.ini
|
|
20
|
+
- dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql
|
|
21
|
+
- dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql
|
|
17
22
|
related:
|
|
18
23
|
- ../architecture.md
|
|
19
24
|
- ./client-schema-drift-audit.md
|
|
@@ -41,8 +46,12 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
|
|
|
41
46
|
## Steps
|
|
42
47
|
|
|
43
48
|
1. **Identify which cluster the environment actually reads.** `api.beta.togahub.com` reads the
|
|
44
|
-
**dev-sandbox** cluster
|
|
45
|
-
|
|
49
|
+
**dev-sandbox** cluster — "beta" is not its own database. Fix the right DB. Confirmed again
|
|
50
|
+
2026-08-17 from the other side: **`api2/Config/sandbox-dev.ini`** carries
|
|
51
|
+
`[api] _ = "https://api.beta.togahub.com/v2"` and `hostname = dev.sandbox.database.togahub.com`.
|
|
52
|
+
So the toga-db alias to target is **`dev-sandbox`**, *not* `client-beta` — e.g. `Client_Aig`
|
|
53
|
+
exists only in **prod and dev-sandbox** and is absent from `client-beta` entirely. Picking the
|
|
54
|
+
wrong alias wastes a full diagnostic cycle.
|
|
46
55
|
2. **Full-table compare the small metadata tables, prod vs. target.** `ApiPayloadInterceptors` in
|
|
47
56
|
particular is small enough to diff wholesale, and the drift is typically **exactly one row**
|
|
48
57
|
(dev-sandbox was missing `(recordId 28, PRE, POST)`, so `prePost` never defaulted
|
|
@@ -78,6 +87,76 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
|
|
|
78
87
|
6. **Re-test the feature end-to-end in that environment**, then record what is still missing so the
|
|
79
88
|
next person doesn't rediscover it.
|
|
80
89
|
|
|
90
|
+
## The `Client/` fan-out is skipped independently of `Core/` — and the gap is NON-CONTIGUOUS
|
|
91
|
+
|
|
92
|
+
Measured on dev-sandbox 2026-08-17 (TRUE-80562): **31 of 33 tenant DBs lack
|
|
93
|
+
`Entitlements.serviceAddressId`** (only `Client_Rate` and `Client_Elite` have it), while
|
|
94
|
+
**Core does have the RecordField (id 2763)**. So `Core/2026-07-22a` ran and the
|
|
95
|
+
**`Client/` fan-out of the same change did not.** Symptom: **EV-10 / MySQL 1054 "Unknown column
|
|
96
|
+
'serviceAddressId'" on every entitlement write** — for 31 tenants, not just the one you are
|
|
97
|
+
debugging. That migration's own header states the column must exist for all tenants *before* the
|
|
98
|
+
`_underscore` code deploys, so the code is live against schemas that never got the column.
|
|
99
|
+
|
|
100
|
+
> **Do not conclude "beta is behind as of date X."** The gap is **partial and non-contiguous** —
|
|
101
|
+
> verified against each file's actual `ALTER`:
|
|
102
|
+
>
|
|
103
|
+
> | State | `Client/` file |
|
|
104
|
+
> |---|---|
|
|
105
|
+
> | **MISSING** | `2026-07-10a` ItemFulfillments.returnAddressId |
|
|
106
|
+
> | **MISSING** | `2026-07-16a` TrackingNumbers.signatureType |
|
|
107
|
+
> | **MISSING** | `2026-07-22a` Entitlements.serviceAddressId |
|
|
108
|
+
> | **MISSING** | `2026-07-28a` TicketStages.ticketTypeId |
|
|
109
|
+
> | PRESENT | `2026-07-17` Items.isFulfillable |
|
|
110
|
+
> | PRESENT | `2026-07-22b` TrackingNumbers.length / weightMeasureId |
|
|
111
|
+
>
|
|
112
|
+
> A skipped `07-22a` sits **beside a `07-22b` that ran**. Check file-by-file; a date cutoff will
|
|
113
|
+
> mislead you.
|
|
114
|
+
|
|
115
|
+
### The gap is NOT limited to `Client/` — `_modules/<module>/` is skipped too
|
|
116
|
+
|
|
117
|
+
Verified 2026-08-17 on beta `Client_Aig`: **`_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`
|
|
118
|
+
had never run** either, so `Client_Aig.Units` was missing six `c_` columns that **production has**.
|
|
119
|
+
`Client_Aig/_modules.txt` contains `netsuite`, so the client *is* opted in — the file was simply not
|
|
120
|
+
applied. Symptom is identical: **MySQL 1054 on a nested write**, surfaced as EV-12 with a
|
|
121
|
+
`modelSaveError`.
|
|
122
|
+
|
|
123
|
+
> **Audit the module folders as well as `Client/`.** For each tenant, read its `_modules.txt` and
|
|
124
|
+
> diff every `_modules/<module>/` file against the tenant's actual schema. A `Client/`-only audit
|
|
125
|
+
> reports a clean bill of health on a tenant that is still broken.
|
|
126
|
+
|
|
127
|
+
**This strengthens the "specific dates were skipped" reading.** The missing module file is dated
|
|
128
|
+
**`2026-07-10a`** — the **same date** as the already-recorded missing
|
|
129
|
+
`Client/2026-07-10a` (`ItemFulfillments.returnAddressId`). Two different fan-out folders both missing
|
|
130
|
+
the same date is far better explained by *that date's run failing* than by "beta is behind as of
|
|
131
|
+
date X."
|
|
132
|
+
|
|
133
|
+
**⚠ Diagnostic trap: do not attribute an unexpected runtime field list to a model class you read
|
|
134
|
+
locally.** During this session the missing-module 1054 was initially mis-diagnosed as a *cross-tenant
|
|
135
|
+
`$_modelCache` leak*, because the model-class comparison was made against a **local `_underscore`
|
|
136
|
+
checkout on branch `TRUE-80282`** while beta deploys **`_sandbox-dev`**. Beta runtime behavior
|
|
137
|
+
compared to a local checkout on a different branch is **not evidence** —
|
|
138
|
+
`_underscore` is cloned at build time from a moving branch
|
|
139
|
+
([environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md)).
|
|
140
|
+
Check the environment's built branch **before** concluding anything about model classes; the boring
|
|
141
|
+
"a migration never ran" explanation is the one that keeps being true.
|
|
142
|
+
|
|
143
|
+
**Fix direction: run the existing pending `Client/` files — do NOT author a new migration**, which
|
|
144
|
+
would duplicate a file already on `_main`. A scoped manual `ALTER` on one tenant unblocks your own
|
|
145
|
+
testing but leaves the other 30 broken; say so when you do it.
|
|
146
|
+
|
|
147
|
+
**LEAD (unproven) — duplicate letter suffixes may be why the executor skipped it.** `dbchanges2/Client/`
|
|
148
|
+
contains **three** files all named `2026-07-22a` (`EntitlementServiceAddressId`, `MeasureUomColumns`,
|
|
149
|
+
`Notification&PersonaTranslations`), violating the one-letter-per-day naming contract in
|
|
150
|
+
[architecture.md](../architecture.md). The skipped file is one of that trio. **Worth checking whether
|
|
151
|
+
the executor dedupes on date+letter** — if it does, the naming violation is the root cause and the
|
|
152
|
+
same trap is live wherever letters collide.
|
|
153
|
+
|
|
154
|
+
**Rule violation worth fixing separately:** `Client/2026-07-23a -
|
|
155
|
+
EntitlementServiceAddressIdFieldPermission.sql` references **`Core.RecordFields` and `Core.Records`
|
|
156
|
+
from a `Client/` fan-out file**, breaking the hard one-database-per-file cluster-isolation rule. Its
|
|
157
|
+
own comment says *"same server"* — which is exactly the trap the rule exists for: it works on
|
|
158
|
+
dev-sandbox (shared cluster) and would fail in production, where `Core` is a separate cluster.
|
|
159
|
+
|
|
81
160
|
## Environment facts worth knowing before you test here
|
|
82
161
|
|
|
83
162
|
- **NO carrier can be tested on ANY non-production environment.** Beta runs
|
|
@@ -156,6 +235,25 @@ blocked by the id-divergence hazard in item 2 above, which is why both go to jca
|
|
|
156
235
|
|
|
157
236
|
## Change history
|
|
158
237
|
|
|
238
|
+
- 2026-08-17 (later pass) — Extended the fan-out gap: it **also hits `_modules/<module>/`**, not just
|
|
239
|
+
`Client/`. Beta `Client_Aig` had never run `_modules/netsuite/2026-07-10a - UnitInventoryFields.sql`
|
|
240
|
+
despite `_modules.txt` listing `netsuite`, so `Units` lacked six `c_` columns prod has → 1054/EV-12
|
|
241
|
+
on nested unit writes. Note it is the **same date** as the missing `Client/2026-07-10a`, which
|
|
242
|
+
favors "specific dates were skipped" over "beta is behind as of date X". Added the **diagnostic
|
|
243
|
+
trap**: that 1054 was first mis-diagnosed as a cross-tenant `$_modelCache` leak because model
|
|
244
|
+
classes were read from a **local `_underscore` checkout on branch `TRUE-80282`** while beta deploys
|
|
245
|
+
`_sandbox-dev` — verify the environment's built branch before blaming code. (mhammontree)
|
|
246
|
+
- 2026-08-17 — TRUE-80562: documented that the **`Client/` fan-out is skipped independently of
|
|
247
|
+
`Core/`** on dev-sandbox — 31 of 33 tenant DBs lack `Entitlements.serviceAddressId` (only
|
|
248
|
+
`Client_Rate`/`Client_Elite` have it) while Core carries the RecordField (2763), so **entitlement
|
|
249
|
+
writes EV-10/1054 for 31 tenants**. The gap is **partial and non-contiguous** (missing 07-10a,
|
|
250
|
+
07-16a, 07-22a, 07-28a; present 07-17, 07-22b), so a date cutoff misleads. Fix = run the existing
|
|
251
|
+
pending files, never author a duplicate. Recorded the unproven **duplicate-letter lead** (three
|
|
252
|
+
`Client/2026-07-22a` files; the skipped one sits beside a `07-22b` that ran — check whether the
|
|
253
|
+
executor dedupes on date+letter) and a **cluster-isolation violation** in `Client/2026-07-23a`
|
|
254
|
+
(reads `Core.RecordFields`/`Core.Records` from a `Client/` file, justified as "same server").
|
|
255
|
+
Step 1 now cites `api2/Config/sandbox-dev.ini` and the `dev-sandbox` vs `client-beta` alias trap.
|
|
256
|
+
(mhammontree)
|
|
159
257
|
- 2026-08-14 — Recorded that metadata drift runs in **both** directions: a cleanup migration that
|
|
160
258
|
*removed* a row in production is as easily skipped in non-prod as an additive one. Evidence —
|
|
161
259
|
`Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql` had never run on
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [AIG Contract Reconciliation & Dealer Programs (STS_001 / SA_001)](features/contract-reconciliation.md) | 2.0 | How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the durable finding that the **Staples Advantage.com (`SA_001`) program do | _underscore/Model/Client/Entitlement.php, _underscore/Model/Aig/Entitlement.php, _underscore/Model/Client/Contact.php, _underscore/Model/Aig/Contact.php |
|
|
6
|
-
| [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql |
|
|
6
|
+
| [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, _underscore/Model/Aig/Unit.php, api2/Component/Api/V2/V2.php, api2/Config/sandbox-dev.ini, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql, dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql |
|
|
7
7
|
| [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
|
|
@@ -5,12 +5,15 @@ project: API
|
|
|
5
5
|
client: aig
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-17
|
|
9
9
|
owners: ["mhammontree"]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Aig/Entitlement.php
|
|
12
|
+
- _underscore/Model/Aig/Unit.php
|
|
12
13
|
- api2/Component/Api/V2/V2.php
|
|
14
|
+
- api2/Config/sandbox-dev.ini
|
|
13
15
|
- dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
|
|
16
|
+
- dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql
|
|
14
17
|
related:
|
|
15
18
|
- clients/aig/features/contract-reconciliation.md
|
|
16
19
|
- 1.0/apps/library/features/toga2-api-client-and-bridge.md
|
|
@@ -18,7 +21,9 @@ related:
|
|
|
18
21
|
- 2.0/apps/api2/features/nested-relationship-writes.md
|
|
19
22
|
- 2.0/apps/api2/architecture.md
|
|
20
23
|
- 2.0/apps/dbchanges2/architecture.md
|
|
24
|
+
- 2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md
|
|
21
25
|
- 2.0/apps/_underscore/architecture.md
|
|
26
|
+
- 2.0/apps/_underscore/features/per-client-database-connections.md
|
|
22
27
|
---
|
|
23
28
|
|
|
24
29
|
## Summary
|
|
@@ -149,6 +154,116 @@ recovered on 07-29 exactly as the resolution above describes. But the creating p
|
|
|
149
154
|
rows (through 2026-08-04) was **not positively identified** — confirm it in `Logs_Aig.Api` before
|
|
150
155
|
asserting either "intake is entirely down" or "intake is healthy."
|
|
151
156
|
|
|
157
|
+
> **Partly answered 2026-08-17 (TRUE-80562):** `Logs_Aig.Api` **does** carry non-2xx entitlement
|
|
158
|
+
> writes on beta (ids 93 / 95 / 98, all 400/EV-12 on 2026-07-21), so the "failures only ever land in
|
|
159
|
+
> core `Logs`" reading is too strong — `Logs_Aig` is a usable first stop for beta 4xx. Only 5xx-class
|
|
160
|
+
> failures reliably escape to the core/writer `Logs`.
|
|
161
|
+
|
|
162
|
+
## Which environment is "beta"? (`api.beta.togahub.com` = **dev-sandbox**)
|
|
163
|
+
|
|
164
|
+
`api2/Config/sandbox-dev.ini` carries `[api] _ = "https://api.beta.togahub.com/v2"` and a database
|
|
165
|
+
`hostname` of `dev.sandbox.database.togahub.com`. So for AIG/API work, **"beta" resolves to the
|
|
166
|
+
dev-sandbox cluster** (toga-db alias `dev-sandbox`), **not** the `client-beta` alias — which has no
|
|
167
|
+
`Client_Aig` schema at all. **`Client_Aig` exists only in prod and dev-sandbox.** Targeting the wrong
|
|
168
|
+
alias costs a whole diagnostic cycle. See
|
|
169
|
+
[non-prod metadata drift repair](../../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
170
|
+
|
|
171
|
+
## EV-12 on the injected fulfillment fields — fix it with a MIGRATION (TRUE-80562)
|
|
172
|
+
|
|
173
|
+
The TRUE-79978 EV-12 (an `Apis_RecordFields` override row with `overrideIsIdentifier = NULL`
|
|
174
|
+
stripping `name` as an identifier) **came back on beta**. The 2026-07-09 manual one-row `UPDATE`
|
|
175
|
+
**did not survive**: the identical error hit `entitlementFulfillmentType` again on **2026-07-21**
|
|
176
|
+
(`Logs_Aig.Api` ids 93 / 95 / 98). The tenant had been **reseeded** — those log rows call `apiId 2`
|
|
177
|
+
*"AIG Integrations"* while beta `Client_Aig.Apis` now names id 2 *"Agilant"*.
|
|
178
|
+
|
|
179
|
+
> **Manual data patches on beta evaporate. The fix must be a `dbchanges2/Client_Aig/` migration** —
|
|
180
|
+
> shipped as `2026-08-17a - TRUE-80562 AIG API identifier overrides.sql`.
|
|
181
|
+
|
|
182
|
+
- **Scope rule:** correct a `NULL` `overrideIsIdentifier` **only** where the BASE
|
|
183
|
+
`Core.RecordFields.isIdentifier = 1`. On a base-`0` field the NULL is a harmless no-op and setting
|
|
184
|
+
it to `1` would **add** an identifier and change nested match-vs-create behavior. Beta
|
|
185
|
+
`Client_Aig` had **15** override rows (all `apiId 2`); only **5** qualified — `recordFieldId`
|
|
186
|
+
**1202** (Entitlement fulfillment types.Name), **1216** (Entitlement fulfillment methods.Name),
|
|
187
|
+
**1259** (Entitlement units.Unitid), **1386** (Entitlement coverage types.Name), **1651**
|
|
188
|
+
(Contacts.Customerid).
|
|
189
|
+
- **Fix the whole set — there is a next landmine.** Fixing only 1202 moved the same EV-12 onto
|
|
190
|
+
`entitlementFulfillmentMethod` (1216), because `prePost` injects **both** name-only objects.
|
|
191
|
+
- **Fingerprint:** the error's `searchableIdentifierFields` is the record's identifier set *minus*
|
|
192
|
+
the stripped field — `["id","uuid","code"]` on record 186 (identifiers Id/Uuid/Code/**Name**)
|
|
193
|
+
points straight at `name`.
|
|
194
|
+
- **Verified 2026-08-17:** `POST /v2/entitlements` on `api.beta.togahub.com` returned **201** with
|
|
195
|
+
both injected fields resolved, 3 coverage types and 3 contact email addresses written.
|
|
196
|
+
|
|
197
|
+
## ⚠ `Client_Aig.Contacts.c_aigCustomerId` integer overflow — destroying identifiers IN PRODUCTION
|
|
198
|
+
|
|
199
|
+
**Needs its own ticket; higher priority than TRUE-80562.**
|
|
200
|
+
|
|
201
|
+
`Client_Aig.Contacts.c_aigCustomerId` is `int unsigned` (max **4,294,967,295**) on **both beta and
|
|
202
|
+
prod**, but AIG customer ids are **13 digits** (e.g. `1000045957545`). MySQL **silently clamps** every
|
|
203
|
+
one of them to the maximum.
|
|
204
|
+
|
|
205
|
+
- **Measured on prod 2026-08-17:** 353 contacts — **314 (89%) clamped to 4294967295**, 14 NULL, only
|
|
206
|
+
**26 distinct values** left.
|
|
207
|
+
- **Why it is severe, not cosmetic:** `c_aigCustomerId` is `isIdentifier = 1` in
|
|
208
|
+
`Client_Aig.CustomRecordFields`, i.e. a **searchable identifier used for nested-object matching**.
|
|
209
|
+
314 contacts now share one identifier value, so a match on it is ambiguous and can attach an
|
|
210
|
+
entitlement / email / claim to the **wrong customer**.
|
|
211
|
+
- **Fix direction:** a `dbchanges2/Client_Aig/` migration to `BIGINT UNSIGNED` **stops the bleeding
|
|
212
|
+
but recovers nothing** — the original values are destroyed in the column. **Possible recovery
|
|
213
|
+
source:** the true ids are in the logged `requestPayload` of inbound entitlement POSTs
|
|
214
|
+
(`Logs_Aig.Api` and the core/writer `Logs`), keyed by contract number. **Confirm that before
|
|
215
|
+
assuming the loss is permanent.**
|
|
216
|
+
|
|
217
|
+
## RESOLVED — the `Units` 1054 was a missing module migration, NOT a cross-tenant leak
|
|
218
|
+
|
|
219
|
+
**Supersedes the earlier "cross-tenant field list" open question. There is no leak — do not repeat
|
|
220
|
+
that hypothesis.** An AIG POST carrying `entitlementUnits` produced an `INSERT INTO Units` including
|
|
221
|
+
`c_itemDescription`, `c_lastModified`, `c_chargingBrick`, `c_deviceStatus`, `c_chargingCable`,
|
|
222
|
+
`c_grade` → **MySQL 1054**. Those are **AIG's own fields**:
|
|
223
|
+
|
|
224
|
+
- `dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql` adds all six columns **and**
|
|
225
|
+
their `CustomRecordFields` rows to every client whose `_modules.txt` lists `netsuite`.
|
|
226
|
+
**`Client_Aig/_modules.txt` contains exactly `netsuite`** — AIG is opted in.
|
|
227
|
+
- **Prod `Client_Aig.Units` has all 7 custom columns.** Beta had never run the module file — the same
|
|
228
|
+
execution gap as `Entitlements.serviceAddressId`. After running it manually on beta `Client_Aig`,
|
|
229
|
+
`Units` gained all 7 columns and the nested unit write succeeded with those fields present.
|
|
230
|
+
- The wrong conclusion came from comparing beta runtime behavior against a **local `_underscore`
|
|
231
|
+
checkout on branch `TRUE-80282`** while beta deploys `_sandbox-dev`. See
|
|
232
|
+
[per-client database connections](../../../2.0/apps/_underscore/features/per-client-database-connections.md)
|
|
233
|
+
and [non-prod metadata drift repair](../../../2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
234
|
+
|
|
235
|
+
## Verified end-to-end with the full real-shaped payload (TRUE-80562, 2026-08-17)
|
|
236
|
+
|
|
237
|
+
`POST /v2/entitlements` on `api.beta.togahub.com` returned **201 with `entitlementUnits`
|
|
238
|
+
INCLUDED** — 1 unit, 3 coverage types (`C001`/`C002`/`C039`), 1 fulfillment method, 3 contact email
|
|
239
|
+
addresses.
|
|
240
|
+
|
|
241
|
+
> **This supersedes the earlier partial verification.** Removing `entitlementUnits` was **only a
|
|
242
|
+
> diagnostic isolation step**. **AIG must keep sending it — the unit is the covered device.** Their
|
|
243
|
+
> payload was never wrong.
|
|
244
|
+
|
|
245
|
+
## `Entitlements.number` carries a UNIQUE key — a replay 400s, it is not idempotent
|
|
246
|
+
|
|
247
|
+
Re-POSTing an already-created contract number returns **EV-10 / MySQL 1062** *"Duplicate entry … for
|
|
248
|
+
key `Entitlements.number`"* as an **HTTP 400**. This matters against AIG's documented
|
|
249
|
+
[scheduled re-send model](#feed-is-a-scheduled-re-send-batch-not-one-shot--self-healing): the feed is
|
|
250
|
+
safe **only while it re-sends unacknowledged contracts**. A bulk replay or back-fill over
|
|
251
|
+
already-created contracts will 400 per row — plan for that before running one.
|
|
252
|
+
|
|
253
|
+
## Beta test data left behind by TRUE-80562 (clean-up list)
|
|
254
|
+
|
|
255
|
+
For whoever cleans up beta `Client_Aig` (row ids only — do not copy the QA contact PII anywhere):
|
|
256
|
+
|
|
257
|
+
- `Entitlements` id **838** (number `1000045957545`) and the 10:41 entitlement (number
|
|
258
|
+
`1000045957546`).
|
|
259
|
+
- `Contacts` **854** and **856**, with the cross-linked primary email row **250** / phone row **18**
|
|
260
|
+
described in
|
|
261
|
+
[nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
262
|
+
- `Items` id **1** — `modelNumber` polluted with a test value; **revert to `NULL`**.
|
|
263
|
+
- **Beta `Client_Aig` had two migrations applied MANUALLY this session** (`Client/2026-07-22a`
|
|
264
|
+
`serviceAddressId` ALTERs, and the `_modules/netsuite/2026-07-10a` module file), so this tenant is
|
|
265
|
+
now **ahead of whatever the beta executor believes it has applied**.
|
|
266
|
+
|
|
152
267
|
## Business Unit (`UserDefined2`) — answered and DROPPED (nothing to build)
|
|
153
268
|
|
|
154
269
|
AIG (Alissa, 2026-05-04) confirmed the **Business Unit / Value** field needs **nothing returned to
|
|
@@ -320,6 +435,20 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
320
435
|
find a searchable identifier for the injected object. Root cause (TRUE-79978) was an
|
|
321
436
|
`Apis_RecordFields` override row that stripped `name` as an identifier — see
|
|
322
437
|
[api2 nested-relationship writes → Per-API overrides](../../../2.0/apps/api2/features/nested-relationship-writes.md).
|
|
438
|
+
- **The empty-`partNumber` variant is CONFIRMED LIVE (observed 2026-08-17, not inferred).** A
|
|
439
|
+
payload with `saleItem.partNumber = ""` resolved to `saleItemId = 10` — on beta a row with
|
|
440
|
+
`partNumber = ''` and description *"2YR Tablet: ($300-$399.99)"*, an item **unrelated to the
|
|
441
|
+
submitted contract**. The same payload with a real code (`SP-3DAD-LTP2`) correctly resolved to
|
|
442
|
+
id 1. The doc predicted this; it now has a live sighting. The guard clause is still not written.
|
|
443
|
+
- **`prePost` OVERRIDES the client-submitted fulfillment method and nothing signals it.** A payload
|
|
444
|
+
sending `entitlementFulfillmentMethod {name: "Credit"}` produced a stored record of **"Gift Card"**
|
|
445
|
+
plus an injected type **"Reimbursement"** — the interceptor populates both from
|
|
446
|
+
`Items_EntitlementFulfillmentMethods`/`Types` for the matched sale item and **discards the
|
|
447
|
+
client-supplied value**. This may be intended (catalog is the authority), but the **201 response
|
|
448
|
+
does not signal the override**, so a partner cannot tell their value was dropped. **Open question
|
|
449
|
+
for the integration owner** — confirm intent, and consider echoing the resolved values.
|
|
450
|
+
- **Beta 4xx entitlement writes DO appear in `Logs_Aig.Api`** (ids 93/95/98, 2026-07-21). Check the
|
|
451
|
+
client log first for validation failures; reserve the core/writer `Logs` hunt for 5xx.
|
|
323
452
|
- **A lookup miss yields an empty `$itemId` that crashes the next query (confirmed in prod,
|
|
324
453
|
TRUE-79534).** The unguarded line is **`_underscore/Model/Aig/Entitlement.php:48`** in `prePost`,
|
|
325
454
|
reached from **`api2/Component/Api/V2/V2.php:2601`**. The malformed statement it emits is:
|
|
@@ -357,6 +486,33 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
357
486
|
|
|
358
487
|
## Change history
|
|
359
488
|
|
|
489
|
+
- 2026-08-17 (later pass) — **Correction + final verification, supersedes two points in the entry
|
|
490
|
+
below.** (1) The nested-`Units` "cross-tenant field list" open question is **WITHDRAWN** — the six
|
|
491
|
+
`c_` columns are AIG's **own**, added by `_modules/netsuite/2026-07-10a` to every client whose
|
|
492
|
+
`_modules.txt` lists `netsuite` (AIG's does); prod has all 7 columns, beta had never run the module
|
|
493
|
+
file. Running it on beta fixed the write. The wrong conclusion came from comparing beta runtime
|
|
494
|
+
against a **local `_underscore` checkout on branch `TRUE-80282`** vs beta's `_sandbox-dev`. (2)
|
|
495
|
+
Verified end-to-end with the **full real-shaped payload including `entitlementUnits`** — 201 with
|
|
496
|
+
1 unit / 3 coverage types / 1 fulfillment method / 3 emails; removing `entitlementUnits` earlier was
|
|
497
|
+
only an isolation step and **AIG must keep sending it**. Also recorded: `Entitlements.number` has a
|
|
498
|
+
**UNIQUE key**, so a replay of an already-created contract returns EV-10/1062 as HTTP 400 (matters
|
|
499
|
+
for any bulk back-fill); and a **beta clean-up list** of the rows/manual migrations this session
|
|
500
|
+
left behind. (mhammontree)
|
|
501
|
+
- 2026-08-17 — TRUE-80562 (beta EV-12 on entitlement intake). The TRUE-79978 **manual beta patch did
|
|
502
|
+
not survive a tenant reseed** — same EV-12 recurred 2026-07-21 (`Logs_Aig.Api` 93/95/98); the fix
|
|
503
|
+
now ships as `dbchanges2/Client_Aig/2026-08-17a`. Recorded the **scope rule** (only correct NULL
|
|
504
|
+
`overrideIsIdentifier` where base `Core.RecordFields.isIdentifier = 1` — 5 of 15 rows:
|
|
505
|
+
1202/1216/1259/1386/1651), the **next landmine** (fixing 1202 alone moves the error to 1216 since
|
|
506
|
+
`prePost` injects both objects), and the `searchableIdentifierFields` fingerprint. **Verified: 201
|
|
507
|
+
on `api.beta.togahub.com`.** Added: `api.beta.togahub.com` = **dev-sandbox** per
|
|
508
|
+
`api2/Config/sandbox-dev.ini` (`Client_Aig` exists only in prod + dev-sandbox, never
|
|
509
|
+
`client-beta`); beta **4xx** writes DO land in `Logs_Aig.Api`. New **⚠ prod data defect**:
|
|
510
|
+
`Contacts.c_aigCustomerId` is `int unsigned` while AIG ids are 13 digits — **314 of 353 prod
|
|
511
|
+
contacts clamped to 4294967295** on an `isIdentifier` field (ambiguous nested matching); needs its
|
|
512
|
+
own ticket, BIGINT migration does not recover the values. New **open question**: a nested `Units`
|
|
513
|
+
write emitted four other tenants' `c_` columns (1054) — alias-keyed `_modelCache` hypothesis,
|
|
514
|
+
unproven, needs security review. Confirmed the empty-`partNumber` trap live (`saleItemId = 10`)
|
|
515
|
+
and that `prePost` silently overrides a submitted `entitlementFulfillmentMethod`. (mhammontree)
|
|
360
516
|
- 2026-08-10 — Read-only prod investigation (no code/SQL changed). Added **Verified prod state**:
|
|
361
517
|
the TRUE-79534 catalog **is** loaded (`Client_Aig.Items` = 2,212 rows — 1,110 `ASI-*` / 558
|
|
362
518
|
`SM-*` / 37 `SP-*`; `ASI-2YG2` present), so a future 1064 here is the **empty-input** variant,
|
package/package.json
CHANGED