toga-ai 1.0.860 → 1.0.861
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/store/INDEX.md +1 -0
- package/knowledge/1.0/apps/store/features/prudential-sso-login.md +37 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/client-user-contactid-required.md +67 -0
- package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +50 -0
- package/knowledge/INDEX.md +3 -2
- package/knowledge/clients/managelife/INDEX.md +5 -0
- package/knowledge/clients/managelife/profile.md +22 -0
- package/knowledge/clients/prudential/features/device-information-import-and-contact-linking.md +5 -0
- package/package.json +1 -1
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
5
|
| [Store (1.0 Storefront / SAML ACS) Architecture](architecture.md) | 1.0 (App_) storefront that also hosts the SAML ACS for several `*.togahub.com` clients (inbound IdP assertion decrypted here, not in the 2.0 `saml` gateway); op |
|
|
6
|
+
| [Prudential SAML/hash SSO login — user+contact provisioning (and the unsigned ?hash impersonation debt)](features/prudential-sso-login.md) | The Prudential storefront SSO block that provisions the user + contact on login, why it now links `Users.contactId`, and the pre-existing unsigned `?hash=` impe |
|
|
6
7
|
| [Store SAML SP Certificate & Private Keys (togahub.com)](features/saml-sso-certificate-and-keys.md) | How the store SAML SP credential (`togahub.com` X.509 cert + RSA private key) is stored, loaded and rotated — incl. |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Prudential SAML/hash SSO login — user+contact provisioning (and the unsigned ?hash impersonation debt)"
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: store
|
|
5
|
+
project: Store
|
|
6
|
+
client: prudential
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- store/_/app/framework.php
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- ../../../../2.0/apps/_underscore/features/client-user-contactid-required.md
|
|
16
|
+
- ../../../../clients/prudential/features/device-information-import-and-contact-linking.md
|
|
17
|
+
---
|
|
18
|
+
The Prudential storefront SSO block that provisions the user + contact on login, why it now links `Users.contactId`, and the pre-existing unsigned `?hash=` impersonation debt. Open before touching store SSO login or Prudential user provisioning.
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
On Prudential SAML SSO (`store/_/app/framework.php`), after the assertion is decrypted the block **finds/creates the user and the contact separately**. It historically **never linked them** — the root cause of ~14k null-`contactId` Prudential users. It now links them.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
- After find/create of the user and contact, the block issues a **non-fatal** `PUT /users/{uuid} {contact:{uuid}}` to link `Users.contactId`.
|
|
27
|
+
- `Users.contactId` is a normal forward FK, so `{uuid}` links safely (not a singular-FK back-reference case).
|
|
28
|
+
- The link checks `isSuccess` and **logs on failure without breaking login** — a failed link must never block a user's SSO.
|
|
29
|
+
|
|
30
|
+
This is the Prudential storefront's slice of the platform-wide contactId initiative — see [client-user-contactid-required](../../../../2.0/apps/_underscore/features/client-user-contactid-required.md). Existing null-contactId Prudential users are backfilled by the `Client_Prudential/2026-09-21a` migration (XID-keeper link) — see [device-information-import-and-contact-linking](../../../../clients/prudential/features/device-information-import-and-contact-linking.md).
|
|
31
|
+
|
|
32
|
+
## Gotchas
|
|
33
|
+
|
|
34
|
+
- **⚠ SECURITY DEBT — the `?hash=` login trusts an UNSIGNED identity from the URL (impersonation).** After assertion, the Prudential branch base64-encodes a plain `xid:first:last:email` string and redirects to `<returnTo>?hash=<base64>` (`framework.php` ~L120-130). The hash is **not signed / no HMAC**, so any crafted `?hash=` can **impersonate any employee** — it creates the user+contact and reads their devices/ServiceNow assets — and PII rides in the URL query string (web-server logs). Pre-existing; a ticket is being filed separately. Do not extend this handoff without signing it. The store architecture also flags this handoff as unsigned/unencrypted.
|
|
35
|
+
|
|
36
|
+
## Change history
|
|
37
|
+
- 2026-09-21 — Initial. Added the non-fatal `PUT /users {contact:{uuid}}` link so SSO no longer leaves `contactId` null (root cause of ~14k null-contactId Prudential users). Recorded the pre-existing unsigned `?hash=` impersonation debt as a known security gotcha (ticket filed separately). (jcardinal)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent |
|
|
16
16
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind Fulfill & Ship: buying/persisting a carrier label (UPS/FedEx) and creating the NetSuite Item Fulfillment; open for label, reprint, retu |
|
|
17
17
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | Minimum recipe + traps for running real 2.0 framework code (`_Model`/`_Query`/`_Database`) from a bare `php script.php`; open before writing a CLI harness/backf |
|
|
18
|
+
| [Client Users require a contactId — the postPost backfill interceptor & every creation path](features/client-user-contactid-required.md) | Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, |
|
|
18
19
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` S3 helpers (copy/get/delete/list) with per-bucket region override and a 5 GB single-PUT limit; open before doing S3 access from 2.0 code. |
|
|
19
20
|
| [_Cloud Secrets Manager helper (_Cloud::getSecret)](features/cloud-secrets-manager.md) | `_Cloud::getSecret` reads AWS Secrets Manager (own default-provider auth, never throws, per-process cache); open before pulling secret material at request time. |
|
|
20
21
|
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Project-local `_Component_*`/`_Model_*` must declare the app namespace AND live in a `<Name>/<Name>.php` directory, or the autoloader fatals at runtime; open wh |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Client Users require a contactId — the postPost backfill interceptor & every creation path"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-21
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/User.php
|
|
13
|
+
- worker2/Worker/Sync/ActiveDirectory.php
|
|
14
|
+
- worker2/Worker/Netsuite/Opportunity.php
|
|
15
|
+
- dbchanges2/Core/2026-09-21d - Register users postPost contact interceptor.sql
|
|
16
|
+
related:
|
|
17
|
+
- ../../api2/features/api-payload-interceptors.md
|
|
18
|
+
- ../../api2/features/nested-relationship-writes.md
|
|
19
|
+
- ./note-record-authorship.md
|
|
20
|
+
- ./user-email-as-identity-and-password-reset.md
|
|
21
|
+
- ../../../1.0/apps/store/features/prudential-sso-login.md
|
|
22
|
+
- ../../../clients/prudential/features/device-information-import-and-contact-linking.md
|
|
23
|
+
- ../../dbchanges2/features/rerunnable-additive-inserts.md
|
|
24
|
+
---
|
|
25
|
+
Every `Client_<Tenant>.Users` row must have a non-null `contactId`; the shared `_Model_Client_User::postPost` interceptor guarantees it for api2-created users, and each other creation path was fixed to link one too. Open before touching user creation or `Users.contactId`.
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
Decision (2026-09-21, CTO Option B): `Client_<Tenant>.Users.contactId` is made **NOT NULL per tenant** after backfilling a Contact for every user missing one. Null `contactId` was the root of the missing/duplicate-contact bugs, so the goal is to kill it and keep it dead — a **forward fix on every creation path** plus a per-client backfill+ALTER.
|
|
30
|
+
|
|
31
|
+
**Rollout order that matters:** deploy the forward fixes first, then run the per-client backfill+`NOT NULL` migrations. A backfill run before the code fix regenerates null rows.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
### The shared forward fix — `_Model_Client_User::postPost` (CTO Option B)
|
|
36
|
+
|
|
37
|
+
`postPost(&$api, &$payload)` on the **base client user model** creates a Contact + a primary **Work** email and links `Users.contactId` when an api2-created user has none. Registered as a **postPost interceptor** via `Core.ApiPayloadInterceptors` (`recordId 2` = users, `clientId` + `apiId` NULL = all clients, `prePostProcessing 'POST'`, `httpMethod 'POST'`, guarded insert) — see [api-payload-interceptors](../../api2/features/api-payload-interceptors.md) for how the row makes the hook live.
|
|
38
|
+
|
|
39
|
+
- **No-op** when `contactId` is already set, or the user has no email. Runs **inside the create transaction**.
|
|
40
|
+
- Chosen over a base-model `save()` override for blast radius / "app-level over core" — the interceptor is the row-gated switch, not a change every save inherits.
|
|
41
|
+
- Placed on `_Model_Client_User` **on purpose** (the "row is the switch" pattern): the `apiId NULL / clientId NULL` row makes it fire for every tenant's api2 CRUD.
|
|
42
|
+
|
|
43
|
+
**⚠ Set `Contacts.primaryContactEmailAddressId` SERVER-SIDE — a second model load+save — NOT via a nested payload.** The nested singular-FK path is unreliable: the CREATE path re-parents a value-matched child, and the UPDATE path echoes null. See [nested-relationship-writes](../../api2/features/nested-relationship-writes.md) (singular-FK back-reference defects).
|
|
44
|
+
|
|
45
|
+
### Every 2.0 user-creation origin path (does it set contactId?)
|
|
46
|
+
|
|
47
|
+
The interceptor covers the **generic api2 CRUD** for everyone. The dedicated creation paths each had to be fixed or already set it:
|
|
48
|
+
|
|
49
|
+
| Path | Repo / code | Client | contactId |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| api2 CRUD (all clients) | `_Model_Client_User::postPost` | all | now linked (interceptor) |
|
|
52
|
+
| AD sync | `worker2/Worker/Sync/ActiveDirectory.php` | True | **fixed** — now creates+links |
|
|
53
|
+
| ClickUp user cache | `worker2/Worker/Netsuite/Opportunity.php` `cacheClickupUser()` (raw SQL) | True | **fixed** — `ensureTrueContactId` |
|
|
54
|
+
| Store SAML/hash SSO | `store/_/app/framework.php` | Prudential | **fixed** — see [prudential-sso-login](../../../1.0/apps/store/features/prudential-sso-login.md) |
|
|
55
|
+
| SAML gateway | `_Model_Rate_ClientAuthentication` (saml) | Rate | already sets it |
|
|
56
|
+
| Compass PeopleFile | worker2 (raw SQL) | Compass | already sets it |
|
|
57
|
+
|
|
58
|
+
Northwell ServiceNow users arrive via api2 eBonding (`_Model_Northwell_Ticket` interceptors), not a dedicated worker, so the interceptor covers them.
|
|
59
|
+
|
|
60
|
+
## Gotchas
|
|
61
|
+
|
|
62
|
+
- **⚠ A reconstructed `new _Model_Client_User($id)` does NOT carry `contactId`.** In `ActiveDirectory.php` the AD sync reads the existing `contactId` from the **bulk SELECT** and only creates a contact when it is truly missing. A naive `empty($user->contactId)` guard against a freshly reconstructed model reads null even when the row has a contact — and would **mint a duplicate contact every sync**. Trust the SELECT'd value, not the rehydrated model property.
|
|
63
|
+
- **⚠ Set `primaryContactEmailAddressId` with a second server-side load+save, never a nested payload.** The nested singular-FK write re-parents (CREATE) or echoes null (UPDATE).
|
|
64
|
+
- **Interceptor no-ops on a user with no email** — a no-email straggler gets no Contact from this path (the backfill migrations handle those name-only, per client). It is why the fan-out `NOT NULL` skips a client that still has a no-email residual (see [rerunnable-additive-inserts](../../dbchanges2/features/rerunnable-additive-inserts.md)).
|
|
65
|
+
|
|
66
|
+
## Change history
|
|
67
|
+
- 2026-09-21 — Initial. Decided `Client_*.Users.contactId` goes NOT NULL platform-wide (CTO Option B); built the shared `_Model_Client_User::postPost` interceptor (Core row: recordId 2, clientId+apiId NULL) to create+link a Contact for api2-created users; fixed the worker2 AD-sync and `cacheClickupUser` True paths; recorded the origin map and the reconstructed-model `contactId` gotcha. (jcardinal)
|
|
@@ -268,6 +268,50 @@ Worked example: `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` (adds
|
|
|
268
268
|
`Client/2026-08-18` `qtyCommitted` file uses a bare `ALTER` and is exactly the shape this avoids** —
|
|
269
269
|
it is not the template to copy.
|
|
270
270
|
|
|
271
|
+
### ⚠ Guarding a `NOT NULL` ALTER — `sql_mode = ''` SILENTLY coerces leftover NULLs
|
|
272
|
+
|
|
273
|
+
`ALTER … MODIFY col <type> NOT NULL` after a backfill looks safe, but this cluster's `sql_mode` is
|
|
274
|
+
`''` (empty), so MySQL does **not** reject rows that are still NULL — it **silently coerces them to
|
|
275
|
+
`0`** (an invalid FK, e.g. `Users.contactId = 0`). Same `sql_mode` family as the zero-date `dtCreated`
|
|
276
|
+
trap above. Guard the ALTER on a residual count and compile it to the ALTER only when clean:
|
|
277
|
+
|
|
278
|
+
```sql
|
|
279
|
+
SET @n = (SELECT COUNT(*) FROM Users WHERE contactId IS NULL);
|
|
280
|
+
SET @sql = IF(@n = 0,
|
|
281
|
+
'ALTER TABLE Users MODIFY contactId INT NOT NULL',
|
|
282
|
+
'<abort-or-skip>'
|
|
283
|
+
);
|
|
284
|
+
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- **Single-tenant file → abort LOUD.** Make `<abort-or-skip>` a statement that errors under `sql_mode ''`
|
|
288
|
+
— `SELECT \`unknown col\` FROM DUAL` throws *Unknown column*, so the file stops instead of shipping a
|
|
289
|
+
broken column.
|
|
290
|
+
- **Fan-out (`Client/`) file → skip with `DO 0`.** Make `<abort-or-skip>` = `'DO 0'` so one tenant's
|
|
291
|
+
no-email straggler does not abort the ALTER for every other tenant. Wrap the backfill in
|
|
292
|
+
`START TRANSACTION … COMMIT` and put the ALTER **after** the COMMIT.
|
|
293
|
+
|
|
294
|
+
### ⚠ latin1 schema drift breaks a forced COLLATE in a fan-out file
|
|
295
|
+
|
|
296
|
+
Some tenants' `uuid`/text columns are `latin1`, not utf8mb4 (verified Nychh, Spglobal). So a forced
|
|
297
|
+
`Contacts.uuid COLLATE utf8mb4_0900_ai_ci` in a `Client/` fan-out dies on those tenants:
|
|
298
|
+
`ERROR 1253 COLLATION 'utf8mb4_0900_ai_ci' is not valid for CHARACTER SET 'latin1'`. Join
|
|
299
|
+
charset-agnostically instead:
|
|
300
|
+
|
|
301
|
+
```sql
|
|
302
|
+
CONVERT(Contacts.uuid USING utf8mb4) COLLATE utf8mb4_0900_ai_ci = <temp.uuid>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A single-tenant file targeting a **known-utf8mb4** client may keep the plain `COLLATE`.
|
|
306
|
+
|
|
307
|
+
### Backfilling a user→contact link deduped by a client identifier key
|
|
308
|
+
|
|
309
|
+
When users are keyed to a contact by a client identifier (e.g. `c_employeeXid`, `c_sysId`), pick
|
|
310
|
+
**ONE keeper per key** with `ROW_NUMBER()` (prefer primary-email, then name, then oldest id), link
|
|
311
|
+
**all** users in that key group to the keeper, and dedupe residual **creates by key too** — one Contact
|
|
312
|
+
per key group, never one per user when they share a key. Create a Work email only when the user has one;
|
|
313
|
+
a no-email user gets a name-only Contact from its own `firstName`/`lastName`.
|
|
314
|
+
|
|
271
315
|
### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
|
|
272
316
|
|
|
273
317
|
The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
|
|
@@ -366,6 +410,12 @@ concluding a migration is production-safe.
|
|
|
366
410
|
[FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
|
|
367
411
|
|
|
368
412
|
## Change history
|
|
413
|
+
- 2026-09-21 — Added three lessons from the platform-wide `Users.contactId` NOT NULL backfill: a guarded
|
|
414
|
+
`NOT NULL` ALTER (`sql_mode ''` silently coerces leftover NULLs to `0`, so gate on a residual count —
|
|
415
|
+
single-tenant aborts loud, fan-out `DO 0` skips); latin1 schema drift breaks a forced
|
|
416
|
+
`COLLATE utf8mb4_0900_ai_ci` in a `Client/` fan-out (Nychh/Spglobal are latin1) so join via
|
|
417
|
+
`CONVERT(col USING utf8mb4) COLLATE …`; and the deduped user→contact backfill (one keeper per client
|
|
418
|
+
key via ROW_NUMBER, dedupe residual creates by key, name-only Contact for a no-email user). (jcardinal)
|
|
369
419
|
- 2026-09-21 — Narrowed the "reuse one uuid literal across tenants" rule: it holds only when the rows share a
|
|
370
420
|
natural key. An env-specific twin file (guarded on `recordFieldId`, which differs per environment) reusing the
|
|
371
421
|
literal dies on `Duplicate entry … aclfieldpermissions.uuid`. Also added the DELETE-first transaction rehearsal
|
package/knowledge/INDEX.md
CHANGED
|
@@ -14,12 +14,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
15
15
|
- **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
|
-
- **store** (Store) —
|
|
17
|
+
- **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
|
|
18
18
|
- **tools** (Tools) — 18 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
19
19
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
22
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
22
|
+
- **_underscore** (_Underscore) _(framework core)_ — 89 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
23
23
|
- **worker2** (Worker) — 74 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
24
24
|
- **api2** (API) — 26 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
25
25
|
- **dbchanges2** (Database Changes) _(framework core)_ — 19 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -55,6 +55,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
55
55
|
- **Endeavor Health** (`endeavor-health`) → [clients/endeavor-health/INDEX.md](clients/endeavor-health/INDEX.md)
|
|
56
56
|
- **Fordham University** (`fordham`) → [clients/fordham/INDEX.md](clients/fordham/INDEX.md)
|
|
57
57
|
- **GroWrk** (`growrk`) → [clients/growrk/INDEX.md](clients/growrk/INDEX.md)
|
|
58
|
+
- **Managelife** (`managelife`) → [clients/managelife/INDEX.md](clients/managelife/INDEX.md)
|
|
58
59
|
- **Northwell Health** (`northwell`) → [clients/northwell/INDEX.md](clients/northwell/INDEX.md)
|
|
59
60
|
- **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
|
|
60
61
|
- **NYC Health & Hospitals** (`nychh`) → [clients/nychh/INDEX.md](clients/nychh/INDEX.md)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Managelife"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- dbchanges2
|
|
6
|
+
project: Database Changes
|
|
7
|
+
client: managelife
|
|
8
|
+
type: profile
|
|
9
|
+
status: active
|
|
10
|
+
updated: 2026-09-21
|
|
11
|
+
owners: [jcardinal]
|
|
12
|
+
files: []
|
|
13
|
+
related:
|
|
14
|
+
- ../../2.0/apps/_underscore/features/client-user-contactid-required.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Managelife 2.0 platform tenant (`Client_Managelife`). Stub profile — expand as work touches it. Formal client name not yet confirmed.
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
- 2.0 tenant with a `Client_Managelife` database.
|
|
22
|
+
- First recorded work: the platform-wide `Users.contactId` NOT NULL backfill — `Client_Managelife/2026-09-21a` gave its 1 user (id 5, no email) a name-only Contact from its own `firstName`/`lastName`, linked it, then set `contactId` NOT NULL. See [client-user-contactid-required](../../2.0/apps/_underscore/features/client-user-contactid-required.md).
|
package/knowledge/clients/prudential/features/device-information-import-and-contact-linking.md
CHANGED
|
@@ -57,6 +57,11 @@ Resumable, unit-driven. For each `Unit` with `contactId` NULL it queries Service
|
|
|
57
57
|
- **No `continue`** in the helpers added to this cron (team standard); the new backfill file is tab-indented (the legacy cron body is space-indented — only the added helpers were converted).
|
|
58
58
|
- API auth uses the `App_Api_Toga2::CLIENT_UUID_PRUDENTIAL` / `API_UUID_PRUDENTIAL` / `API_SECRET_PRUDENTIAL` constants (names only — values live in config, not here).
|
|
59
59
|
|
|
60
|
+
- **⚠ `Client_Prudential.Users.contactId` is now NOT NULL — and do NOT blind email-create contacts for Prudential.** The `Client_Prudential/2026-09-21a` backfill linked ~14,298 null-contactId users to their `c_employeeXid` **keeper** contact (ROW_NUMBER: primary-email, then name, then oldest id), created a Contact+email for only ~20 residual, then set `contactId` NOT NULL. Prudential's Contacts were just deduped 893k→19.7k, so a blanket email-create backfill would **re-pollute** them — link by XID keeper, never create-per-user. Part of the platform-wide contactId initiative: [client-user-contactid-required](../../../2.0/apps/_underscore/features/client-user-contactid-required.md).
|
|
61
|
+
|
|
62
|
+
## Change history
|
|
63
|
+
- 2026-09-21 — `Client_Prudential.Users.contactId` made NOT NULL after the `2026-09-21a` backfill linked null-contactId users to their XID keeper contact. Noted the "never blind email-create for Prudential" rule (would re-pollute the just-deduped Contacts) and cross-linked the platform contactId initiative. (jcardinal)
|
|
64
|
+
|
|
60
65
|
## Related
|
|
61
66
|
- [Prudential profile](../profile.md)
|
|
62
67
|
- [2.0 api2 — Nested-relationship writes & child matching](../../../2.0/apps/api2/features/nested-relationship-writes.md) — the general api2 behavior this incident exposed.
|
package/package.json
CHANGED