toga-ai 1.0.493 → 1.0.494

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,13 +6,15 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-28
9
+ updated: 2026-08-03
10
10
  owners: [jcardinal, mhammontree]
11
11
  files:
12
12
  - library/app/api/toga2.php
13
13
  - worker/crons/toga2/aig/sync_togasupply_aig.php
14
14
  - worker/crons/toga2/wje/sync_togasupply_wje.php
15
15
  related:
16
+ - ../../../2.0/apps/api2/features/nested-relationship-writes.md
17
+ - ../../../clients/aig/features/entitlement-intake.md
16
18
  - netsuite-suiteql-api-reference.md
17
19
  - netsuite-suiteql-rest-shim.md
18
20
  - ../../worker/features/netsuite-togasupply-per-client-sync.md
@@ -106,6 +108,80 @@ bool $enabled2to1, bool $enabled1to2)`. When `enabled2to1`:
106
108
  - The 1→2 direction (`enabled1to2`) is a stub (not yet implemented).
107
109
  - DB target is selected with `App_Model_Client::changeDbLinkToClientDatabase(getClientDatabaseNames()[$togaClientId])`.
108
110
 
111
+ ### TRUE-79401 payload shape — RESOLVED 2026-08-03 (nested; NO api2 work needed)
112
+
113
+ **The multi-email payload shape is settled, and the 2026-07-28 library change is the COMPLETE
114
+ implementation — zero api2/`_underscore` work is required.** (This supersedes the previous "open
115
+ question, owner Paulina".) Sources: AIG's post-entitlements API field-mapping spreadsheet, the
116
+ 2026-05-01 Teams meeting, and the 2026-05-04 AIG email exchange.
117
+
118
+ AIG's mapping document maps **both** `UserDefined3` (secondary email) **and** `UserDefined4` (third
119
+ email) to the **same** nested collection — `Entitlements.contact.contactEmailAddresses[].emailAddress`.
120
+ In the meeting the team looked up our own record/field names specifically to write them into that
121
+ document ("it is called contact email addresses and the field is called email address").
122
+
123
+ > **`UserDefined3` / `UserDefined4` are AIG's SOURCE-SIDE column labels only.** They are not, and
124
+ > never were, fields to implement on our side. Verified: `UserDefined3` appears **nowhere** in
125
+ > `api2` or `_underscore` — its only occurrences in the tree are a comment in
126
+ > `library/app/api/toga2.php` and this doc.
127
+
128
+ Correct inbound shape:
129
+
130
+ ```json
131
+ "contact": {
132
+ "primaryContactEmailAddress": { "emailAddress": "primary@example.com" },
133
+ "contactEmailAddresses": [
134
+ { "emailAddress": "primary@example.com" },
135
+ { "emailAddress": "second@example.com" }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ The primary **may** be repeated inside the collection — `syncContactToToga1Customer` de-dupes with
141
+ `array_unique`.
142
+
143
+ **Three distinct layers — only the last one is delimited.** Conflating them is the easy mistake:
144
+
145
+ | Layer | Shape |
146
+ |---|---|
147
+ | 1. AIG → us (V2 payload) | nested `contactEmailAddresses[]` **array of objects** |
148
+ | 2. `Client_Aig.ContactEmailAddresses` (2.0) | one **ROW** per address |
149
+ | 3. `TOGA_AIG.Customers.emailAddress` (1.0) | a single `'; '`-joined **STRING** |
150
+
151
+ The 2026-05-01 meeting's comma-vs-semicolon debate was entirely about **layer 3** (a legacy
152
+ `varchar(255)` can only hold a string) and says nothing about the inbound payload.
153
+
154
+ **Structural trap:** `contactEmailAddresses` is a **direct child collection**, so it is a **flat**
155
+ array of objects — *not* the double-wrapped bridge-table form used by its siblings in the very same
156
+ AIG payload (`entitlementEntitlementCoverageTypes: [{ entitlementCoverageType: {…} }]`). And a
157
+ wrong shape does **not** error: V2 returns **201** and drops the data — see
158
+ [api2 nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
159
+
160
+ **Purpose is verification/lookup ONLY.** Settled 2026-05-01: "it's just for visual, this is for
161
+ verification, it's a string." The use case is a support agent looking up a caller by **any** of
162
+ their addresses after pulling the contract number. Multi-recipient notification (CC/BCC on TogaDesk
163
+ tickets) was **explicitly rejected** — see the gotcha about the single-recipient registration email.
164
+
165
+ ### Open items (TRUE-79401)
166
+
167
+ 1. Confirm the library change is **merged and deployed to the prod worker** (git/deploy state not
168
+ verified as of 2026-08-03).
169
+ 2. **Beta verification of the nested write is outstanding.** Assert real
170
+ `Client_Aig.ContactEmailAddresses` **row counts** (never the HTTP status), covering **both** a
171
+ brand-new contact (CREATE) **and** a second entitlement for an **existing** contact (UPDATE).
172
+ The UPDATE path is the suspect one: the reverse back-reference injection at `V2.php:4985–4996`
173
+ breaks single-key forced-MATCH and explicitly names `primaryContactEmailAddress` as likely
174
+ affected (see the api2 doc, 2026-07-20).
175
+ 3. Give AIG's dev team a **concrete JSON example** (above), not just the
176
+ `contactEmailAddresses[].emailAddress` path expression, so they don't emit an array of bare
177
+ strings.
178
+ 4. Confirm with **Paulina** that the mapping document actually reached AIG. Risk: our 2026-05-04
179
+ email told AIG to send the email fields "only if they are necessary for the process" — ambiguous
180
+ phrasing, sent in the same breath as dropping Business Unit, which may have read as a de-scope.
181
+ No AIG reply. Meanwhile AIG won't enable the extra fields until we tell them the feature is live
182
+ → **both sides are waiting on each other.** The feature has been functionally complete since
183
+ 2026-07-28 and is gated on **communication + verification, not development.**
184
+
109
185
  ### `syncWithTogadesk()` — 2.0 ⇄ 1.0 (TOGaDesk)
110
186
 
111
187
  A large bi-directional sync gated by **two checkpoint parameters** stored in 2.0 via
@@ -176,18 +252,51 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
176
252
  enough to hold several `'; '`-joined emails, so TRUE-79401 needed **no** schema/dbchanges migration
177
253
  (the earlier fear that it was ~VARCHAR(55) was wrong). `TOGA_AIG` is the 1.0 legacy DB;
178
254
  `Client_Aig` is the 2.0 prod tenant.
179
- - **The multi-email path is not yet exercised (open, TRUE-79401).** As of 2026-07-28 all 20
180
- `Client_Aig.ContactEmailAddresses` rows are one-per-contact, and **no** api2/`_underscore` code
181
- references `UserDefined3`/`UserDefined4` or creates `ContactEmailAddresses` rows. Whether an api2
182
- intake mapping is still needed depends on how Staples / SA.com sends the extra emails: if they POST
183
- nested `contactEmailAddresses` records to the V2 API the library change is complete; if they send
184
- flat `UserDefined3`/`UserDefined4` fields expecting us to map them into email rows, an api2 intake
185
- mapping must be built. Open question owned by **Paulina**.
255
+ - **The multi-email path is not yet exercised in prod data (TRUE-79401)** — as of 2026-07-28 all 20
256
+ `Client_Aig.ContactEmailAddresses` rows are one-per-contact. The *payload-shape* question that
257
+ used to sit here is **RESOLVED** — see *TRUE-79401 payload shape* above. Verification (real row
258
+ counts on beta, CREATE **and** UPDATE paths) is the remaining open item.
259
+ - **Do not "fix" the single-recipient registration email.**
260
+ `_Model_Aig_Entitlement::postPost` sends the Staples Protection Plan registration email to
261
+ `addTo($payload->contact->primaryContactEmailAddress->emailAddress)` only. That is **correct by
262
+ design**: TRUE-79401's scope is *verification/lookup*, and the team explicitly rejected
263
+ CC/BCC-ing every address onto TogaDesk tickets. Multi-recipient notification is out of scope.
264
+ - **The dedupe `LIKE` is an equality match a stored list can no longer satisfy (latent).**
265
+ `syncContactToToga1Customer`'s fallback lookup is
266
+ `WHERE Customers.emailAddress LIKE '$primaryEmailAddress'` — **no wildcards**, so it is an
267
+ equality match. Now that the column can hold a `'; '`-joined list, a 1.0 row that already holds a
268
+ list will **not** match a primary-only lookup. Shielded in practice because dedupe is
269
+ authoritatively keyed on `c_togaCustomerId` first and a successful sync writes that id back to
270
+ 2.0, dropping the contact out of the `c_togaCustomerId IS NULL` query forever. It only bites if
271
+ the **write-back fails after the insert succeeds** — a retry then inserts a **duplicate**
272
+ customer instead of matching. Hardening if it ever fires: `LIKE '%$primaryEmailAddress%'`. Note
273
+ this is a consequence of a deliberate deviation: the 2026-05-01 meeting said to drop the primary
274
+ email entirely, but the implementation retained a separately-escaped primary for this lookup (the
275
+ right call — dropping it would have broken legacy record matching outright).
276
+ - **No `emailAddress2`/`emailAddress3` columns — deliberate.** The 2026-05-01 meeting explicitly
277
+ rejected adding extra email columns across the ~15 Toga Classic client DBs and chose the
278
+ delimited single field instead. That is **why TRUE-79401 has no `dbchanges` migration** — an
279
+ intentional decision, not an oversight. (The doc's earlier "fear that `Customers.emailAddress`
280
+ was ~VARCHAR(55)" traces to a garbled transcript line — "needs to fifty-five… maybe we should
281
+ change it to a long"; the column is `varchar(255)`. Closed.)
186
282
  - **PHP 7.2** target (prod worker/library) — no arrow functions, typed properties, `??=`, `match`.
187
283
  Lint with `C:\xampp7\php\php.exe -l`.
188
284
 
189
285
  ## Change history
190
286
 
287
+ - 2026-08-03 — TRUE-79401 **payload shape RESOLVED** (review/no-code session; sources: AIG's
288
+ field-mapping spreadsheet, the 2026-05-01 Teams meeting, the 2026-05-04 AIG email). Both
289
+ `UserDefined3` and `UserDefined4` map to the **same** nested
290
+ `contact.contactEmailAddresses[].emailAddress` collection; `UserDefined3/4` are **AIG's
291
+ source-side labels only** and exist nowhere in `api2`/`_underscore` — so **no api2 intake mapping
292
+ is needed** and the 2026-07-28 library change is the complete implementation. Replaced the
293
+ "open question, owner Paulina" gotcha with the resolved shape + a concrete JSON example. Also
294
+ recorded: purpose is **verification/lookup only** (multi-recipient notification explicitly
295
+ rejected, so `postPost`'s single-recipient email is correct by design); the three-layer
296
+ payload/row/delimited-string distinction; the extra-email-columns rejection that explains the
297
+ absent `dbchanges` migration; and the latent wildcard-less dedupe `LIKE` vs. a stored list.
298
+ Remaining: deploy confirmation, beta row-count verification (CREATE **and** UPDATE), sending AIG
299
+ a concrete JSON example, and confirming the mapping doc reached AIG. (mhammontree)
191
300
  - 2026-07-28 — TRUE-79401: `syncContactToToga1Customer` now stores a contact's **full** email list in
192
301
  1.0 `TOGA_AIG.Customers.emailAddress` as a `CUSTOMER_EMAIL_DELIMITER` (`'; '`)-joined, deduped,
193
302
  escaped string (was primary-only) for AIG/Staples support lookup. Added
@@ -9,7 +9,7 @@
9
9
  | [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
10
10
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
11
11
  | [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
12
- | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php |
12
+ | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
13
13
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
14
14
  | [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | The V2 engine logs **every inbound request** — success *and* failure, with response code and payload — and routes each log entry to the **client** log or the ** | api2/Component/Api/V2/V2.php, _underscore/Model/Client/Logs/Api.php, _underscore/Model/Core/Logs/Api.php |
15
15
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
@@ -6,10 +6,11 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-08-03
10
10
  owners: ["bala", "mhammontree", "tcox"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
+ - _underscore/Model/Client/ContactEmailAddress.php
13
14
  related:
14
15
  - ../architecture.md
15
16
  - ../../../clients/aig/features/entitlement-intake.md
@@ -124,6 +125,33 @@ build — audit such rows before/with the deploy. First seen in AIG (TRUE-79978)
124
125
  beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
125
126
  [AIG entitlement intake](../../../clients/aig/features/entitlement-intake.md).
126
127
 
128
+ ## A wrong-shaped nested collection returns 201 and silently drops the data
129
+
130
+ V2 **ignores payload keys it does not recognize** and does not validate the *element shape* of a
131
+ nested collection. Two distinct failure modes both return **HTTP 201 Created** with no error
132
+ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `Logs`:
133
+
134
+ 1. **Unknown / flat key.** A partner sends a source-side column label (e.g. a literal
135
+ `UserDefined3`) instead of the nested collection. The key is dropped; no child row is created.
136
+ 2. **Array of bare strings instead of array of objects.** `"contactEmailAddresses":
137
+ ["email2@domain.com"]` instead of `[{"emailAddress": "email2@domain.com"}]`. Nothing is
138
+ created, and any consumer doing `array_column($list, 'emailAddress')` gets an **empty** list —
139
+ so a downstream sync silently falls back to the primary email and the extra addresses vanish
140
+ with no trace.
141
+
142
+ **Operational rules that follow:**
143
+
144
+ - **When verifying a nested-collection integration, assert actual child ROW COUNTS** (e.g.
145
+ `SELECT COUNT(*) FROM Client_Aig.ContactEmailAddresses …`). **Never** treat the HTTP status as
146
+ evidence — a 201 proves only that the parent was written.
147
+ - **Never hand a partner a path expression as the spec.** `contactEmailAddresses[].emailAddress`
148
+ does not pin element structure and reads as "an array of emails" to many developers. Always give
149
+ a **concrete JSON example** of the collection, objects included.
150
+ - **Know whether the collection is direct or bridged.** A direct child collection is a **flat
151
+ array of objects** (`contactEmailAddresses: [{emailAddress}]`); a bridge/join collection is
152
+ **double-wrapped** (`entitlementEntitlementCoverageTypes: [{entitlementCoverageType: {…}}]`).
153
+ Both shapes routinely appear in the *same* payload, so the sibling collections actively mislead.
154
+
127
155
  ## Gotcha
128
156
 
129
157
  - **Nested write with only a non-identifier field silently creates duplicates.** This is a
@@ -135,6 +163,13 @@ beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
135
163
 
136
164
  ## Change history
137
165
 
166
+ - 2026-08-03 — Documented that V2 **silently ignores unrecognized payload keys and does not
167
+ validate nested-collection element shape**: both a flat/unknown key and an array of bare strings
168
+ (instead of an array of objects) return **HTTP 201** with the data dropped and no error logged
169
+ anywhere. Added the verification rule (assert child row counts, never the HTTP status), the rule
170
+ to give partners a concrete JSON example rather than a `collection[].field` path expression, and
171
+ the direct-vs-bridged collection shape distinction. Surfaced while resolving the AIG TRUE-79401
172
+ multi-email payload shape (`Client_Aig.ContactEmailAddresses`); no code changed. (mhammontree)
138
173
  - 2026-07-20 — Root-caused the **UPDATE-path** failure of the single-key-uuid forced-MATCH: the
139
174
  update path injects a reverse back-reference (`contactId = parent id`) into nested child objects
140
175
  at `V2.php:4985–4996` (commit `91d803f`) when the child has a back-FK to the parent, turning the
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-09
10
- owners: ["ajean", "jcardinal"]
9
+ updated: 2026-08-03
10
+ owners: ["ajean", "jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
13
13
  - worker2/Config/production.ini
@@ -140,6 +140,15 @@ process is identical for all.
140
140
  those files. Acceptable as a one-off — the ledger `s3Key` is updated to match — but if you
141
141
  ever need a minimal re-file, change the move test to compare only the `YYYY-MM-DD` folder
142
142
  segment and relocate while preserving the original filename.
143
+ - **There is NO transcript coverage before ~2026-06-12 — pre-June meetings are not in our store.**
144
+ `Team/Transcripts/Export` first ran successfully in production on **2026-06-12** (PR #78), and it
145
+ works off an **incremental watermark** with a bounded `lookbackDays`; nothing in its history shows
146
+ a multi-week backfill ever running. So searching `Team.TranscriptExports` /
147
+ `Team.TranscriptProcessing` / the `togaiq` KBs for a meeting older than ~2026-06-12 comes back
148
+ **empty even though the meeting WAS recorded**. Do not conclude "it wasn't recorded."
149
+ **Recovery path for an older meeting: Microsoft Graph** (which still retains it) or a manually
150
+ exported `.docx`/`.vtt` — not our own store. Hit in practice on 2026-08-03: a 2026-05-01 meeting
151
+ had to be supplied as a downloaded `.docx`.
143
152
  - **Cron rows cannot be `DELETE`d once they have fired.** `Core.WorkerJobs.cronJobId` has a FK
144
153
  (`WorkerJobs_CronJobs_FK`) to `CronJobs.id`, so a schedule change must `UPDATE` an existing
145
154
  row (and set `isActive = 0` on the ones being retired), not delete-and-reinsert. The historical
@@ -181,6 +190,12 @@ Policy gap; a `400` only on a UPN means the id was never resolved to a GUID. (A
181
190
 
182
191
  ## Change history
183
192
 
193
+ - 2026-08-03 — Documented the **pre-2026-06-12 coverage gap**: `Team/Transcripts/Export` first ran in prod on
194
+ 2026-06-12 (PR #78) and is watermark-incremental with a bounded `lookbackDays`, with no backfill
195
+ ever run — so any meeting older than ~2026-06-12 is absent from `Team.TranscriptExports` /
196
+ `Team.TranscriptProcessing` / `togaiq` despite having been recorded. Recovery is via Microsoft
197
+ Graph or a manual `.docx`/`.vtt` export. Found while recovering a 2026-05-01 meeting. No code
198
+ changed. (mhammontree)
184
199
  - 2026-07-09 — **`Export` re-architected to GRAPH-DIRECT** (merged into the ingestion pipeline).
185
200
  The `toga-private` staging bucket is dropped: `Export` is now a thin poller that records a
186
201
  discovery row and enqueues one `Process` job per transcript; `Process` downloads the VTT
@@ -5,13 +5,14 @@ project: API
5
5
  client: aig
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-07-29
8
+ updated: 2026-08-03
9
9
  owners: ["mhammontree"]
10
10
  files:
11
11
  - _underscore/Model/Aig/Entitlement.php
12
12
  - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
13
13
  related:
14
14
  - clients/aig/features/contract-reconciliation.md
15
+ - 1.0/apps/library/features/toga2-api-client-and-bridge.md
15
16
  - 2.0/apps/api2/features/request-logging.md
16
17
  - 2.0/apps/api2/features/nested-relationship-writes.md
17
18
  - 2.0/apps/api2/architecture.md
@@ -121,6 +122,23 @@ Loading the catalog fixed intake, and because the feed re-sends outstanding cont
121
122
  **backlog of dropped contracts self-healed on the next scheduled run** — no manual back-fill was
122
123
  needed (see [contract reconciliation](contract-reconciliation.md)).
123
124
 
125
+ ## Business Unit (`UserDefined2`) — answered and DROPPED (nothing to build)
126
+
127
+ AIG (Alissa, 2026-05-04) confirmed the **Business Unit / Value** field needs **nothing returned to
128
+ AIG**: they already store it from the sale, and it is used only for **sales invoicing**, which is a
129
+ **different process from claims invoicing** (the one we participate in). We replied asking them to
130
+ **omit it from the API call entirely**. **No invoice-file template was ever needed.**
131
+
132
+ Verified there is no dead custom field to clean up: `c_businessUnit` exists **nowhere** in
133
+ `_underscore`, `api2`, or `dbchanges2` (the only `BusinessUnits` hits in the tree are unrelated, in
134
+ the `forecast` repo). Corroborated by AIG's field-mapping spreadsheet, where the `UserDefined2`
135
+ row's target column is **empty** while every other row is mapped.
136
+
137
+ **This — not the extra emails — was the item actually blocking Paulina from sending the mapping
138
+ document.** The two were conflated previously. See
139
+ [the library bridge doc](../../../1.0/apps/library/features/toga2-api-client-and-bridge.md) for the
140
+ resolved email-payload shape.
141
+
124
142
  ## Key files / entry points
125
143
 
126
144
  - **`_underscore/Model/Aig/Entitlement.php`** — `prePost(&$api, &$payload)` is the PRE-POST
@@ -223,6 +241,19 @@ this interceptor or use this dual-purpose Items pattern.
223
241
 
224
242
  ## Gotchas / known issues
225
243
 
244
+ - **`postPost`'s SINGLE-recipient registration email is correct by design — do not "fix" it.**
245
+ It sends the Staples Protection Plan registration email to
246
+ `addTo($payload->contact->primaryContactEmailAddress->emailAddress)` only. TRUE-79401's multi-email
247
+ work is for **verification/lookup only** (a support agent finding a caller by any of their
248
+ addresses); the team **explicitly rejected** CC/BCC-ing every address onto TogaDesk tickets, so
249
+ multi-recipient notification is out of scope. Settled in the 2026-05-01 meeting: "it's just for
250
+ visual, this is for verification, it's a string."
251
+ - **A wrong-shaped nested collection on intake returns HTTP 201 and silently drops the data.**
252
+ Both a flat/unknown key (a literal `UserDefined3`) and an array of bare strings
253
+ (`"contactEmailAddresses": ["a@b.com"]` instead of `[{"emailAddress": "a@b.com"}]`) are accepted
254
+ with **no error anywhere**. When verifying AIG's multi-email feed, assert **actual
255
+ `Client_Aig.ContactEmailAddresses` row counts** — never the HTTP status. See
256
+ [api2 nested-relationship writes](../../../2.0/apps/api2/features/nested-relationship-writes.md).
226
257
  - **A sale-item code missing from `Client_Aig.Items` currently produces an HTTP 500, not a clean
227
258
  "Missing AIG item ID."** In prod (TRUE-79534) a valid-but-unknown code made `$itemId` empty and
228
259
  the downstream fulfillment query threw MySQL ERROR 1064 — so the request 500'd and the contract
@@ -275,6 +306,15 @@ this interceptor or use this dual-purpose Items pattern.
275
306
 
276
307
  ## Change history
277
308
 
309
+ - 2026-08-03 — Review-only session (no code changed). **Business Unit (`UserDefined2`) is answered
310
+ and dropped**: AIG (2026-05-04) needs nothing returned — it is theirs, from the sale, for **sales**
311
+ invoicing (not claims invoicing); we asked them to omit it from the API call and **no invoice-file
312
+ template was needed**. Verified `c_businessUnit` exists nowhere in `_underscore`/`api2`/`dbchanges2`,
313
+ so there is no dead field to clean up; the mapping spreadsheet's `UserDefined2` target column is
314
+ empty. This — not the extra emails — was what blocked Paulina from sending the mapping document.
315
+ Also recorded that `postPost`'s **single-recipient** registration email is **correct by design**
316
+ (TRUE-79401 is verification/lookup only; multi-recipient CC/BCC was explicitly rejected), and the
317
+ 201-with-dropped-data trap when verifying the nested `contactEmailAddresses` feed. (mhammontree)
278
318
  - 2026-07-29 — TRUE-80179 / TRUE-79534 (**RESOLVED + correction**): the `STS_001` intake gap is
279
319
  **closed and it self-healed.** After the catalog load + collation fix shipped, AIG's next
280
320
  scheduled batch delivered all **156** outstanding `STS_001` contracts and every one succeeded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.493",
3
+ "version": "1.0.494",
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",