toga-ai 1.0.1049 → 1.0.1050
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/contact-customer-required-internal-customer.md +15 -5
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +4 -1
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +5 -0
- package/knowledge/2.0/apps/worker2/features/startech-webhook-handler.md +2 -1
- package/package.json +1 -1
package/knowledge/2.0/apps/_underscore/features/contact-customer-required-internal-customer.md
CHANGED
|
@@ -18,6 +18,8 @@ files:
|
|
|
18
18
|
- worker2/Worker/Startech.php
|
|
19
19
|
- toga25-supply/src/utils/internalCustomer.ts
|
|
20
20
|
- toga2-commerce/src/utils/internalCustomer.ts
|
|
21
|
+
- toga2-commerce/src/api/CartApi.ts
|
|
22
|
+
- toga2-commerce/src/api/axiosInstance.ts
|
|
21
23
|
related:
|
|
22
24
|
- ./client-user-contactid-required.md
|
|
23
25
|
- ../../api2/features/nested-relationship-writes.md
|
|
@@ -28,7 +30,10 @@ Every `Client_<Tenant>.Contacts` row must have a `customerId`; with no real cust
|
|
|
28
30
|
|
|
29
31
|
## Summary
|
|
30
32
|
|
|
31
|
-
Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NULL** in every 2.0 client DB. Every tenant has ONE `Customers` row
|
|
33
|
+
Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NULL** in every 2.0 client DB. Every tenant has ONE "Internal customer" `Customers` row with the **same hardcoded uuid** `20436694-8925-4eea-95b9-c97d7caa813b`. Contacts with no real customer (client staff, imports) point at it.
|
|
34
|
+
|
|
35
|
+
- **The row is NAMED AFTER THE CLIENT, not "Internal"** (jcardinal). `Client/2026-10-16a` sets the name with a `CASE DATABASE()` map of client DB → client name (copied from prod `Core.Clients` at authoring time, e.g. `Client_Compass` → `'Compass Group'`); an unknown DB falls back to `SUBSTRING(DATABASE(), 8)`.
|
|
36
|
+
- **Find it only by the uuid, never by name.**
|
|
32
37
|
|
|
33
38
|
**Rule:** every code path that creates a Contact must set a customer.
|
|
34
39
|
- api2 payload → `customer: {uuid: <INTERNAL_CUSTOMER_UUID>}` (or the real customer's uuid).
|
|
@@ -36,13 +41,16 @@ Decision (2026-10-16, jcardinal, TRUE-82841): `Contacts.customerId` is **NOT NUL
|
|
|
36
41
|
|
|
37
42
|
Rejected: a `Customers.slug` marker column; app-level validation instead of a DB `NOT NULL`; a silent model-wide default.
|
|
38
43
|
|
|
39
|
-
**Rollout order:** `Client/2026-10-16a` seed (before deploy) → code deploy → `Client/2026-10-16b` backfill + `NOT NULL` (after deploy).
|
|
44
|
+
**Rollout order:** `Client/2026-10-16a` seed **+ backfill** (before deploy) → code deploy → `Client/2026-10-16b` straggler backfill + `NOT NULL` (after deploy).
|
|
45
|
+
- Backfill must run BEFORE the code that sends `customer`: api2 matches nested contacts on `customerId` too (gotcha below), so an un-backfilled NULL contact would be duplicated.
|
|
46
|
+
- Running `b` before the code fix breaks every unfixed create path.
|
|
40
47
|
|
|
41
48
|
## How it works
|
|
42
49
|
|
|
43
50
|
- Constant: `_Model_Client_Customer::INTERNAL_CUSTOMER_UUID` (`_underscore/Model/Client/Customer.php`). Front-ends keep their own `INTERNAL_CUSTOMER_UUID` in `src/utils/internalCustomer.ts` (toga25-supply, toga2-commerce).
|
|
44
51
|
- `internalCustomerId(string $database = DB_CLIENT): int` — resolves the id by uuid on the given connection; ids differ per tenant, the uuid does not.
|
|
45
|
-
- `Contacts.customerId` = `Core.RecordFields` id **1651** (record 37 `contacts`), childPolicy `MATCH
|
|
52
|
+
- `Contacts.customerId` = `Core.RecordFields` id **1651** (record 37 `contacts`), childPolicy `MATCH`, **`isIdentifier = 1`** (prod) — a `{uuid}` nested customer links, never creates (`customers.uuid` 675 is an identifier). Base role can write it in Wje / Elite / Pcmaticb2b / Compass / CompassCanada; Public cannot.
|
|
53
|
+
- Nested-write ACL = top-level record create + FIELD write for every nested field sent; a missing 1651 grant is EV-12 (400), never a silent drop — see [nested-relationship-writes](../../api2/features/nested-relationship-writes.md).
|
|
46
54
|
- A contact can also be created **through a parent's nested `contact` FK** — those paths need the customer too (list of contact FKs that can CREATE: [nested-relationship-writes](../../api2/features/nested-relationship-writes.md)).
|
|
47
55
|
|
|
48
56
|
### Every contact-creation path (2026-10-16)
|
|
@@ -58,15 +66,17 @@ Rejected: a `Customers.slug` marker column; app-level validation instead of a DB
|
|
|
58
66
|
| Freshservice syncs (contacts + nested on units/tickets) | `worker2/Worker/Wje.php`, `Worker/Elite.php` | Wje, Elite | Internal uuid |
|
|
59
67
|
| Startech tickets (nested contact) | `worker2/Worker/Startech.php` | Pcmaticb2b | Internal uuid — see gotcha |
|
|
60
68
|
| Location contact | toga25-supply `useCreateLocationContact.ts` | all | Internal uuid |
|
|
61
|
-
| Cart "Add user" nested contact | toga2-commerce `CartApi.ts` `addUser` | all | Internal uuid |
|
|
69
|
+
| Cart "Add user" nested contact | toga2-commerce `CartApi.ts` `addUser` (signed-in user's token) | all | Internal uuid — see gotcha |
|
|
62
70
|
| Rate SSO | `Model/Rate/ClientAuthentication.php` | Rate | already a real customer |
|
|
63
71
|
|
|
64
72
|
## Gotchas
|
|
65
73
|
|
|
66
|
-
- **⚠ A
|
|
74
|
+
- **⚠ A nested contact that sends `customer` is MATCHED on `customerId` too** (1651 is an identifier). `{c_wjeUserId, customer:{uuid}}` searches c_wjeUserId AND customerId; an existing contact with NULL or a different customer does not match, so `MATCH_CREATE`/`MATCH_UPSERT` **creates a duplicate contact** (not an overwrite). Startech/Pcmaticb2b: 3 contacts with a real customer + `c_startechUserId` get a duplicate on their next Startech ticket — **open item**; proposed fix (look up by `c_startechUserId` first, send `{uuid}`) not taken.
|
|
75
|
+
- **Commerce signup ACL (prod 2026-10-16):** `addUser` (`POST /users` depth -1) runs on the signed-in user's token (`src/api/axiosInstance.ts` ~L193), not the public token. Client_Compass: users-create roles = Base(1), API(3), Admin(4), SuperUser(5), Manager(8); 1651 writable for Base, API, Compass Base(7) only — Admin(4) alone would fail EV-12 (no current user depends on it; parity grant offered, not taken). Client_CompassCanada: only Base creates users, and Base can write 1651. Everyone who can sign up today still can.
|
|
67
76
|
- **Prod baseline 2026-10-16:** ~443k contacts had `customerId` NULL; only Rate (726/728) and Pcmaticb2b (21/398) used the field; many clients had zero `Customers` rows. Most NULL contacts are client staff linked from `Users.contactId`.
|
|
68
77
|
- New raw-SQL contact INSERTs: include `customerId` in the column list — omitted, the INSERT fails once the column is `NOT NULL`. Same write-order lesson as [client-user-contactid-required](./client-user-contactid-required.md).
|
|
69
78
|
- Backfill/ALTER technique (`dtUpdated = dtUpdated`, strict-mode in-place ALTER): [rerunnable-additive-inserts](../../dbchanges2/features/rerunnable-additive-inserts.md).
|
|
70
79
|
|
|
71
80
|
## Change history
|
|
81
|
+
- 2026-10-16 — Row named after the client (find by uuid only); backfill moved into `a` (before deploy) because 1651 is an identifier and unmatched contacts get duplicated, not overwritten; commerce signup ACL check (TRUE-82841). (jcardinal)
|
|
72
82
|
- 2026-10-16 — Initial. Internal customer (fixed uuid, every tenant) + `Contacts.customerId` NOT NULL; every contact-create path fixed (TRUE-82841). (jcardinal)
|
|
@@ -196,10 +196,13 @@ behaviour — it bites any write to a nested child, not one feature. Worked exam
|
|
|
196
196
|
- **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
|
|
197
197
|
- **An identifier that is unique only inside a parent (e.g. `bundles.number` per catalog) links the WRONG record, silently.** The match uses only the identifier fields sent, so `bundle: {number: "001"}` hits another catalog's `001`. Send `uuid` (or add the scoping identifier) resolved in the caller. Seen on Prudential India REQs — see [bundle catalog resolution](../../../clients/prudential/features/service-request-bundle-catalog-resolution.md).
|
|
198
198
|
- **⚠ A nested `unit` with only an `item` (no serial/asset tag) REUSES one existing unit.** `ServiceRequests_Units.unitId` is `MATCH_UPSERT` and `Units.itemId` is an identifier, so `{unit:{item:{uuid}}}` matches the first unit of that item — every SR links the same unit. For serial-less requests attach a **bundle** instead (`ServiceRequests_Bundles.bundleId` is `MATCH`; send `{bundle:{uuid}}`). See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md).
|
|
199
|
-
- **Parent FKs that can CREATE a Contact** (prod `Core.RecordFields.childPolicy`, 2026-10-16): `MATCH_CREATE` — `Users.contactId`, `PurchaseOrders.contactId`; `MATCH_UPSERT` — `Tickets.contactId`, `Tickets.reportedByContactId`, `Units.contactId`, `UnitNotes.contactId`, `Entitlements.contactId`, `ServiceRequests.requestedForContactId`, `ContactPhoneNumbers.contactId`. A nested contact created through any of these must carry a `customer` — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md).
|
|
199
|
+
- **Parent FKs that can CREATE a Contact** (prod `Core.RecordFields.childPolicy`, 2026-10-16): `MATCH_CREATE` — `Users.contactId`, `PurchaseOrders.contactId`; `MATCH_UPSERT` — `Tickets.contactId`, `Tickets.reportedByContactId`, `Units.contactId`, `UnitNotes.contactId`, `Entitlements.contactId`, `ServiceRequests.requestedForContactId`, `ContactPhoneNumbers.contactId`. A nested contact created through any of these must carry a `customer` — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md).
|
|
200
|
+
- **⚠ Every identifier field SENT joins the match.** `contacts.customerId` (1651) is `isIdentifier = 1`, so `{c_wjeUserId, customer:{uuid}}` matches on c_wjeUserId AND customerId. A contact with a NULL/different customer does not match → `MATCH_CREATE`/`MATCH_UPSERT` **creates a duplicate**. Backfill the field before code starts sending it.
|
|
201
|
+
- **Nested-write ACL** (`V2.php` `getForeignKeyValue` ~L9790-10469): no record-level ACL on the nested record. Checked = top-level record create (e.g. users, Core.Records 2) + FIELD write for every field sent on the nested record (e.g. contacts, 37), using the caller's `id.client.roles`. A missing field grant fails **EV-12** (400, "lack write permissions"), never a silent drop.
|
|
200
202
|
- **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
|
|
201
203
|
|
|
202
204
|
## Change history
|
|
205
|
+
- 2026-10-16 — Corrected: a sent identifier field (contacts.customerId 1651) joins the match, so a mismatched contact is duplicated, not overwritten; nested-write ACL = top-level create + per-field write, EV-12 on a gap (TRUE-82841). (jcardinal)
|
|
203
206
|
- 2026-10-16 — Listed the parent FKs whose nested contact can CREATE a Contact (each must send `customer`); MATCH_UPSERT overwrites a matched contact's customer (TRUE-82841). (jcardinal)
|
|
204
207
|
- 2026-09-30 — Gotcha: item-only nested unit matches an existing unit; use a bundle for serial-less SRs. (rgirish)
|
|
205
208
|
- 2026-09-29 — Gotcha: a per-parent identifier (`bundles.number`) matches across parents and links the wrong row. (rgirish)
|
|
@@ -326,6 +326,10 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
|
326
326
|
NULL (no silent `0`), and in-place NOT NULL **needs** strict mode (error 1846 otherwise, even with
|
|
327
327
|
0 NULLs). Backfill with `dtUpdated = dtUpdated` so syncs do not see every row as changed. Worked
|
|
328
328
|
file: `Client/2026-10-16b - TRUE-82841 Contacts customerId NOT NULL.sql`.
|
|
329
|
+
- **Backfill BEFORE the deploy when the column is an api2 identifier.** Put the main backfill in the
|
|
330
|
+
pre-deploy seed file, and repeat it for stragglers in the post-deploy ALTER file. Otherwise new code
|
|
331
|
+
that sends the field fails to match un-backfilled rows and api2 creates duplicates (TRUE-82841:
|
|
332
|
+
`Client/2026-10-16a` seed + backfill, `Client/2026-10-16b` straggler backfill + ALTER).
|
|
329
333
|
|
|
330
334
|
### ⚠ A column DEFAULT does NOT protect a `_Model`-written column
|
|
331
335
|
|
|
@@ -555,6 +559,7 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
|
|
|
555
559
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
556
560
|
|
|
557
561
|
## Change history
|
|
562
|
+
- 2026-10-16 — Backfill an api2-identifier column before the deploy (seed file), stragglers + ALTER after (TRUE-82841). (jcardinal)
|
|
558
563
|
- 2026-10-16 — NOT NULL alternative for big tables: strict session + `ALGORITHM=INPLACE, LOCK=NONE`
|
|
559
564
|
(1846 without strict), `lock_wait_timeout`, `dtUpdated = dtUpdated` backfill (TRUE-82841). (jcardinal)
|
|
560
565
|
- 2026-10-16 — Fan-out seed keyed on a slug another folder sets can silently no-op; back-fill slugs
|
|
@@ -62,9 +62,10 @@ Both filters use `fields` to request only `id`/`uuid`. Filtering on `ticketTypeI
|
|
|
62
62
|
|
|
63
63
|
- `c_escalateToStartech: 0` must be in the payload. If omitted, the interceptor still skips Startech (null is also falsy), but the field stays NULL in TOGA 2.0 — making webhook tickets indistinguishable from TOGaDesk-created ones.
|
|
64
64
|
- `ticketType` is resolved via `c_startechTicketTypeId` on `TicketTypes` — not a hardcoded string. Startech type IDs: SR=6, PCS=54, API=201.
|
|
65
|
-
- **⚠ The nested `contact` sends the Internal customer** (`Contacts.customerId` NOT NULL — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md)). `Tickets.contactId` is `MATCH_UPSERT
|
|
65
|
+
- **⚠ The nested `contact` sends the Internal customer** (`Contacts.customerId` NOT NULL — see [contact-customer-required-internal-customer](../../_underscore/features/contact-customer-required-internal-customer.md)). `Tickets.contactId` is `MATCH_UPSERT` and `customerId` is an identifier, so the match uses `c_startechUserId` AND customerId. A contact whose customer is not the Internal one does not match and **gets a DUPLICATE contact** (not an overwrite). Open item (2026-10-16): 3 Pcmaticb2b contacts with a real customer + `c_startechUserId` will be duplicated on their next ticket. Proposed fix, not taken: look up the contact by `c_startechUserId` first and send `{uuid}`.
|
|
66
66
|
|
|
67
67
|
## Change history
|
|
68
|
+
- 2026-10-16 — Corrected: a mismatched-customer contact is duplicated, not overwritten; proposed lookup-first fix recorded (TRUE-82841). (jcardinal)
|
|
68
69
|
- 2026-10-16 — Nested contact now carries the Internal customer; noted the customer-overwrite risk on matched contacts (TRUE-82841). (jcardinal)
|
|
69
70
|
|
|
70
71
|
## Related
|
package/package.json
CHANGED