toga-ai 1.0.492 → 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.
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +117 -8
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +26 -1
- package/knowledge/2.0/apps/_underscore/features/carrier-shipping-labels.md +102 -1
- package/knowledge/2.0/apps/api2/INDEX.md +3 -2
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +86 -0
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +36 -1
- package/knowledge/2.0/apps/api2/features/record-scripts.md +42 -2
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md +153 -0
- package/knowledge/2.0/apps/toga2-supply/features/fulfill-and-ship.md +79 -12
- package/knowledge/2.0/apps/worker2/features/teams-transcript-export.md +17 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/aig/features/entitlement-intake.md +41 -1
- package/package.json +1 -1
|
@@ -6,13 +6,15 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
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 (
|
|
180
|
-
`Client_Aig.ContactEmailAddresses` rows are one-per-contact
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -214,6 +214,23 @@ read fresh per request) — no Apache restart needed. Concrete case: `TrackingNu
|
|
|
214
214
|
threw EO-1 purely because `_underscore` was on a branch whose generated `TrackingNumber.php` lacked
|
|
215
215
|
the field.
|
|
216
216
|
|
|
217
|
+
### ⚠ Verify the mirror sibling's grants EXIST in the target DB — or the clone inserts nothing
|
|
218
|
+
|
|
219
|
+
A "clone a sibling's grants" migration reads the **target database's own** `AclFieldPermissions`
|
|
220
|
+
rows, so on a client where the sibling itself has no grants the `INSERT … SELECT` matches zero rows
|
|
221
|
+
and **succeeds with 0 inserted and no error**. Always confirm the sibling has rows in that DB first.
|
|
222
|
+
|
|
223
|
+
**Reference siblings that are safe to mirror** (verified present + complete per client):
|
|
224
|
+
`shippingMethodId` (338), `measures.slug` (723), `containsBattery` (962), `shipToAddressId` (991).
|
|
225
|
+
**Do NOT mirror `needsReturnLabel`** — its grants are absent in some clients.
|
|
226
|
+
|
|
227
|
+
Reconfirmed 2026-08-03 (TRUE-80494): `measures.measureType` had **zero** grant rows in dev-sandbox
|
|
228
|
+
`Client_Growrk`, so it was omitted from `GET /v2/measures`; the frontend's strict
|
|
229
|
+
`m.measureType === "LENGTH"` split then produced empty arrays and **both unit dropdowns rendered
|
|
230
|
+
empty with no error anywhere**. The same audit found `lengthMeasureId`/`weightMeasureId` granted to
|
|
231
|
+
**role 3 only, not role 1** — grant fan-out is per-client and must be verified per client, not
|
|
232
|
+
assumed from one.
|
|
233
|
+
|
|
217
234
|
## Checklist (so the chain is never half-built)
|
|
218
235
|
|
|
219
236
|
- [ ] `AclRecordPermissions` row for the role × record with the right CRUD flags.
|
|
@@ -227,6 +244,14 @@ the field.
|
|
|
227
244
|
|
|
228
245
|
## Change history
|
|
229
246
|
|
|
247
|
+
- **2026-08-03** — TRUE-80494: reconfirmed the **silent-omission** read rule in a second environment
|
|
248
|
+
(`measures.measureType` ungranted in dev-sandbox `Client_Growrk` → absent from `GET /v2/measures` →
|
|
249
|
+
empty unit dropdowns, no error) and added the **verify-the-mirror-sibling** rule: a clone migration
|
|
250
|
+
reads the target DB's own rows, so a sibling with no grants there inserts **zero rows without
|
|
251
|
+
erroring**. Listed safe reference siblings (`shippingMethodId` 338, `measures.slug` 723,
|
|
252
|
+
`containsBattery` 962, `shipToAddressId` 991) and flagged `needsReturnLabel` as unsafe to mirror.
|
|
253
|
+
Also noted `lengthMeasureId`/`weightMeasureId` were granted to role 3 but not role 1 — verify the
|
|
254
|
+
role set per client. (mhammontree)
|
|
230
255
|
- **2026-07-22** — TRUE-79191: documented that the V2 GET **OUTPUT** field list is itself built from
|
|
231
256
|
`AclFieldPermissions` (`getAclFieldPermissions` + the output `foreach` loops in
|
|
232
257
|
`api2/.../V2.php`), so an **ungranted field is SILENTLY OMITTED** from the response (200, no
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ItemFulfillment.php
|
|
@@ -27,6 +27,8 @@ related:
|
|
|
27
27
|
- ../architecture.md
|
|
28
28
|
- recursive-item-fulfillments.md
|
|
29
29
|
- ../../toga2-supply/features/fulfill-and-ship.md
|
|
30
|
+
- ../../api2/features/record-scripts.md
|
|
31
|
+
- ../../dbchanges2/workflows/nonprod-metadata-drift-repair.md
|
|
30
32
|
---
|
|
31
33
|
|
|
32
34
|
## Summary
|
|
@@ -168,6 +170,65 @@ outbound label:
|
|
|
168
170
|
falls back to Signature Required (`2`).** None Specified is **omitted** (UPS default, no
|
|
169
171
|
confirmation).
|
|
170
172
|
|
|
173
|
+
## Reference numbers printed on the label (Reference 1 / Reference 2 — implemented 2026-07-31)
|
|
174
|
+
|
|
175
|
+
Two user-entered reference values now reach the carrier and **print on the shipping label**. They
|
|
176
|
+
are **transient**: not sourced from NetSuite, not persisted anywhere, no DB columns, no
|
|
177
|
+
`RecordFields`, **zero `dbchanges2` work**. They ride as **query-string params on the existing
|
|
178
|
+
carrier RecordScript calls** — see
|
|
179
|
+
[Record Scripts → named-argument binding](../../api2/features/record-scripts.md), including the
|
|
180
|
+
**mandatory backend-before-frontend deploy order** that pattern imposes.
|
|
181
|
+
|
|
182
|
+
- `Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php` gained **`$referenceString2`**
|
|
183
|
+
(alongside the existing `$referenceString`).
|
|
184
|
+
- `upsShipmentApi` / `fedexShipmentApi` (`Model/Client/ItemFulfillment.php`) gained optional
|
|
185
|
+
**`$reference1` / `$reference2`** params, and the two **OUTBOUND** shipment builders now use them
|
|
186
|
+
**instead of `SalesOrders.customerPurchaseOrder`**.
|
|
187
|
+
- Semantics (decided; the field-config placeholders were authoritative over the initial verbal
|
|
188
|
+
description, and confirmed in testing when `281144` = `SalesOrders.number` was entered into
|
|
189
|
+
Reference 1): **Reference 1 = NetSuite SALES order #, Reference 2 = NetSuite PURCHASE order #.**
|
|
190
|
+
- Length is capped at **35 characters** (carrier limit), enforced in the form config
|
|
191
|
+
(`characterLimit: 35`).
|
|
192
|
+
|
|
193
|
+
### ⚠ CRITICAL — UPS `TransactionReference.CustomerContext` NEVER prints on a label
|
|
194
|
+
|
|
195
|
+
The code previously sent the reference into
|
|
196
|
+
`ShipmentRequest.Request.TransactionReference.CustomerContext`. That field is UPS's **request/response
|
|
197
|
+
correlation echo**: UPS accepts it, returns **HTTP 200**, and **prints nothing**. This is why a
|
|
198
|
+
reference never appeared on a GroWrk label — with no error to point at it.
|
|
199
|
+
|
|
200
|
+
> **A carrier accepting a value with a 200 is not evidence that it printed. Only the rendered label
|
|
201
|
+
> is.** Verify every label-content change against the actual label image.
|
|
202
|
+
|
|
203
|
+
**Real label references require PACKAGE-LEVEL `ReferenceNumber` entries** (`Code` + `Value`):
|
|
204
|
+
|
|
205
|
+
- **Shipment-level `ReferenceNumber` is REJECTED by UPS on US/PR → US/PR domestic shipments** — which
|
|
206
|
+
is most GroWrk volume — so **package level is mandatory**, not merely preferred.
|
|
207
|
+
- **UPS prints the code's MEANING next to the value**, so the code choice is user-visible. Verified
|
|
208
|
+
code table: **`PO`** = Purchase Order Number, **`TN`** = Transaction Reference Number, **`IK`** =
|
|
209
|
+
Invoice Number, **`ON`** = Dealer Order Number, **`RZ`** = Return Authorization, plus `AJ`, `AT`,
|
|
210
|
+
`BM`, `9V`, `DP`, `3Q`, `MK`, `MJ`, `PM`, `PC`, `RQ`, `SA`, `SE`, `ST`, `EI`, `TJ`, `SY`.
|
|
211
|
+
**UPS has NO sales-order code.**
|
|
212
|
+
- **Limits:** max **2** reference numbers per shipment, **35 chars** each; only **one** can be
|
|
213
|
+
barcoded per shipment (barcoding additionally requires ≤14 alphanumeric / ≤24 numeric and **no
|
|
214
|
+
spaces**).
|
|
215
|
+
|
|
216
|
+
**FedEx** (`requestedPackageLineItems[].customerReferences[]`) **does** print. Enum values:
|
|
217
|
+
`CUSTOMER_REFERENCE`, `P_O_NUMBER`, `INVOICE_NUMBER`, `DEPARTMENT_NUMBER`, `BILL_OF_LADING`,
|
|
218
|
+
`RMA_ASSOCIATION`, `STORE_NUMBER`, `ELECTRONIC_PRODUCT_CODE`, `SHIPMENT_INTEGRITY`,
|
|
219
|
+
`INTRACOUNTRY_REGULATORY_REFERENCE`. Max **3 per package**, and FedEx **rejects two entries of the
|
|
220
|
+
same type**.
|
|
221
|
+
|
|
222
|
+
**Chosen mapping (kept identical across carriers so a label reads the same either way):**
|
|
223
|
+
|
|
224
|
+
| Field | UPS code | FedEx type |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| Reference 1 (NetSuite Sales Order #) | `TN` (Transaction Reference Number) | `CUSTOMER_REFERENCE` |
|
|
227
|
+
| Reference 2 (NetSuite Purchase Order #) | `PO` (Purchase Order Number) | `P_O_NUMBER` |
|
|
228
|
+
|
|
229
|
+
`TN` was chosen over `ON` (Dealer Order Number) precisely **because UPS prints the code's meaning** —
|
|
230
|
+
"Dealer Order Number" would be misleading next to a sales-order number.
|
|
231
|
+
|
|
171
232
|
## `FIELD_STORAGE` mechanics (reference)
|
|
172
233
|
|
|
173
234
|
`Model.php` handles storage fields separately from regular columns (write loop ~line
|
|
@@ -204,6 +265,30 @@ still discards the unsaved in-memory value.
|
|
|
204
265
|
|
|
205
266
|
## Gotchas / known issues
|
|
206
267
|
|
|
268
|
+
- **⚠ `[ups] label_format` must be set in EVERY environment's api2 config.** Without it **production
|
|
269
|
+
UPS returns ZPL**, while the Fulfill & Ship return-label path expects a **raster PNG** for FPDF.
|
|
270
|
+
`beta.ini` already carried `label_format = "PNG"` and `production.ini` did **not** — which is
|
|
271
|
+
exactly why the flow worked on beta and failed in production (api2 `851c227` added it). When a
|
|
272
|
+
label-format bug reproduces in only one environment, diff the `[ups]` config block first.
|
|
273
|
+
- **⚠ NO carrier can be tested on ANY non-production environment.** Beta (`api.beta.togahub.com`,
|
|
274
|
+
which reads the **dev-sandbox** cluster) runs `debug_mode = 1` → UPS `wwwcie.ups.com` (CIE) and
|
|
275
|
+
FedEx `apis-sandbox.fedex.com`; production uses `apis.fedex.com`. But the carrier **account
|
|
276
|
+
numbers** in non-prod data are **copied from production**, and neither test environment accepts a
|
|
277
|
+
production account: UPS → **"Missing or invalid shipper number" (120100)**, FedEx → **HTTP 400**.
|
|
278
|
+
**Label generation is therefore verifiable only in production** until CIE/sandbox-valid test
|
|
279
|
+
accounts are seeded into non-prod `ShippingCarrierAccountNumbers` (the stated remedy — not done).
|
|
280
|
+
Also: **beta and production point at the SAME NetSuite account (1095849)**, so a beta fulfillment
|
|
281
|
+
writes to production NetSuite. See the
|
|
282
|
+
[non-prod metadata drift workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
283
|
+
- ~~FedEx shipment failures collapse to a generic "temporarily unavailable" message.~~ **Fixed
|
|
284
|
+
2026-07-31.** `Fedex.php` did
|
|
285
|
+
`$errorData?->errors[0]?->message ?? 'Shipment service is temporarily unavailable (HTTP n)'`, so any
|
|
286
|
+
unexpected response shape lost the real cause — and because the ship call runs with
|
|
287
|
+
`setLogging(false)`, **that message is the only diagnostic there is**. A new private static
|
|
288
|
+
**`describeShipmentError()`** (with `MAX_ERROR_BODY_LENGTH = 300`) now reports **every** `errors[]`
|
|
289
|
+
entry as `"CODE: message"`, echoes a **truncated raw body** when the shape is unrecognized, and
|
|
290
|
+
distinguishes an **empty** body. General rule for this codebase: a `??` fallback string on an error
|
|
291
|
+
path destroys the only evidence — report the unexpected shape instead of masking it.
|
|
207
292
|
- UPS **test** endpoint (`wwwcie.ups.com`, debug mode) returns a constant tracking
|
|
208
293
|
number `1ZXXXXXXXXXXXXXXXX` + SAMPLE labels → `TrackingNumbers.number` UNIQUE
|
|
209
294
|
collision and File Cabinet duplicate-filename failures on repeat tests (see the
|
|
@@ -282,6 +367,22 @@ still discards the unsaved in-memory value.
|
|
|
282
367
|
bill) — expect a hard error rather than a silently-wrong bill in those cases.
|
|
283
368
|
|
|
284
369
|
## Change history
|
|
370
|
+
- 2026-08-03 — TRUE-80494: **wired Reference 1 / Reference 2 through to the carrier so they PRINT on
|
|
371
|
+
the label** — `ShipmentRequest` gained `$referenceString2`, `upsShipmentApi`/`fedexShipmentApi`
|
|
372
|
+
gained optional `$reference1`/`$reference2` (transient query-string params, **zero dbchanges2
|
|
373
|
+
work**), and the outbound builders now use them instead of `SalesOrders.customerPurchaseOrder`.
|
|
374
|
+
**Critical carrier discovery:** UPS `TransactionReference.CustomerContext` is a correlation echo
|
|
375
|
+
that **never prints** (200, no error) — label references need **package-level `ReferenceNumber`**
|
|
376
|
+
(`Code`+`Value`), since shipment-level is **rejected** on US/PR→US/PR domestic; UPS prints the
|
|
377
|
+
code's *meaning*, max 2 refs × 35 chars, one barcodable; FedEx
|
|
378
|
+
`requestedPackageLineItems[].customerReferences[]` does print (max 3/package, no duplicate types).
|
|
379
|
+
Mapping: Ref1 (NS Sales Order) → UPS `TN` / FedEx `CUSTOMER_REFERENCE`; Ref2 (NS Purchase Order) →
|
|
380
|
+
UPS `PO` / FedEx `P_O_NUMBER`. Fixed **FedEx error swallowing** (`describeShipmentError()` reports
|
|
381
|
+
every `errors[]` entry + a truncated raw body; the ship call logs nothing, so that string was the
|
|
382
|
+
only diagnostic). Recorded that **no carrier is testable outside production** (non-prod holds
|
|
383
|
+
production account numbers → UPS 120100 / FedEx 400; beta shares prod NetSuite 1095849) and that
|
|
384
|
+
**`[ups] label_format = "PNG"` was missing from `production.ini`** (prod returned ZPL where FPDF
|
|
385
|
+
needs PNG — beta had it, which is why only prod failed). (mhammontree)
|
|
285
386
|
- 2026-07-17 — TRUE-79191: applied **signature type** to outbound labels for the first time (carrier
|
|
286
387
|
libs previously ignored it). Added a generic `$signatureType` on `ShipmentRequest`;
|
|
287
388
|
`fedexShipmentApi`/`upsShipmentApi` now SELECT + pass `TrackingNumbers.signatureType`; FedEx maps to
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
|
+
| [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ItemFulfillment.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql |
|
|
6
7
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, _underscore/Model/Core/Record.php, worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
7
8
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
8
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 |
|
|
9
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 |
|
|
10
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 |
|
|
11
|
-
| [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
|
-
| [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/Query.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
|
+
| [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 |
|
|
13
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 |
|
|
14
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 |
|
|
15
16
|
| [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: API Payload Interceptors (metadata-registered prePost/postPut model hooks)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Model/Client/ItemFulfillment.php
|
|
14
|
+
- dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
|
|
15
|
+
related:
|
|
16
|
+
- ./record-scripts.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
- ../../_underscore/features/acl-permission-chain.md
|
|
19
|
+
- ../../_underscore/features/recursive-item-fulfillments.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the
|
|
25
|
+
model. They are dispatched by the api2 **/v2 engine**, and **only if a metadata row exists** in
|
|
26
|
+
`Core.ApiPayloadInterceptors` (or the Client-level equivalent) for that record + processing phase +
|
|
27
|
+
HTTP method. **A missing registration row means the hook silently never runs** — the code is
|
|
28
|
+
present, correct, deployed, and dead. This is the single most misleading failure mode in the
|
|
29
|
+
interceptor mechanism: the symptom looks exactly like a code bug.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
1. On a write, `_Component_Api_V2` (`api2/Component/Api/V2/V2.php`) looks up
|
|
34
|
+
`ApiPayloadInterceptors` by **`(recordId, prePostProcessing, httpMethod)`** — e.g.
|
|
35
|
+
`(28, 'PRE', 'POST')`.
|
|
36
|
+
2. The PHP method name is **derived from those two enum values**, not stored:
|
|
37
|
+
`strtolower(prePostProcessing) . ucfirst(strtolower(httpMethod))`. So:
|
|
38
|
+
- `PRE` + `POST` → **`prePost`**
|
|
39
|
+
- `POST` + `POST` → **`postPost`**
|
|
40
|
+
- `POST` + `PUT` → **`postPut`**
|
|
41
|
+
3. If a row matches, the engine calls that static/instance method on the resolved model (the
|
|
42
|
+
**client subclass** when one exists — `_Model_Growrk_ItemFulfillment` before
|
|
43
|
+
`_Model_Client_ItemFulfillment`). If no row matches, nothing is called and there is **no error,
|
|
44
|
+
no message, and no log line**.
|
|
45
|
+
|
|
46
|
+
Because the registration is a **data** row, it is per-environment: the code ships with a deploy, the
|
|
47
|
+
hook only becomes live when the migration lands.
|
|
48
|
+
|
|
49
|
+
## Worked example — the EV-10 that was not a code bug
|
|
50
|
+
|
|
51
|
+
`_Model_Client_ItemFulfillment::prePost()` defaults `itemFulfillmentStageId` to the shipped stage on
|
|
52
|
+
a `POST /v2/item-fulfillments`. In **dev-sandbox** the `(recordId 28, PRE, POST)` row was missing, so
|
|
53
|
+
`prePost` never ran, `itemFulfillmentStageId` stayed `NULL`, and the write failed with **`EV-10`
|
|
54
|
+
"Column 'itemFulfillmentStageId' cannot be null"** — while the identical code worked in production.
|
|
55
|
+
Fix was purely metadata: apply the existing
|
|
56
|
+
`dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql`.
|
|
57
|
+
|
|
58
|
+
**Diagnostic that works:** do a **full-table compare of `ApiPayloadInterceptors` between production
|
|
59
|
+
and the failing environment**. It is a small table, and the drift is usually exactly one row.
|
|
60
|
+
|
|
61
|
+
## Gotchas / known issues
|
|
62
|
+
|
|
63
|
+
- **⚠ A hook that "isn't running" is a missing registration row before it is a code bug.** Check
|
|
64
|
+
`ApiPayloadInterceptors` for `(recordId, phase, method)` **first** — never start by re-reading the
|
|
65
|
+
PHP.
|
|
66
|
+
- **The method name is derived, so a typo'd enum silently misses.** A row with
|
|
67
|
+
`prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine
|
|
68
|
+
will simply find no method and move on.
|
|
69
|
+
- **Registration is per-environment.** Like `RecordFields` / `RecordScripts` / ACL rows, this is
|
|
70
|
+
metadata — "works in prod, not in beta/dev-sandbox" is the signature of drift, not of a bad
|
|
71
|
+
deploy. See the
|
|
72
|
+
[non-prod metadata drift repair workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
73
|
+
- **Never emit output from an interceptor.** A stray `echo`/`print_r` corrupts the `/v2`
|
|
74
|
+
`{isSuccess,…,data}` envelope (this really happened in `ItemFulfillment.php` — see the
|
|
75
|
+
carrier-shipping-labels doc).
|
|
76
|
+
- **Interceptors are a client-behavior extension point.** Per api2's architecture doc, client
|
|
77
|
+
specifics belong in `_Model_<Slug>_X` overrides + interceptor hooks, **not** in `V2.php`.
|
|
78
|
+
|
|
79
|
+
## Change history
|
|
80
|
+
|
|
81
|
+
- 2026-08-03 — TRUE-80494: documented the mechanism after an `EV-10`
|
|
82
|
+
(`itemFulfillmentStageId cannot be null`) in dev-sandbox turned out to be a **missing
|
|
83
|
+
`ApiPayloadInterceptors` `(28, PRE, POST)` row**, not a code fault — `prePost` is dispatched only
|
|
84
|
+
from that metadata, and the method name is derived as
|
|
85
|
+
`strtolower(phase) . ucfirst(strtolower(method))`. Added the prod-vs-env full-table compare as the
|
|
86
|
+
diagnostic. (mhammontree)
|
|
@@ -6,10 +6,11 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
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,11 +6,12 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["kyalamarthi"]
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["kyalamarthi", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Team/Sprint.php
|
|
14
|
+
- _underscore/Model/Client/ItemFulfillment.php
|
|
14
15
|
- _underscore/Query.php
|
|
15
16
|
related:
|
|
16
17
|
- ./sprint-dashboard-api.md
|
|
@@ -67,6 +68,37 @@ Rules the engine enforces (see `getRecordScriptPhpMethod()` + the dispatch in
|
|
|
67
68
|
method itself still returns the raw object/array — do not echo, and never build the
|
|
68
69
|
success/error envelope yourself.
|
|
69
70
|
- Invocation is effectively `$model::$phpMethod(...$args)`.
|
|
71
|
+
|
|
72
|
+
### ⚠ Argument binding is PHP NAMED-ARGUMENT unpacking of the QUERY STRING — with two consequences
|
|
73
|
+
|
|
74
|
+
Verified in `api2/Component/Api/V2/V2.php` (~L4202 and ~L4468): the engine does
|
|
75
|
+
`parse_str($_SERVER['QUERY_STRING'], $args)`, **merges the JSON request body** into the same array
|
|
76
|
+
(**the query string wins** on a key collision — body support exists so a large value can avoid an
|
|
77
|
+
HTTP 414; see [scripted-api POST-body args](./scripted-api-post-body-args.md)), sets
|
|
78
|
+
`$args['api'] = $this`, and then calls
|
|
79
|
+
`$candidateClientModelName::$recordScriptPhpMethod(...$args)`. Because `$args` is a **string-keyed**
|
|
80
|
+
array, that spread is **PHP named-argument unpacking**: each query-string key must match a
|
|
81
|
+
**declared parameter name** on the method.
|
|
82
|
+
|
|
83
|
+
1. **Free extension point — a transient value needs NO schema, RecordField, or ACL work.**
|
|
84
|
+
`Core.RecordScripts` has columns `recordId`, `method`, `route`, `phpMethod` and **no parameter
|
|
85
|
+
signature at all**, so adding a parameter to a script is a **pure code change** — no migration.
|
|
86
|
+
This is the sanctioned way to pass a per-request, **unpersisted** value through to an external
|
|
87
|
+
call (first use: TRUE-80494 passed the user-entered Reference 1 / Reference 2 through
|
|
88
|
+
`upsShipmentApi` / `fedexShipmentApi` to the carrier label — the values are never stored).
|
|
89
|
+
Declare the new params **optional** (`?string $reference1 = null`).
|
|
90
|
+
2. **⚠ An unmatched query key THROWS — so backend MUST deploy before frontend.** A key with no
|
|
91
|
+
matching declared parameter raises `Unknown named parameter $x` and the call fails outright.
|
|
92
|
+
Two real hazards: (a) if several **sibling** scripts receive the same new query params, every
|
|
93
|
+
sibling must declare them — adding the param to only `upsShipmentApi` breaks
|
|
94
|
+
`fedexShipmentApi`; (b) a frontend deployed **ahead** of the backend sends a param the deployed
|
|
95
|
+
method doesn't declare and breaks **every** call to that script. Any change that adds a script
|
|
96
|
+
parameter therefore carries a **mandatory deploy order: `_underscore`/api2 first, frontend
|
|
97
|
+
second.**
|
|
98
|
+
|
|
99
|
+
> **`_underscore/Route.php` is NOT involved in this.** `Route.php` binds **path segments**
|
|
100
|
+
> (`explode('/', urldecode($route))`), not the query string. Don't go looking there when debugging
|
|
101
|
+
> record-script arguments.
|
|
70
102
|
- **No current-sprint (or any per-request context) middleware.** Unlike a hand-built app, the
|
|
71
103
|
`/v2` engine has no middleware that pre-resolves a "current" entity for a script. Each Record
|
|
72
104
|
Script must resolve its own context (e.g. the current sprint) inside the method.
|
|
@@ -225,6 +257,14 @@ an `AclRecordScripts` grant in each client DB.
|
|
|
225
257
|
|
|
226
258
|
## Change history
|
|
227
259
|
|
|
260
|
+
- 2026-08-03 — TRUE-80494: documented that argument binding is **PHP named-argument unpacking of the
|
|
261
|
+
query string** (`parse_str` + merged JSON body, query wins, `$args['api']` last, then
|
|
262
|
+
`$model::$phpMethod(...$args)`; `V2.php` ~4202/~4468) and its two consequences: (a) a script
|
|
263
|
+
parameter is a **pure code change** — `Core.RecordScripts` stores no signature, so a transient,
|
|
264
|
+
unpersisted per-request value can be threaded to an external call with **no migration/ACL work**
|
|
265
|
+
(first use: Reference 1/2 → carrier label); (b) an **unmatched query key throws**, so every
|
|
266
|
+
sibling script must declare shared new params and the **backend must deploy before the frontend**.
|
|
267
|
+
Also noted `_underscore/Route.php` binds path segments, not query args. (mhammontree)
|
|
228
268
|
- 2026-07-31 — TRUE-80487: added the **reusable client-side `CustomRecordScript` pattern** for a
|
|
229
269
|
scripted API declared on a client model — `CustomRecordScripts` + `AclCustomRecordScripts` are
|
|
230
270
|
**both** `DB_CLIENT`, so registration and grant land in **one** database and the grant resolves
|
|
@@ -5,3 +5,4 @@
|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql |
|
|
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
|
+
| [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/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini |
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
|
|
13
|
+
- dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql
|
|
14
|
+
- dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql
|
|
15
|
+
- api2/Config/beta.ini
|
|
16
|
+
related:
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
19
|
+
- ../../api2/workflows/environment-configuration-and-provisioning.md
|
|
20
|
+
- ../../_underscore/features/acl-permission-chain.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`,
|
|
26
|
+
`Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` rows. Non-prod
|
|
27
|
+
environments receive those rows by someone remembering to run a migration there, so they **drift**.
|
|
28
|
+
|
|
29
|
+
> **Default assumption: when a feature works in production but not in beta / dev-sandbox, the
|
|
30
|
+
> environment's metadata is behind — not the code.** Verify drift before touching PHP.
|
|
31
|
+
|
|
32
|
+
This workflow is the repair procedure, written from bringing dev-sandbox up to date so beta could
|
|
33
|
+
exercise TOGa Supply Fulfill & Ship (TRUE-80494).
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
1. **Identify which cluster the environment actually reads.** `api.beta.togahub.com` reads the
|
|
38
|
+
**dev-sandbox** cluster (`api2/Config/beta.ini` `[database] hostname =
|
|
39
|
+
dev.sandbox.database.togahub.com`) — "beta" is not its own database. Fix the right DB.
|
|
40
|
+
2. **Full-table compare the small metadata tables, prod vs. target.** `ApiPayloadInterceptors` in
|
|
41
|
+
particular is small enough to diff wholesale, and the drift is typically **exactly one row**
|
|
42
|
+
(dev-sandbox was missing `(recordId 28, PRE, POST)`, so `prePost` never defaulted
|
|
43
|
+
`itemFulfillmentStageId` → `EV-10 Column 'itemFulfillmentStageId' cannot be null`). Apply the
|
|
44
|
+
existing migration rather than writing a new one.
|
|
45
|
+
3. **For a field that reads back empty/blank, check `AclFieldPermissions` before the selector.** An
|
|
46
|
+
ungranted field is **silently omitted** from the GET body (HTTP 200, no message). `Measures.measureType`
|
|
47
|
+
had zero grant rows in dev-sandbox `Client_Growrk` → omitted from `GET /v2/measures` → the frontend's
|
|
48
|
+
`measureType === "LENGTH"` split produced empty arrays → both unit dropdowns rendered empty with no
|
|
49
|
+
error anywhere. See the [ACL permission chain](../../_underscore/features/acl-permission-chain.md).
|
|
50
|
+
4. **Before running a grant migration that clones a sibling, verify the sibling's own grants exist in
|
|
51
|
+
that DB.** The clone reads the target DB's own rows, so a sibling with no grants there inserts
|
|
52
|
+
**zero rows and does not error**. Safe reference siblings (present + complete per client):
|
|
53
|
+
`shippingMethodId` (338), `measures.slug` (723), `containsBattery` (962), `shipToAddressId` (991).
|
|
54
|
+
**Do NOT mirror `needsReturnLabel`** — its grants are absent in some clients.
|
|
55
|
+
5. **Register any missing Core `RecordField` + client column.** In dev-sandbox
|
|
56
|
+
`tracking-numbers.signatureType` was unregistered; it was registered, the
|
|
57
|
+
`TrackingNumbers.signatureType VARCHAR(255)` utf8mb4 column added, and `containsBattery` (962)
|
|
58
|
+
mirrored for its grants. **⚠ `Core.RecordFields` ids are team-maintained constants — ask for the
|
|
59
|
+
next `id` per the [dbchanges2 architecture rule](../architecture.md) (2026-07-31, jcardinal).**
|
|
60
|
+
What was actually done here diverged from that rule; see *Open question for jcardinal* below
|
|
61
|
+
before copying it.
|
|
62
|
+
6. **Re-test the feature end-to-end in that environment**, then record what is still missing so the
|
|
63
|
+
next person doesn't rediscover it.
|
|
64
|
+
|
|
65
|
+
## Environment facts worth knowing before you test here
|
|
66
|
+
|
|
67
|
+
- **NO carrier can be tested on ANY non-production environment.** Beta runs
|
|
68
|
+
`[_underscore] debug_mode = 1`, which routes UPS to `wwwcie.ups.com` (CIE/test), and its
|
|
69
|
+
`[fedex] oauth` points at `https://apis-sandbox.fedex.com` (production uses `https://apis.fedex.com`).
|
|
70
|
+
But the carrier **account numbers** in non-prod data are copied from production, and neither test
|
|
71
|
+
environment accepts a production account — UPS returns **"Missing or invalid shipper number"
|
|
72
|
+
(120100)** and FedEx returns **HTTP 400**. **Label generation can only be verified in production**
|
|
73
|
+
until CIE/sandbox-valid test accounts are seeded into non-prod
|
|
74
|
+
`ShippingCarrierAccountNumbers`. (Remedy = obtain those test accounts; documented as the fix, not done.)
|
|
75
|
+
- **⚠ Beta and production point at the SAME NetSuite account (1095849).** Any beta fulfillment
|
|
76
|
+
writes to **production** NetSuite. Treat a beta fulfill run as a production side effect.
|
|
77
|
+
- **In dev-sandbox, `Core` and `Client_*` share one cluster**, so a `Core.`-qualified reference in a
|
|
78
|
+
`Client/` migration resolves there and looks fine. **Production splits them** (prod-core /
|
|
79
|
+
prod-client) — a clean dev-sandbox run is never proof a migration is cluster-safe (see the
|
|
80
|
+
architecture doc's hard isolation rule).
|
|
81
|
+
|
|
82
|
+
## Open question for jcardinal — observed `Core.RecordFields` id divergence
|
|
83
|
+
|
|
84
|
+
> **This section records evidence only. It does NOT amend
|
|
85
|
+
> [`2.0/apps/dbchanges2/architecture.md`](../architecture.md).** The 2026-07-31 rule there
|
|
86
|
+
> (jcardinal, commit `0af489d`, enforced by the `dbchanges2-record-ids` hook) is **normative**:
|
|
87
|
+
> `Core.Records`/`Core.RecordFields` ids are team-maintained constants **so that** a client-database
|
|
88
|
+
> migration can hardcode a `recordFieldId` and never read `Core` — which is what makes the
|
|
89
|
+
> *Database isolation* hard rule workable, since production splits `Core` and `Client_*` across
|
|
90
|
+
> separate clusters. The observations below indicate the rule is being **violated in dev-sandbox**;
|
|
91
|
+
> they are not evidence the rule is wrong. Escalated to jcardinal, unresolved.
|
|
92
|
+
|
|
93
|
+
**1. Observed divergence** (verified 2026-08-03, read-only inspection):
|
|
94
|
+
|
|
95
|
+
- `items.isFulfillable` = **2434** in production, **2615** in dev-sandbox — **pre-existing**, not
|
|
96
|
+
created by this session.
|
|
97
|
+
- In dev-sandbox, ids **2480 / 2481 / 2482** belong to `vocabularies.id` / `vocabularies.uuid` /
|
|
98
|
+
`vocabularies.dtCreated`. In production the same ids are `item-fulfillments.returnAddressId`
|
|
99
|
+
(2480), `tracking-numbers.signatureType` (2481), `entitlements.serviceAddressId` (2482).
|
|
100
|
+
- Consequence: the shipped `Core/2026-07-10a - ItemFulfillmentReturnAddressIdRecordField.sql` and
|
|
101
|
+
`Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql` **cannot be applied to dev-sandbox
|
|
102
|
+
as written** — they would fail on a duplicate primary key.
|
|
103
|
+
|
|
104
|
+
**2. The hazard this creates for the new rule (the important part).** Because the rule directs that
|
|
105
|
+
`recordFieldId` **should** be hardcoded in client-database migrations, a `Client/` file hardcoding
|
|
106
|
+
**2481** (production's `tracking-numbers.signatureType`) and run against **dev-sandbox** resolves to
|
|
107
|
+
**`vocabularies.uuid`** and would **silently grant permissions on the wrong field** — a silent
|
|
108
|
+
wrong-field write, strictly **worse** than a no-op.
|
|
109
|
+
|
|
110
|
+
**3. Question for jcardinal.** Should dev-sandbox `Core.RecordFields` be **reconciled to production**
|
|
111
|
+
so the stable-id premise holds in every environment — and if so, how, given those rows are already
|
|
112
|
+
referenced by client `AclFieldPermissions`? And **until** it is reconciled, what should a `Client/`
|
|
113
|
+
migration do: hardcode the id (risking the wrong field in non-prod) or resolve by the **stable
|
|
114
|
+
`uuid`**?
|
|
115
|
+
|
|
116
|
+
**4. Workaround of record (not a precedent).** When registering `tracking-numbers.signatureType` in
|
|
117
|
+
dev-sandbox during this session, `id` was **omitted** and `AUTO_INCREMENT` assigned **2764**. This is
|
|
118
|
+
**contrary to the 2026-07-31 rule** and was done as a **one-off manual environment repair** to
|
|
119
|
+
unblock testing. It is **not** a precedent for committed migrations — a committed `Core/` migration
|
|
120
|
+
must still carry a team-assigned `id` literal.
|
|
121
|
+
|
|
122
|
+
**5. Related observation, same escalation.** `Client/2026-07-22c -
|
|
123
|
+
TrackingNumberMeasureIdsFieldPermission.sql` and `Client/2026-07-16b -
|
|
124
|
+
TrackingNumberSignatureTypeFieldPermission.sql` resolve their `recordFieldId` via
|
|
125
|
+
`SET @fieldId = (SELECT … Core.RecordFields …)` before each INSERT block, with later blocks' `SET`s
|
|
126
|
+
positioned **after** earlier INSERTs. Two factual consequences observed: (a) reading
|
|
127
|
+
`Core.RecordFields` from a `Client/` file is itself a **Database-isolation violation** per
|
|
128
|
+
[architecture.md](../architecture.md); (b) when the file is run in chunks / per client selection, the
|
|
129
|
+
later block's variables are unset, `WHERE sibling.recordFieldId = NULL` matches nothing, and **0 rows
|
|
130
|
+
insert with no error** — this silently no-op'd the `measures.measureType` read grant **twice** (first
|
|
131
|
+
in production, then again in dev-sandbox), each time presenting as "empty unit dropdowns". The
|
|
132
|
+
architecture-consistent remedy is to **hardcode the `recordFieldId` literal**, which removes the
|
|
133
|
+
`Core` lookup entirely and with it the half-execution failure mode — but that is exactly the practice
|
|
134
|
+
blocked by the id-divergence hazard in item 2 above, which is why both go to jcardinal together.
|
|
135
|
+
|
|
136
|
+
## Known remaining drift (dev-sandbox, as of 2026-08-03)
|
|
137
|
+
|
|
138
|
+
- `item-fulfillments.returnAddressId` — the **column exists** but there is **no Core RecordField**.
|
|
139
|
+
Only needed for the return-label flow.
|
|
140
|
+
|
|
141
|
+
## Change history
|
|
142
|
+
|
|
143
|
+
- 2026-08-03 — TRUE-80494: created from repairing dev-sandbox so beta could exercise Fulfill & Ship —
|
|
144
|
+
completed `Client_Growrk` measure grants (2476/2477/2478, role 1), applied the missing
|
|
145
|
+
`ApiPayloadInterceptors` PRE/POST row for recordId 28, registered
|
|
146
|
+
`tracking-numbers.signatureType` (+ column, + mirrored `containsBattery` grants). Recorded the
|
|
147
|
+
diagnostic sequence (full-table interceptor compare; verify sibling grants exist before a clone, or
|
|
148
|
+
it inserts zero rows silently), the safe reference siblings, that **beta reads the dev-sandbox
|
|
149
|
+
cluster**, that **no carrier is testable outside production**, and that **beta shares production's
|
|
150
|
+
NetSuite account**. Also opened an **escalation to jcardinal** (evidence only — architecture.md
|
|
151
|
+
deliberately left unchanged) on observed `Core.RecordFields` id divergence between production and
|
|
152
|
+
dev-sandbox and the silent wrong-field hazard it creates for hardcoded `recordFieldId`s.
|
|
153
|
+
(mhammontree)
|
|
@@ -6,7 +6,7 @@ project: TOGa Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/Measure.php
|
|
@@ -184,14 +184,19 @@ bare string** — see the gotcha below before wiring a new select field.
|
|
|
184
184
|
`renderEditShipmentFormInput` rendering it via `BaseDisabledInput` on an `isReadOnly` flag. **Not
|
|
185
185
|
persisted to `IF.locationId`** for now — persisting would be a separate backend "set from SO"
|
|
186
186
|
change plus a NetSuite-mapping check.
|
|
187
|
-
- **Reference 1 / Reference 2
|
|
188
|
-
Definitions
|
|
189
|
-
Order #** (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
- **Reference 1 / Reference 2 are WIRED (2026-07-31, TRUE-80494) — user-entered, NOT persisted.**
|
|
188
|
+
Definitions confirmed: **Reference 1 = NetSuite Sales Order #, Reference 2 = NetSuite Purchase
|
|
189
|
+
Order #** (the field-config placeholders were authoritative over the initial verbal description,
|
|
190
|
+
and testing confirmed it — `281144` = `SalesOrders.number` — entered into Reference 1). Per SME
|
|
191
|
+
(Skyler) the values are **user-entered, NOT prefilled from NetSuite, and NOT persisted**; their
|
|
192
|
+
**sole purpose is to reach the carrier and print on the label**. Implemented as **transient
|
|
193
|
+
query-string params** on the existing carrier RecordScript calls (`fulfillShipment` in
|
|
194
|
+
`UpdateShipmentApi.ts` → `upsShipmentApi`/`fedexShipmentApi`) — **no defaultValues, no save
|
|
195
|
+
payload, no DB columns, zero `dbchanges2` work**. Form enforces `characterLimit: 35` (carrier
|
|
196
|
+
limit). Carrier codes + the UPS "CustomerContext never prints" trap: see the
|
|
197
|
+
[carrier-shipping-labels doc](../../_underscore/features/carrier-shipping-labels.md). **⚠ Adding a
|
|
198
|
+
param to a RecordScript means the BACKEND MUST DEPLOY FIRST** — a frontend sending a param the
|
|
199
|
+
deployed PHP doesn't declare throws `Unknown named parameter` and breaks every call.
|
|
195
200
|
- **Phone # is REQUIRED by design (verified, kept per Eric):** config `isRequired:true` +
|
|
196
201
|
`showRequiredIndicator`, enforced by `validateFormOnSubmit`.
|
|
197
202
|
- **Address block layout (Figma):** inline **left** labels (Addressee / Attention / Address 1 /
|
|
@@ -366,6 +371,42 @@ the call 403s and returns no label.
|
|
|
366
371
|
|
|
367
372
|
## Gotchas / known issues
|
|
368
373
|
|
|
374
|
+
- **⚠⚠ A config-driven validator MUST skip DISPLAY-ONLY fields — this caused a TOTAL production
|
|
375
|
+
submit outage (2026-07-29 → 2026-07-31, all clients).** `DUMMYUPDATESHIPMENTFIELDS.json` contains a
|
|
376
|
+
**display-only section header** (id 31, "Address") with **`valueKey: ""` and `isRequired: true`**.
|
|
377
|
+
react-hook-form's `getValues("")` **always returns `undefined`** (its internal `get` helper
|
|
378
|
+
short-circuits on a falsy path), so `validateFormOnSubmit` **always** set a validation error keyed
|
|
379
|
+
on **`""`**. This was harmless for months because the submit guard filtered blank keys
|
|
380
|
+
(`Object.keys(errors).filter(key => key && key.trim() !== "")`). The CodeRabbit **batch 2** change
|
|
381
|
+
replaced that guard with the **fresh boolean** returned by `validateFormOnSubmit`, which does **not**
|
|
382
|
+
filter blank keys → `hasErrors` always `true` → **silent early return → NO network request at all**.
|
|
383
|
+
It was invisible because the error is keyed on `""` and that field renders through `BaseInput`'s
|
|
384
|
+
`case "none"` branch, which has **no `errorMessage` slot**. Blast radius: **every client**, and all
|
|
385
|
+
three actions (Fulfill & Ship, Save, Save & Create New Shipment) share `onFormSubmit`.
|
|
386
|
+
**Two durable rules:**
|
|
387
|
+
1. A config-driven validator must skip display-only fields —
|
|
388
|
+
`if (!field.valueKey || inputType === "none") continue;`
|
|
389
|
+
2. **"No network request at all" is the diagnostic signature of client-side validation blocking**,
|
|
390
|
+
not a backend fault. Check the Network tab for the *absence* of a call before debugging the API.
|
|
391
|
+
|
|
392
|
+
**Decision (2026-07-31):** the fix shipped as a **minimal revert of the CALLER guard only**,
|
|
393
|
+
deliberately **keeping** `validateFormOnSubmit`/`checkDimensions` returning booleans so CodeRabbit
|
|
394
|
+
**batch 3**'s `clearErrors` fix survives. Rationale: smallest, safest diff to end a total production
|
|
395
|
+
outage. **Accepted trade-off:** this restores the first-submit-slips-through behaviour CodeRabbit
|
|
396
|
+
flagged, and CodeRabbit will likely re-flag it. The durable alternative — skip
|
|
397
|
+
`!field.valueKey || inputType === "none"` **inside** the validator, keeping both behaviours — is
|
|
398
|
+
recorded as future work (see the TRUE-80494 deployment doc).
|
|
399
|
+
- **⚠ `saveShipment` is NOT transactional, and the edit screen masks the fallout.** `saveShipment`
|
|
400
|
+
(`UpdateShipmentApi.ts`) performs **three sequential POSTs** — item-fulfillment → tracking-number →
|
|
401
|
+
bridge — with **no rollback**. When the tracking-number POST fails, the already-created
|
|
402
|
+
`ItemFulfillment` is **orphaned**; and because `prePost` defaults the stage to SHIPPED, that orphan
|
|
403
|
+
can appear in **Fulfilled Shipments as a real shipment with zero tracking numbers** (observed:
|
|
404
|
+
F100074, id 7180, dev-sandbox). The edit screen then hides it: `EditShipmentForm.tsx` (~line 287)
|
|
405
|
+
does `const tn = editShipment.itemFulfillmentTrackingNumbers?.[0]?.trackingNumber ?? {}`, turning
|
|
406
|
+
"no tracking number" into an **empty object** — so the page **loads looking fine** and only fails on
|
|
407
|
+
save with `PUT /tracking-numbers/undefined` → **EV-6 with `uuid: "undefined"`**. **Diagnostic rule: a
|
|
408
|
+
literal `"undefined"` in a request URL means a client-side value was never populated — look for a
|
|
409
|
+
masking `?? {}` fallback**, not a backend bug.
|
|
369
410
|
- **⚠ The shared `GoogleMapsLink` mapped the RECIPIENT, not the address.** The shared
|
|
370
411
|
`components/ui/GoogleMapsLink.tsx` built its query from `[addressee, line1, line2, city, zip]`,
|
|
371
412
|
which had two bugs: (a) it included the **`addressee` (recipient name)**, so Google surfaced the
|
|
@@ -669,8 +710,14 @@ the call 403s and returns no label.
|
|
|
669
710
|
50px); may explore stacking. Owners: David Mancol (design) + Alexandra Peterson (phase 2/3).
|
|
670
711
|
**Partly landed 2026-07-14:** the flexible-gap part of the spec now ships as a CSS
|
|
671
712
|
`clamp(2px,1vw,50px)` on the row gaps; column-count reduction and stacking are still open.
|
|
672
|
-
-
|
|
673
|
-
|
|
713
|
+
- ~~Reference 1/2 value wiring~~ — **done 2026-07-31 (TRUE-80494)**: transient carrier params, not
|
|
714
|
+
persisted; see the form section + the carrier-shipping-labels doc.
|
|
715
|
+
- **Harden the validator instead of the caller** — replace the reverted caller-side blank-key guard
|
|
716
|
+
with a `!field.valueKey || inputType === "none"` skip **inside** `validateFormOnSubmit`, restoring
|
|
717
|
+
CodeRabbit's first-submit fix without re-breaking submit. See the outage gotcha.
|
|
718
|
+
- **Make `saveShipment` transactional (or clean up on failure)** — today a failed tracking-number POST
|
|
719
|
+
orphans an ItemFulfillment that shows as a fulfilled shipment; and drop the masking `?? {}` on the
|
|
720
|
+
edit rehydrate so a missing tracking number surfaces on load, not on save.
|
|
674
721
|
- **Global-vs-per-SO Shipments list** — pending Eric's model decision; see the gotcha above.
|
|
675
722
|
- **"Fulfill & Ship from an existing Item Fulfillment"** third entry mode — see Planned direction.
|
|
676
723
|
|
|
@@ -682,7 +729,12 @@ expected once QA/users start.
|
|
|
682
729
|
The return-label feature (TRUE-79191) needs **four** things present in a given environment or it
|
|
683
730
|
silently mis-behaves; treat these as the release checklist when promoting to a new env:
|
|
684
731
|
1. **`ItemFulfillments.returnAddressId` column** (Client DB) — the persisted return address FK.
|
|
685
|
-
2. **RecordField
|
|
732
|
+
2. **The Core `RecordField` backing `returnAddressId`** — **id 2480 in production** (corrected
|
|
733
|
+
2026-07-31; the migration previously said 2434, which in prod is `items.isFulfillable`). **Do not
|
|
734
|
+
verify a `RecordFields` id against the target environment before relying on it** — prod and
|
|
735
|
+
dev-sandbox have drifted (open escalation: see the
|
|
736
|
+
[non-prod metadata drift workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md));
|
|
737
|
+
in dev-sandbox this field is **not registered at all**.
|
|
686
738
|
3. **`generateReturnLabel` RecordScript** (Client DB) — the scripted-API dispatch grant.
|
|
687
739
|
4. **Bridge ACL logic groups for records 317–322** (Client DB) — or reads 403 EZ-1; see the
|
|
688
740
|
tracking-number-bridges doc.
|
|
@@ -709,6 +761,21 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
|
|
|
709
761
|
for Compass and is not yet in prod.
|
|
710
762
|
|
|
711
763
|
## Change history
|
|
764
|
+
- 2026-08-03 — TRUE-80494 (hotfix + feature follow-up to TRUE-79191): **ended a TOTAL production
|
|
765
|
+
Fulfill & Ship submit outage** (all clients, since the 2026-07-29 deploy) — the display-only
|
|
766
|
+
"Address" section header (`valueKey: ""`, `isRequired: true`) always produced a validation error
|
|
767
|
+
keyed on `""` (RHF `getValues("")` is always `undefined`), which was previously filtered by the
|
|
768
|
+
caller's blank-key guard and became fatal once the caller branched on the validator's fresh boolean
|
|
769
|
+
→ silent early return, **no network request at all**, invisible because `BaseInput`'s `case "none"`
|
|
770
|
+
has no error slot. Fixed by a **minimal revert of the caller guard only** (keeping the boolean
|
|
771
|
+
returns so CodeRabbit batch 3's `clearErrors` fix survives); validator-side skip recorded as future
|
|
772
|
+
work. **Wired Reference 1 / Reference 2** end-to-end as **transient, unpersisted** carrier params
|
|
773
|
+
(user-entered per SME Skyler; Ref1 = NS Sales Order, Ref2 = NS Purchase Order; `characterLimit: 35`;
|
|
774
|
+
zero dbchanges2 work) — **backend must deploy before frontend** because an undeclared RecordScript
|
|
775
|
+
param throws. Documented that **`saveShipment` is not transactional** (three sequential POSTs, no
|
|
776
|
+
rollback → orphaned ItemFulfillment that shows as *fulfilled* with no tracking, e.g. F100074/7180)
|
|
777
|
+
and that the edit rehydrate's `?? {}` masks it into a `PUT /tracking-numbers/undefined` → EV-6.
|
|
778
|
+
(mhammontree)
|
|
712
779
|
- 2026-07-22 — TRUE-79191 (commit `e91f7029d`, full-stack): **IMPLEMENTED package Units of Measure** —
|
|
713
780
|
the package weight + dimensions now have real Pound/Inch unit selectors that persist on the
|
|
714
781
|
`TrackingNumber` and rehydrate on reopen (verified end-to-end). ZERO new tables: reused the client
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
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
|
package/knowledge/INDEX.md
CHANGED
|
@@ -20,8 +20,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
20
20
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 42 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 38 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
|
-
- **api2** (API) —
|
|
24
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
23
|
+
- **api2** (API) — 21 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 4 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 7 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -5,13 +5,14 @@ project: API
|
|
|
5
5
|
client: aig
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
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