toga-ai 1.0.461 → 1.0.463
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +69 -0
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +13 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/architecture.md +37 -0
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +14 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/architecture.md +18 -0
- package/knowledge/2.0/apps/worker2/features/alb-target-group-auto-registration.md +162 -0
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/prudential/INDEX.md +2 -1
- package/knowledge/clients/prudential/features/dell-asn-units-interceptor.md +26 -2
- package/knowledge/clients/prudential/features/dell-lch-iop-transmissions.md +27 -4
- package/knowledge/clients/prudential/features/service-request-address-validation.md +21 -3
- package/knowledge/clients/prudential/features/service-request-rejection-alert-email.md +94 -0
- package/knowledge/clients/prudential/profile.md +20 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
| [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
|
|
14
14
|
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php |
|
|
15
15
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
16
|
+
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, worker2/Worker/Infrastructure/Email/Send.php |
|
|
16
17
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
17
18
|
| [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
18
19
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 2.0 Email Send Pipeline (queue + Send worker)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-28
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Email.php
|
|
13
|
+
- worker2/Worker/Infrastructure/Email/Send.php
|
|
14
|
+
related:
|
|
15
|
+
- email-template-sending.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
In 2.0, `_Email::send()` **does not transmit** — it queues the message. It writes a `PENDING`
|
|
21
|
+
row into `Logs_[Client].Email` (the `body` column is `mediumtext`) and stores any attachments as
|
|
22
|
+
BLOBs in `Logs_[Client].EmailAttachment`, then returns. A separate cron worker action,
|
|
23
|
+
`Infrastructure/Email/Send` (`_Worker_Infrastructure_Email_Send::Run`), scheduled in
|
|
24
|
+
`Core.CronJobs` at `* * * * *` (every minute), actually sends the `PENDING` rows via **AWS SES
|
|
25
|
+
SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 email lands within
|
|
26
|
+
~1 minute of the `send()` call — **not instantly**.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
|
|
30
|
+
- `_underscore/Email.php` — `_Email::send()` builds/validates the PHPMailer message, writes the
|
|
31
|
+
`PENDING` `Logs_[Client].Email` row (+ attachment BLOBs), and enqueues the send. It **throws**
|
|
32
|
+
unless both `setClientIdentifier(...)` and a From address (`fromEmailAddress`) are set. The
|
|
33
|
+
`Logs_[Client]` databases live on a separate cluster (`production-logs-*`), so `send()` points
|
|
34
|
+
`DB_CLIENT_LOGS` at the correct `Logs_[Client]` schema before inserting.
|
|
35
|
+
- `worker2/Worker/Infrastructure/Email/Send.php` — `Run()` fetches up to 100 `PENDING` rows for
|
|
36
|
+
the client, and `sendEmail()` transmits each via SES SMTP. Constants: `STATUS_PENDING`,
|
|
37
|
+
`STATUS_SENT`, `STATUS_FAILED`, `MAX_RETRIES`.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
1. A caller (model, interceptor, worker) builds `_Email`, sets client identifier + From, adds
|
|
42
|
+
recipients/subject/body, and calls `send()`.
|
|
43
|
+
2. `send()` inserts a `PENDING` `Logs_[Client].Email` row (attachments → `EmailAttachment` BLOBs)
|
|
44
|
+
and returns — nothing is transmitted yet.
|
|
45
|
+
3. Every minute, `Infrastructure/Email/Send::Run` selects up to 100 `PENDING` rows and sends each
|
|
46
|
+
through PHPMailer over SES SMTP.
|
|
47
|
+
4. On success the row flips to `SENT`; on failure `retryCount` is incremented and the row is
|
|
48
|
+
retried on subsequent runs until `MAX_RETRIES` (3), after which it is marked `FAILED`.
|
|
49
|
+
|
|
50
|
+
## Gotchas / known issues
|
|
51
|
+
|
|
52
|
+
- **~1-minute latency, not instant.** Anything that assumes an email is sent synchronously at the
|
|
53
|
+
`send()` call is wrong — transmission happens on the next Send-worker tick.
|
|
54
|
+
- **The Send worker forces HTML mode.** `sendEmail()` calls `$mailer->IsHTML(true)`
|
|
55
|
+
**unconditionally**, ignoring the sender's `setIsHtml(false)`. A caller that queued a
|
|
56
|
+
"plain-text" message is still transmitted HTML-mode. Author bodies accordingly.
|
|
57
|
+
- **⚠ SECURITY — hardcoded AWS SES SMTP credentials in `_underscore/Email.php`.** The
|
|
58
|
+
`SMTP_USERNAME` / `SMTP_PASSWORD` class constants hold **real** SES SMTP credentials committed
|
|
59
|
+
in source, violating the no-secrets-in-code rule. Documenting the **location only** — do not
|
|
60
|
+
copy the values. Remediation: treat as compromised, rotate in AWS SES, and move them into
|
|
61
|
+
`Config/[environment].ini` (read via `_Config`). Also tracked on
|
|
62
|
+
[`email-template-sending.md`](email-template-sending.md).
|
|
63
|
+
|
|
64
|
+
## Change history
|
|
65
|
+
- 2026-07-28 — Created: documented that `_Email::send()` **queues** (writes a `PENDING`
|
|
66
|
+
`Logs_[Client].Email` row + attachment BLOBs) rather than transmitting, and that the worker2
|
|
67
|
+
`Infrastructure/Email/Send` cron (`Core.CronJobs` `* * * * *`) does the actual SES SMTP send
|
|
68
|
+
with `MAX_RETRIES` (3) → `SENT`/`FAILED`, giving ~1-minute delivery latency. Noted the Send
|
|
69
|
+
worker's unconditional `IsHTML(true)` override and the hardcoded SES SMTP creds location. (bala)
|
|
@@ -6,13 +6,14 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
13
13
|
- _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
|
|
14
14
|
- _underscore/Email.php
|
|
15
15
|
related:
|
|
16
|
+
- email-send-pipeline.md
|
|
16
17
|
- ../../worker2/features/notification-email.md
|
|
17
18
|
- ../../worker2/features/notification-email-template.md
|
|
18
19
|
---
|
|
@@ -86,6 +87,13 @@ can keep using `sendEmail($api, ...)`.
|
|
|
86
87
|
|
|
87
88
|
## Gotchas / known issues
|
|
88
89
|
|
|
90
|
+
- **`_Email::send()` QUEUES, it does not directly transmit.** `send()` writes a `PENDING` row into
|
|
91
|
+
`Logs_[Client].Email` (+ attachment BLOBs) and returns; the actual PHPMailer/SES SMTP send,
|
|
92
|
+
retries, and the `SENT`/`FAILED` outcome happen ~1 minute later in the worker2
|
|
93
|
+
`Infrastructure/Email/Send` cron. So an email is **not** sent synchronously at the `send()` call,
|
|
94
|
+
and the "throws when `PHPMailer::Send()` returns false" behavior described under *Failure
|
|
95
|
+
surfacing* below reflects the pre-queue path — final delivery success/failure is now decided by
|
|
96
|
+
the Send worker. See [`email-send-pipeline.md`](email-send-pipeline.md) for the full pipeline.
|
|
89
97
|
- **Use `send()` from any non-API context (workers, cron, internal code).** Before
|
|
90
98
|
2026-06-15 the only entry point was `sendEmail(&$api, ...)`, so callers with no API
|
|
91
99
|
context faked one: `$api = (object)['client' => (object)['clientIdentifier' => …]]`.
|
|
@@ -138,6 +146,10 @@ worker method) in-process instead.
|
|
|
138
146
|
|
|
139
147
|
## Change history
|
|
140
148
|
|
|
149
|
+
- 2026-07-28 — Clarified that `_Email::send()` **queues** a `PENDING` `Logs_[Client].Email` row
|
|
150
|
+
rather than transmitting; the actual SES SMTP send/retry/`SENT`/`FAILED` happens in the worker2
|
|
151
|
+
`Infrastructure/Email/Send` cron (~1-min latency). Added the new
|
|
152
|
+
[`email-send-pipeline.md`](email-send-pipeline.md) feature doc and cross-linked it. (bala)
|
|
141
153
|
- 2026-07-16 — Recorded the **hardcoded AWS SES SMTP credentials** security gotcha in
|
|
142
154
|
`_Email` (`SMTP_USERNAME`/`SMTP_PASSWORD` constants; pre-existing — rotate + move to
|
|
143
155
|
`Config`). Surfaced while fixing a production 500 (EO-1) whose root cause was a single
|
|
@@ -18,5 +18,5 @@
|
|
|
18
18
|
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql |
|
|
19
19
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
20
20
|
| [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TrackingNumber.php |
|
|
21
|
-
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. |
|
|
21
|
+
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
|
|
22
22
|
| [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, dbchanges2/Logs/, _underscore/Route.php |
|
|
@@ -107,6 +107,15 @@ One ~2,000-line `execute()` then `processRoutePairs()`:
|
|
|
107
107
|
5. **Transaction logging** — every request logged (to client/core Logs DB, or as a JSONL
|
|
108
108
|
line shipped by CloudWatch when `[api] log_filepath` is set).
|
|
109
109
|
|
|
110
|
+
> **⚠ Auto-generated `Api.transactionId` collides under concurrency → 1062 → HTTP 500.**
|
|
111
|
+
> Separate from the *client-supplied* `transactionId` uniqueness check (EV-5, above): the inbound
|
|
112
|
+
> request-logger inserts its `Api` log row with a UNIQUE `transactionId` set to a
|
|
113
|
+
> millisecond-precision timestamp (`Y-m-d H:i:s.v`). Concurrent nested writes generated within the
|
|
114
|
+
> same millisecond collide on that UNIQUE key → MySQL **1062** → **HTTP 500**. This breaks ingestion
|
|
115
|
+
> for high-volume senders (seen on the Compass/Veyer ASN feed, `sourceIp 34.232.23.158`) and is
|
|
116
|
+
> platform-wide. Fix direction: make the logged `transactionId` unique-enough (uuid) or
|
|
117
|
+
> retry-on-1062. Until fixed, a burst of concurrent posts can intermittently 500 with no app-level cause.
|
|
118
|
+
|
|
110
119
|
## CRUD engine — `processRoutePairs()`
|
|
111
120
|
|
|
112
121
|
**Metadata-driven** — routes/models/fields/permissions come from Core/Client DB tables, not
|
|
@@ -154,6 +163,13 @@ region-aware DB host selection; CloudWatch agent ships the JSONL log file;
|
|
|
154
163
|
`long_gateway_timeout.conf` sets `ProxyTimeout 1800`/`Timeout 1800` — **the fix for the
|
|
155
164
|
"exactly 60 second" 504** (EB Apache→PHP-FPM defaults to 60s); `enforce_https.conf`.
|
|
156
165
|
|
|
166
|
+
`.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh` +
|
|
167
|
+
`ebs/register_instance_to_shared_application_load_balancer.php` register a **non-production**
|
|
168
|
+
instance into the ALB target group whose name equals the EB environment name (production skipped;
|
|
169
|
+
always exits 0). See
|
|
170
|
+
[the worker2 reference implementation](../worker2/features/alb-target-group-auto-registration.md) —
|
|
171
|
+
api2's copy is the **unhardened original** and must be brought up to it.
|
|
172
|
+
|
|
157
173
|
## CI — commit message policy
|
|
158
174
|
|
|
159
175
|
`.github/workflows/true-devteam-requirements.yml` enforces **`TRUE-{ticket}: {Subject}`** —
|
|
@@ -171,6 +187,18 @@ build. These must be **rotated** (treat the committed tokens as compromised) and
|
|
|
171
187
|
Parameter Store / EB env properties. **Flag this if you touch config or deploy.** (Location +
|
|
172
188
|
remediation only — do not record the token value anywhere.)
|
|
173
189
|
|
|
190
|
+
**Committed IAM access key (found 2026-07-28).** An IAM access key id + secret access key are
|
|
191
|
+
hardcoded as PHP constants in `ebs/register_instance_to_shared_application_load_balancer.php`
|
|
192
|
+
(**lines 29–30**) and are in **committed git history** (`git log -S`; commit subject
|
|
193
|
+
*"Auto-registering to target groups"*) — exposed to every reader of the repo, every clone, and every
|
|
194
|
+
EB bundle. **Remediation:** deactivate + delete the key in IAM, audit CloudTrail for its use, and
|
|
195
|
+
replace the block with the instance-profile pattern worker2 now uses. **Rotation, not history
|
|
196
|
+
rewrite, closes the exposure**; a history rewrite is a separate sign-off-required decision because
|
|
197
|
+
the team git rule forbids force-pushing `_main` in application repos. The same file also passes the
|
|
198
|
+
IMDSv2 token on the `curl` command line (visible in `ps`/`/proc/<pid>/cmdline`, 6h TTL) and falls
|
|
199
|
+
back silently to IMDSv1 — fix both alongside the key. *(Location + remediation only; no key material
|
|
200
|
+
is recorded anywhere.)*
|
|
201
|
+
|
|
174
202
|
## Known issues / accepted risks
|
|
175
203
|
|
|
176
204
|
Open items a maintainer should know before changing this tier. None are "bugs to fix right now" —
|
|
@@ -204,6 +232,13 @@ they are the known sharp edges. Do not re-discover these from scratch.
|
|
|
204
232
|
8. **JWT signing-secret rotation accepts current + previous.** During the overlap window a token
|
|
205
233
|
signed with the retired secret still validates. Revocation is therefore not immediate —
|
|
206
234
|
don't rely on rotation alone to lock out a compromised token.
|
|
235
|
+
9. **Committed IAM access key in the ALB-registration script, not yet rotated (found 2026-07-28).**
|
|
236
|
+
`ebs/register_instance_to_shared_application_load_balancer.php` lines 29–30, present in committed
|
|
237
|
+
git history. Treat as compromised: deactivate + delete the key, audit CloudTrail, and move to the
|
|
238
|
+
EC2 instance profile as
|
|
239
|
+
[worker2 does](../worker2/features/alb-target-group-auto-registration.md). The same script's
|
|
240
|
+
IMDSv2 handling (token on the `curl` command line, silent IMDSv1 fallback) must be fixed with it.
|
|
241
|
+
See the Security note above.
|
|
207
242
|
|
|
208
243
|
## When making changes here
|
|
209
244
|
|
|
@@ -221,6 +256,8 @@ they are the known sharp edges. Do not re-discover these from scratch.
|
|
|
221
256
|
or `execute()`.
|
|
222
257
|
|
|
223
258
|
## Change history
|
|
259
|
+
- 2026-07-28 — Documented the previously unrecorded `060_register_instance_to_shared_application_load_balancer` postdeploy hook pair in the Deployment section (non-prod self-registration into the same-named ALB target group; production skipped), and recorded a **committed IAM access key** in `ebs/register_instance_to_shared_application_load_balancer.php` as a security note + Known issue #9 — location, line range, commit subject, and remediation only (rotate, audit CloudTrail, move to the instance profile; history rewrite is a separate sign-off). Flagged api2's copy as the unhardened original vs. the new worker2 reference implementation. (jcardinal)
|
|
224
260
|
- 2026-07-28 — Added a consolidated **Known issues / accepted risks** section (8 items), absorbing the previously free-floating deferred raw-exception-disclosure follow-up as item 1, so the tier's sharp edges (unrotated committed secrets, pre-execute phase still outside the main guard, local Logs DB name mismatch, permissive CORS, unpinned `_underscore` build clone, untested `V2.php` monolith, JWT rotation overlap window) are in one place instead of scattered. Recorded that `DB_CACHE` is resolved by name (`Databases.name = 'Cache'`), never by a hardcoded id, which differs per Core instance. (jcardinal)
|
|
261
|
+
- 2026-07-28 — Added gotcha: the request-logger's auto-generated `Api.transactionId` (millisecond timestamp `Y-m-d H:i:s.v`, UNIQUE) collides under concurrent same-millisecond nested writes → MySQL 1062 → HTTP 500; platform-wide, observed on the Compass/Veyer ASN feed (`sourceIp 34.232.23.158`). Distinct from the client-supplied `transactionId`/EV-5 uniqueness contract. Fix direction: uuid the logged id or retry-on-1062. (bala)
|
|
225
262
|
- 2026-07-27 — Sharpened the committed-secret note: the plaintext GitHub PAT lives in the per-env **`.ebextensions/git.*.json`** files (used by the `prebuild/git.sh` clone hook to pull `_underscore`), must be rotated and moved to SSM / EB env properties (location + remediation only, no value). (mhammontree)
|
|
226
263
|
- 2026-07-23 — Documented the now-guarded Core/Logs DB bootstrap in the front controller: the pre-execute block runs before the `execute()` try/catch, the Core Logs schema name is resolved from a `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`) so a name-mismatched local Logs DB reads as missing, and the failure is now wrapped in `try/catch (\Throwable)` returning `INVALID_CONFIGURATION` + Sentry instead of a fatal (guarded no-op rollback, `Database.php:219–226`). Added the deferred raw-getMessage/getTrace client-disclosure follow-up to the Security note. (jcardinal)
|
|
@@ -6,9 +6,11 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: ["jcardinal", "mhammontree"]
|
|
11
|
-
files:
|
|
11
|
+
files:
|
|
12
|
+
- api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
13
|
+
- api2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
12
14
|
related: []
|
|
13
15
|
---
|
|
14
16
|
|
|
@@ -106,6 +108,15 @@ debugging the code. (Seen 2026-06-18 chasing a "missing FPDF / old code still ru
|
|
|
106
108
|
beta that was really the LB still pointing at a terminated instance's replacement that was never
|
|
107
109
|
registered.)
|
|
108
110
|
|
|
111
|
+
**Automated for non-production tiers that carry the `060` hook.** `api2` and (as of 2026-07-28)
|
|
112
|
+
`worker2` ship a postdeploy hook pair that makes a **non-production** instance register *itself*
|
|
113
|
+
into the target group whose name equals the EB environment name — so on those tiers the manual
|
|
114
|
+
step above is only needed if the hook is absent, misnamed, or silently failed (it always exits 0).
|
|
115
|
+
Production is deliberately skipped and still registers by the existing process. See
|
|
116
|
+
[deploy-time auto-registration to the shared ALB target group](../../worker2/features/alb-target-group-auto-registration.md),
|
|
117
|
+
which is also the reference implementation — **api2's copy has known security defects**, including
|
|
118
|
+
a committed IAM access key.
|
|
119
|
+
|
|
109
120
|
### On-instance composer install (when a dep is missing post-deploy)
|
|
110
121
|
If a Composer dep is missing on the running instance (e.g. `Class "FPDF" not found` because
|
|
111
122
|
`composer.lock` wasn't committed), you can install it on the box over SSH/PuTTY — but
|
|
@@ -128,6 +139,7 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
|
|
|
128
139
|
```
|
|
129
140
|
|
|
130
141
|
## Change history
|
|
142
|
+
- 2026-07-28 — Manual LB re-registration is now automated on **non-production** tiers carrying the `.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh` hook pair (api2 already had it; ported to worker2 this session). Linked the new worker2 feature doc as the reference implementation and flagged that api2's copy has known security defects. (jcardinal)
|
|
131
143
|
- 2026-07-27 — Noted that not every EB env pulls `_underscore` from its `_<env>` branch: **`API-Sandbox-Dev`** re-clones `_underscore` from the branch in `.ebextensions/git.sandbox-dev.json` (**`_sandbox-dev`**), overwriting the pipeline copy — so a `_beta` merge doesn't reach it. Added the `Logs_<Client>.Api` server-side request-log note as the reliable way to debug a beta env without Sentry (TRUE-79533). (mhammontree)
|
|
132
144
|
- 2026-07-14 — Documented the branch model: app repos (`_underscore`/`api2`/`toga2-supply`) deploy from long-lived `_beta`/`_production` branches (not `_main`); api2 pulls `_underscore`'s `_<env>` branch at EB build; `dbchanges2` has only `_main` and its migrations are applied per-env by the team process (not by a code deploy). (mhammontree)
|
|
133
145
|
- 2026-06-18 — Added two EB-instance gotchas surfaced during the TOGa Supply beta/prod label deploys: (1) terminated instances must be **manually re-registered** as LB targets (we don't pay for auto-registration) — until then the new code never serves traffic and looks like deploy-lag; (2) on-instance `composer require` stopgap syntax (no space after the colon, fix root cause by committing `composer.lock`). (mhammontree)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Worker (worker2) Architecture](architecture.md) | Worker (repo `worker2`) is an AWS Elastic Beanstalk **Worker Tier** application that processes background jobs. | worker2/Controller/Index.php, worker2/Worker/, worker2/LambdaFunctions/, _underscore/Worker.php |
|
|
6
|
+
| [Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)](features/alb-target-group-auto-registration.md) | TOGA does **not** pay for EB-managed load-balancer registration, so an EB instance is normally **not** added to its environment's ALB target group — a fresh or | worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, worker2/ebs/register_instance_to_shared_application_load_balancer.php, worker2/.platform/hooks/prebuild/_shared/040-write-instance-id.sh, worker2/.platform/hooks/prebuild/_shared/041-write-region.sh, worker2/.platform/hooks/postdeploy/015_install_composer.sh, api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php |
|
|
6
7
|
| [Automated PR Merger — Concurrent Force-Push Clobber Race](features/automated-pr-merger-force-push-race.md) | The automated PR merger `_Worker_Team_GitHub::Merge` (`worker2` `Worker/Team/Github.php`) merges approved PRs to `_production` by **force-pushing from a clone t | Worker/Team/Github.php |
|
|
7
8
|
| [ClickUp Connectivity Watchdog](features/clickup-connectivity-watchdog.md) | A cron watchdog that emails when the ClickUp integration looks disconnected during business hours. | worker2/Worker/Clickup/Health.php, worker2/Database/ClickupHealthWatchdog.sql |
|
|
8
9
|
| [ClickUp Design Sprint Automation (Final Design Outcome)](features/clickup-design-sprint-automation.md) | `_Worker_Clickup_Design` is meant to drive the design-sprint workflow in ClickUp via the API, replacing a set of native ClickUp automations. | worker2/Worker/Clickup/Design.php, worker2/Worker/Clickup.php, worker2/Controller/ClickupDesignTest.php, _underscore/Component/Api/Clickup/Clickup.php |
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- _underscore/Worker.php
|
|
16
16
|
related:
|
|
17
17
|
- ./features/creating-worker-actions.md
|
|
18
|
+
- ./features/alb-target-group-auto-registration.md
|
|
18
19
|
- ../_underscore/features/async-query-execution.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -31,6 +32,18 @@ processes background jobs. It's a `_underscore` 2.0 app (`index.php` is just
|
|
|
31
32
|
**MySQL is the source of truth; SQS is delivery only.** All job state lives in
|
|
32
33
|
`Core.WorkerJobs`. Every worker invocation reads from that table and writes its result back.
|
|
33
34
|
|
|
35
|
+
**Production vs. non-production inbound differ.** The SQS/Lambda job pipeline above describes
|
|
36
|
+
**production**. **Non-production worker2 environments do not incorporate SQS at all** — they exist
|
|
37
|
+
for **manual invocation over HTTP** (hence `index.php` dispatch and `.platform/httpd/conf.d/`, and
|
|
38
|
+
no `cron.yaml`). That is precisely why non-prod instances must be reachable through the shared ALB;
|
|
39
|
+
see [deploy-time auto-registration to the shared ALB target group](./features/alb-target-group-auto-registration.md).
|
|
40
|
+
SQS may be added to non-prod later, but is not planned. **Do not assume a non-prod worker2 job ever
|
|
41
|
+
passes through SQS** — a queue-based reproduction of a prod issue will not work there.
|
|
42
|
+
|
|
43
|
+
**Associated open risk:** because non-prod is HTTP-invoked and now ALB-reachable, the shared-ALB
|
|
44
|
+
listener rules and security groups must restrict non-prod to internal/VPN sources, and the
|
|
45
|
+
HTTP-triggered job endpoints must enforce auth.
|
|
46
|
+
|
|
34
47
|
## AWS infrastructure
|
|
35
48
|
|
|
36
49
|
| Component | Notes |
|
|
@@ -260,6 +273,11 @@ the MySQL-first design was departed from **on purpose** here.
|
|
|
260
273
|
|
|
261
274
|
## Change history
|
|
262
275
|
|
|
276
|
+
- 2026-07-28 — Corrected the inbound model: **non-production** worker2 environments are not
|
|
277
|
+
SQS-driven at all; they are manually invoked over HTTP (no `cron.yaml`; `index.php` +
|
|
278
|
+
`.platform/httpd/conf.d/`), which is why they must be reachable through the shared ALB.
|
|
279
|
+
Production remains SQS-driven. Recorded the resulting non-prod exposure risk (listener/SG
|
|
280
|
+
restriction + endpoint auth). (jcardinal)
|
|
263
281
|
- 2026-07-28 — Added **Cron actions must return a result on BOTH paths**: a new cron action is
|
|
264
282
|
expected to return a structured result for a success as well as a failure, because both are
|
|
265
283
|
recorded against the job and the successful results are what auditing reads (a completed run that
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Deploy-Time Auto-Registration to the Shared ALB Target Group (non-production)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-28
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
13
|
+
- worker2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
14
|
+
- worker2/.platform/hooks/prebuild/_shared/040-write-instance-id.sh
|
|
15
|
+
- worker2/.platform/hooks/prebuild/_shared/041-write-region.sh
|
|
16
|
+
- worker2/.platform/hooks/postdeploy/015_install_composer.sh
|
|
17
|
+
- api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
18
|
+
- api2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
19
|
+
related:
|
|
20
|
+
- ../architecture.md
|
|
21
|
+
- ../../api2/workflows/codepipeline-codeconnections-deploy.md
|
|
22
|
+
- ../../api2/architecture.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
TOGA does **not** pay for EB-managed load-balancer registration, so an EB instance is normally
|
|
28
|
+
**not** added to its environment's ALB target group — a fresh or replacement instance serves no
|
|
29
|
+
traffic until someone registers it by hand (see the manual-registration gotcha in the
|
|
30
|
+
[api2 deploy workflow](../../api2/workflows/codepipeline-codeconnections-deploy.md)). This hook
|
|
31
|
+
pair automates that step for **non-production** environments: on every deploy the instance
|
|
32
|
+
registers **itself** into the target group whose **name exactly equals the EB environment name**.
|
|
33
|
+
|
|
34
|
+
Originally built in `api2`; ported to `worker2` on 2026-07-28 with three security hardenings
|
|
35
|
+
(instance-profile credentials, native-PHP IMDSv2, fail-closed token handling). **`worker2` is now
|
|
36
|
+
the reference implementation — `api2`'s copy has known defects, see below.**
|
|
37
|
+
|
|
38
|
+
Non-prod `worker2` environments need this because they are reached **over HTTP for manual job
|
|
39
|
+
invocation**, not through SQS.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. **`.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh`**
|
|
44
|
+
- Reads `get-config environment -k ENVIRONMENT`; if it is `production`, **skips** entirely.
|
|
45
|
+
Production instances are registered by the existing production process, not by this hook.
|
|
46
|
+
- Otherwise invokes `ebs/register_instance_to_shared_application_load_balancer.php`.
|
|
47
|
+
- **Always `exit 0`.** A registration failure must never brick a deploy.
|
|
48
|
+
2. **`ebs/register_instance_to_shared_application_load_balancer.php`**
|
|
49
|
+
- Reads the instance id and region from
|
|
50
|
+
`/var/app/current/storage/instance-id.txt` and `storage/region.txt` (written earlier by the
|
|
51
|
+
`_shared/040-write-instance-id.sh` / `041-write-region.sh` prebuild hooks), falling back to a
|
|
52
|
+
direct **IMDSv2** lookup.
|
|
53
|
+
- Reads the EB environment name via `get-config container -k environment_name`.
|
|
54
|
+
- Uses the AWS SDK (`aws/aws-sdk-php`) ELBv2 client to `DescribeTargetGroups` and selects the
|
|
55
|
+
target group whose **`TargetGroupName` is an exact string match** for the environment name —
|
|
56
|
+
this naming equality *is* the wiring convention; there is no tag or config lookup.
|
|
57
|
+
- Calls `RegisterTargets` with this instance id.
|
|
58
|
+
|
|
59
|
+
### Hook ordering is load-bearing — why `060`
|
|
60
|
+
|
|
61
|
+
`060` sorts **after** everything it depends on:
|
|
62
|
+
|
|
63
|
+
| Hook | Provides |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `prebuild/_shared/040-write-instance-id.sh` | `storage/instance-id.txt` |
|
|
66
|
+
| `prebuild/_shared/041-write-region.sh` | `storage/region.txt` |
|
|
67
|
+
| `postdeploy/015_install_composer.sh` | `vendor/` (the AWS SDK) |
|
|
68
|
+
|
|
69
|
+
Renumber it below `015` and the SDK autoloader does not exist yet.
|
|
70
|
+
|
|
71
|
+
### Porting prerequisites (all already satisfied in worker2)
|
|
72
|
+
|
|
73
|
+
The port needed **no adaptation** because `worker2` already had: `aws/aws-sdk-php` in
|
|
74
|
+
`composer.json`; **byte-identical** `_shared/040-write-instance-id.sh` and `041-write-region.sh`;
|
|
75
|
+
an identical `015_install_composer.sh`; and the same `get-config environment -k ENVIRONMENT`
|
|
76
|
+
convention already used by `.ebextensions/git.php`. Check these four before porting the pair to
|
|
77
|
+
any other 2.0 tier.
|
|
78
|
+
|
|
79
|
+
## Credentials — instance profile only (decision, 2026-07-28)
|
|
80
|
+
|
|
81
|
+
**Deploy-time AWS credentials come from the EC2 instance profile. Never from source, never from
|
|
82
|
+
EB environment properties.** The `worker2` version passes **no `credentials`** to the SDK client
|
|
83
|
+
so the default provider chain resolves the instance profile.
|
|
84
|
+
|
|
85
|
+
**Why EB environment properties were rejected as a fallback:** they are not a secret store. They
|
|
86
|
+
are returned by `elasticbeanstalk:DescribeConfigurationSettings`, displayed in plaintext in the EB
|
|
87
|
+
console and `eb config`, and exported into the process environment — so any `var_dump($_ENV)`,
|
|
88
|
+
`phpinfo()`, or debug handler leaks them. If a static key is ever genuinely unavoidable, fetch it
|
|
89
|
+
at runtime from **SSM Parameter Store SecureString** or Secrets Manager.
|
|
90
|
+
|
|
91
|
+
### Required IAM on the EB EC2 instance profile — exactly two actions
|
|
92
|
+
|
|
93
|
+
- **`elasticloadbalancing:DescribeTargetGroups`** — `Resource: "*"` is unavoidable (ELBv2
|
|
94
|
+
`Describe*` has no resource-level permissions). Constrain it with a condition on
|
|
95
|
+
`aws:RequestedRegion`.
|
|
96
|
+
- **`elasticloadbalancing:RegisterTargets`** — scope to the **non-production target-group ARN
|
|
97
|
+
pattern**.
|
|
98
|
+
|
|
99
|
+
> **Do not use `targetgroup/*/*`.** On a *shared* ALB that lets any instance carrying this profile
|
|
100
|
+
> insert any instance into any target group — a traffic-hijack primitive.
|
|
101
|
+
|
|
102
|
+
Do **not** grant `DeregisterTargets`, any `Modify*`, or `elasticloadbalancing:*`.
|
|
103
|
+
|
|
104
|
+
## IMDSv2 must not be fetched by shelling out to `curl`
|
|
105
|
+
|
|
106
|
+
Passing the IMDSv2 session token as a **`curl` command-line argument** exposes it in
|
|
107
|
+
`/proc/<pid>/cmdline` and in `ps` output to every other process on the instance. The `worker2`
|
|
108
|
+
implementation instead:
|
|
109
|
+
|
|
110
|
+
- uses a **native PHP stream context** (`stream_context_create` + `file_get_contents`) so the
|
|
111
|
+
token never enters `argv`;
|
|
112
|
+
- sets the token TTL to **60s instead of 21600s (6h)** — this is a one-shot, sub-second fetch;
|
|
113
|
+
- **fails closed**: if no token can be obtained it aborts, rather than falling back to an
|
|
114
|
+
unauthenticated IMDSv1 request (a silent downgrade on any instance with `HttpTokens=optional`).
|
|
115
|
+
|
|
116
|
+
## Gotchas
|
|
117
|
+
|
|
118
|
+
- **Production is skipped by design.** Adding a prod environment to this hook is a separate
|
|
119
|
+
decision, not a config tweak.
|
|
120
|
+
- **Naming equality is the contract.** If the target group is not named *exactly* the EB
|
|
121
|
+
environment name, the hook silently registers nothing (and still exits 0). Check the target
|
|
122
|
+
group name first when a non-prod env is unreachable after a deploy.
|
|
123
|
+
- **Exit 0 hides failures.** Registration problems will not show in deploy status — read
|
|
124
|
+
`/var/log/eb-hooks.log` on the instance.
|
|
125
|
+
- **The hook turns on reachability; it does not secure it.** The target group and listener already
|
|
126
|
+
exist by convention, but because this hook is what actually puts non-prod instances behind the
|
|
127
|
+
shared ALB, the **listener rules and security groups must restrict non-prod to internal/VPN
|
|
128
|
+
sources**, and the HTTP-triggered job endpoints must enforce authentication. Open risk, tracked
|
|
129
|
+
here deliberately.
|
|
130
|
+
|
|
131
|
+
## api2 divergence — three defects to fix (do not copy the api2 original)
|
|
132
|
+
|
|
133
|
+
`api2` carries the same hook pair under the same two filenames. Its version predates the
|
|
134
|
+
hardening above and has:
|
|
135
|
+
|
|
136
|
+
1. **Committed IAM access key — security incident (2026-07-28).** An IAM access key id and secret
|
|
137
|
+
access key are hardcoded as PHP constants in
|
|
138
|
+
`api2/ebs/register_instance_to_shared_application_load_balancer.php` (**lines 29–30**) and are
|
|
139
|
+
present in **committed git history** (found with `git log -S`; commit subject *"Auto-registering
|
|
140
|
+
to target groups"*). They are exposed to anyone with read access to `api2`, to every clone, and
|
|
141
|
+
to every EB application bundle built from the repo.
|
|
142
|
+
**Remediation:** (a) deactivate then delete the key in IAM — rotation, not history rewrite, is
|
|
143
|
+
what closes the exposure; (b) audit CloudTrail for its use; (c) replace the credential block
|
|
144
|
+
with the instance-profile pattern above. **Rewriting git history is a separate, sign-off-required
|
|
145
|
+
decision** — the team git rule forbids force-pushing `_main` in application repos.
|
|
146
|
+
*(Location + remediation only. Never record the key id, the secret, or any fragment.)*
|
|
147
|
+
2. **IMDSv2 token passed on the `curl` command line** — visible in `ps` / `/proc/<pid>/cmdline`,
|
|
148
|
+
with a 6-hour TTL.
|
|
149
|
+
3. **Silent IMDSv1 fallback** when no token is obtained — downgrades below the IMDSv2 posture.
|
|
150
|
+
|
|
151
|
+
This hook pair is also **not yet described in the api2 architecture doc's Deployment section**,
|
|
152
|
+
which lists the other hooks but not `060` — pending an elevated-doc update.
|
|
153
|
+
|
|
154
|
+
## Change history
|
|
155
|
+
|
|
156
|
+
- 2026-07-28 — Ported the hook pair from `api2` into `worker2` (new files `060_….sh` +
|
|
157
|
+
`ebs/register_instance_to_shared_application_load_balancer.php`) so non-production worker2
|
|
158
|
+
environments are reachable through the shared ALB for manual HTTP invocation. Hardened vs. the
|
|
159
|
+
api2 original: instance-profile credentials only (EB env properties explicitly rejected), native
|
|
160
|
+
PHP stream-context IMDSv2 with a 60s TTL and fail-closed behaviour, and a two-action least-privilege
|
|
161
|
+
IAM policy. Recorded the api2 committed-IAM-key incident (location + remediation only) and the
|
|
162
|
+
non-prod ALB exposure risk. (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,8 +17,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 40 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
|
+
- **worker2** (Worker) — 34 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 18 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Prudential: Dell ASN units PRE/POST interceptor (legacy key + flat tracking)](features/dell-asn-units-interceptor.md) | 2.0 | After the tracking-number bridge migration, the ASN unit route was renamed (`advance-shipping-notice-units` → `advance-shipping-notice-item-units`), so the inhe | _underscore/Model/Prudential/AdvanceShippingNotice.php, dbchanges2/Client_Prudential/2026-06-10 - AsnUnitsInterceptor.sql |
|
|
6
|
-
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. | library/app/api/delllch.php, library/app/apitransaction.php, worker/crons/toga2/prudential/transmissions_to_dell_usa.php, worker/crons/toga2/prudential/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential/transmissions_to_dell_india.php |
|
|
6
|
+
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. | library/app/api/delllch.php, library/app/apitransaction.php, worker/crons/toga2/prudential/transmissions_to_dell_usa.php, worker/crons/toga2/prudential/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential/transmissions_to_dell_india.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php |
|
|
7
7
|
| [Prudential: Device information import + unit→contact linking (import_device_information.php)](features/device-information-import-and-contact-linking.md) | 1.0 | Prudential's **device-sync** cron (`worker/crons/toga2/prudential/import_device_information.php`) pulls device/asset records (from ServiceNow / the Dell CMDB fe | worker/crons/toga2/prudential/import_device_information.php, worker/crons/toga2/prudential/backfill_unit_contacts.php, dbchanges2/Client_Prudential/2026-07-07 - Contact Dedup Merge.sql |
|
|
8
8
|
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer | _underscore/Model/Prudential/ServiceRequest.php, _underscore/Test/Prudential/ServiceRequestTest.php |
|
|
9
|
+
| [Prudential: Service Request rejection alert email](features/service-request-rejection-alert-email.md) | 2.0 | When a Prudential ServiceNow→TOGa service-request submission (`POST /v2/service-requests`) is **rejected by validation**, TOGa now sends a real-time internal al | worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php, _underscore/Model/Prudential/ServiceRequest.php |
|
|
9
10
|
| [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/notifications/reports/prudential_exception_report.php |
|
|
10
11
|
| [Prudential Financial](profile.md) | 2.0 | Prudential is a TOGA client whose device-fulfillment flow is driven by **Dell** via the Dell API (`Client_Prudential.Apis.id = 2`). | |
|
|
11
12
|
| [Prudential: Dell ASN failed POST backfill replay](workflows/dell-asn-backfill-replay.md) | 2.0 | When Dell ASN POSTs fail in bulk (e.g. | |
|
|
@@ -5,13 +5,14 @@ project: _Underscore
|
|
|
5
5
|
client: prudential
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
9
|
-
owners: ["jcardinal", "rgirish"]
|
|
8
|
+
updated: 2026-07-28
|
|
9
|
+
owners: ["jcardinal", "rgirish", "bala"]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Prudential/AdvanceShippingNotice.php
|
|
12
12
|
- dbchanges2/Client_Prudential/2026-06-10 - AsnUnitsInterceptor.sql
|
|
13
13
|
related:
|
|
14
14
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
15
|
+
- transmit-ordershipped-email.md
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
## Summary
|
|
@@ -60,6 +61,24 @@ UPDATE Core.RecordFields SET childPolicy = 'MATCH_CREATE' WHERE id = 2198;
|
|
|
60
61
|
found. `MATCH` (the default) errors when no row exists — that was the root cause of the Jun 2026
|
|
61
62
|
`EV-12` outage.
|
|
62
63
|
|
|
64
|
+
## Inbound endpoint & logging (where a Dell ASN lands)
|
|
65
|
+
|
|
66
|
+
- **Endpoint:** Dell ASNs arrive via inbound `POST /v2/advance-shipping-notices`; **201** =
|
|
67
|
+
accepted/created.
|
|
68
|
+
- **Success vs. failure log split.** Successful ASN POSTs are logged in `Logs_Prudential.Api`, but
|
|
69
|
+
**failures often land in the shared base `Logs` schema, not the client log** — so a "no trace in
|
|
70
|
+
`Logs_Prudential.Api`" ASN may have failed and been recorded in `Logs.Api` instead. Check both.
|
|
71
|
+
- **Filter by `sourceIp` to avoid conflating clients.** Prudential's ASN traffic comes from
|
|
72
|
+
**`13.86.101.210`** (apiId 2). A *different* client's ASN feed comes from **`34.232.23.158`** —
|
|
73
|
+
when auditing the shared `Logs` schema, filter on `sourceIp` or you will mix the two clients'
|
|
74
|
+
ASNs together.
|
|
75
|
+
- **`AdvanceShippingNotices.purchaseOrderId` links an ASN to its PO.** A NULL/unmatched
|
|
76
|
+
`purchaseOrderId` corresponds to a **400 `EV-12`** on the inbound POST (the ASN could not be
|
|
77
|
+
matched/created). A successful 201 with a matched PO is what then triggers the shipped-email
|
|
78
|
+
chain (`transmit_ordershipped_updates_prudential.php` stamps
|
|
79
|
+
`c_dtTransmittedOrderShippedUpdateToPrudential` + `c_dtEmailSentOrderShipped`) — see
|
|
80
|
+
`transmit-ordershipped-email.md`.
|
|
81
|
+
|
|
63
82
|
## Gotchas
|
|
64
83
|
- Scoped to `apiId = 2` so other (internal) Prudential callers — which already send the new shape —
|
|
65
84
|
are not double-transformed.
|
|
@@ -72,6 +91,11 @@ found. `MATCH` (the default) errors when no row exists — that was the root cau
|
|
|
72
91
|
replay workflow doc.
|
|
73
92
|
|
|
74
93
|
## Change history
|
|
94
|
+
- 2026-07-28 — Documented the inbound ASN endpoint (`POST /v2/advance-shipping-notices`, 201 =
|
|
95
|
+
success), the success-vs-failure log split (successes in `Logs_Prudential.Api`, failures often
|
|
96
|
+
in shared base `Logs`), the `sourceIp` filter to separate Prudential (`13.86.101.210`) from
|
|
97
|
+
another client's ASN feed (`34.232.23.158`), and that `AdvanceShippingNotices.purchaseOrderId`
|
|
98
|
+
links the ASN to its PO (unmatched = 400 `EV-12`) and gates the shipped-email chain. (bala)
|
|
75
99
|
- 2026-06-12 — Fixed interceptor bug: bridge row keys were `trackingNumber`/`returnTrackingNumber`
|
|
76
100
|
instead of `trackingNumberId`/`returnTrackingNumberId`, causing EV-12 on all Dell ASN POSTs. Also
|
|
77
101
|
set `Core.RecordFields childPolicy = MATCH_CREATE` on ids 1437 and 2198. Replayed 28 failed payloads
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/delllch.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- worker/crons/toga2/prudential/transmissions_to_dell_usa.php
|
|
15
15
|
- worker/crons/toga2/prudential/transmissions_to_dell_ireland.php
|
|
16
16
|
- worker/crons/toga2/prudential/transmissions_to_dell_india.php
|
|
17
|
+
- worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php
|
|
17
18
|
related:
|
|
18
19
|
- ../profile.md
|
|
19
20
|
- dell-asn-units-interceptor.md
|
|
@@ -57,9 +58,12 @@ WHERE PurchaseOrders.vendorId = 1 # Dell = Vendors.id 1
|
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
- **Region lives on `ServiceRequests.customerId`**, resolved through the join to
|
|
60
|
-
`Client_Prudential.Customers`. Region rows:
|
|
61
|
-
`b3f7a2c1-4e89-4d6a-9c3b-8f1e5d2a7b04
|
|
62
|
-
|
|
61
|
+
`Client_Prudential.Customers`. Region rows:
|
|
62
|
+
- **USA — id 3, uuid `b3f7a2c1-4e89-4d6a-9c3b-8f1e5d2a7b04`**
|
|
63
|
+
- **Ireland — id 5, uuid `9a4e7d2b-5f13-48c6-b8e1-3d6a9c2f7e85`**
|
|
64
|
+
- **India — id 4, uuid `e6d1c8f4-2a75-4b3e-a9d7-1c4f6e8b3a52`**
|
|
65
|
+
|
|
66
|
+
This is NOT the SalesOrder customer — the SO customer is always
|
|
63
67
|
**"Agilant - Tech Hub" (id 1, uuid `36da53f8-38d2-404f-becf-f58f33c215d3`)**.
|
|
64
68
|
- A REQ is only ever transmitted, validated, or marked once it is selected here. Unit/bundle/SO/PO
|
|
65
69
|
creation happens elsewhere and does **not** send anything to Dell.
|
|
@@ -172,6 +176,17 @@ This is the established pattern for all Prudential script changes.
|
|
|
172
176
|
stuck this way, all with `customerId` NULL. The 2.0-side guard added in
|
|
173
177
|
`service-request-address-validation.md` stops NEW customer-less REQs; already-stuck ones need a
|
|
174
178
|
`customerId` backfill from ship-to country.
|
|
179
|
+
- **Root cause is a TOGa-side mapping gap, not missing SNOW data.** The inbound ServiceNow
|
|
180
|
+
payload carries **no customer field** at all — `ServiceRequests.customerId` is assigned
|
|
181
|
+
*inside TOGa* by `generate_sales_and_purchase_orders_from_service_requests.php`, so a NULL
|
|
182
|
+
means the generation cron failed to derive/assign a region, not that SNOW omitted anything.
|
|
183
|
+
- **The country-split cutover (~2026-07-17) is when this started biting.** Before the split,
|
|
184
|
+
`transmissions_to_dell_v1.php` had **no customer gate**, so every REQ transmitted regardless.
|
|
185
|
+
The regional split files added `INNER JOIN Customers` + `WHERE Customers.uuid = '<region>'`,
|
|
186
|
+
which is what drops NULL-`customerId` REQs. ~38 REQs stranded this way since the cutover.
|
|
187
|
+
- **Remediation:** backfill `ServiceRequests.customerId = 3` (USA) for the stuck US-ship-to
|
|
188
|
+
REQs; **durable fix** = have the generation cron derive country/`customerId` from
|
|
189
|
+
`deliverToAddress` so `customerId` is never NULL.
|
|
175
190
|
- **`dtSubmitted` is stamped without inspecting the response (latent bug, not yet fixed).** Every
|
|
176
191
|
regional cron sets `PurchaseOrders.dtSubmitted = NOW()` immediately after the send without
|
|
177
192
|
checking Dell's HTTP status, and `App_ApiTransaction::execute()` (`library/app/apitransaction.php`)
|
|
@@ -190,6 +205,14 @@ This is the established pattern for all Prudential script changes.
|
|
|
190
205
|
do not reintroduce them.
|
|
191
206
|
|
|
192
207
|
## Change history
|
|
208
|
+
- 2026-07-28 — Recorded all three region rows (USA id 3, Ireland id 5, India id 4, with uuids) and
|
|
209
|
+
the root cause of NULL `ServiceRequests.customerId`: the inbound SNOW payload carries no customer
|
|
210
|
+
field, so `customerId` is assigned TOGa-side by
|
|
211
|
+
`generate_sales_and_purchase_orders_from_service_requests.php` — a NULL is a TOGa mapping gap.
|
|
212
|
+
The ~2026-07-17 regional-split cutover added the `INNER JOIN Customers` gate (the old
|
|
213
|
+
`transmissions_to_dell_v1.php` had none), stranding ~38 REQs. Durable fix = derive
|
|
214
|
+
country/`customerId` from `deliverToAddress` in the generation cron; interim = backfill
|
|
215
|
+
`customerId = 3` for stuck US-ship-to REQs. (bala)
|
|
193
216
|
- 2026-07-24 — Documented the live-cron reality (production is the three regional split files under
|
|
194
217
|
`crons/toga2/prudential/`; monolith `transmissions_to_dell.php` disabled, `_v1` dead), the
|
|
195
218
|
selection query, region model on `ServiceRequests.customerId` (USA id 3), the `App_Api_Delllch`
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-28
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Prudential/ServiceRequest.php
|
|
@@ -15,6 +15,7 @@ related:
|
|
|
15
15
|
- clients/prudential/profile.md
|
|
16
16
|
- clients/prudential/features/dell-asn-units-interceptor.md
|
|
17
17
|
- clients/prudential/features/dell-lch-iop-transmissions.md
|
|
18
|
+
- clients/prudential/features/service-request-rejection-alert-email.md
|
|
18
19
|
- ../../../2.0/apps/_underscore/features/model-interceptor-unit-testing.md
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -47,8 +48,11 @@ back to USA rules.
|
|
|
47
48
|
`Client_Prudential.Customers` for the matching `name`. Returns `'India'`, `'Ireland'`, or
|
|
48
49
|
`'USA'` (default for anything unrecognised or when uuid is null).
|
|
49
50
|
3. A `match` expression dispatches to the correct validator.
|
|
50
|
-
4.
|
|
51
|
-
|
|
51
|
+
4. **Errors are collected, not fail-fast.** `prePost` runs all validators and accumulates every
|
|
52
|
+
error. On any failure it enqueues the internal rejection-alert worker task (see
|
|
53
|
+
`service-request-rejection-alert-email.md`) and then throws a single `_Exception_Validation`
|
|
54
|
+
carrying all errors — the API engine maps it to a 400 (behavior unchanged from the caller's
|
|
55
|
+
perspective; the change is that all errors are now reported at once and staff are alerted).
|
|
52
56
|
|
|
53
57
|
## Data model
|
|
54
58
|
|
|
@@ -109,8 +113,22 @@ with a space (e.g. `F92 FP83`). No numeric restriction.
|
|
|
109
113
|
when Prudential starts including them.
|
|
110
114
|
- **`line3` is India-only.** USA and Ireland validators do not check `line3` (it is not in
|
|
111
115
|
their Dell spec). If Dell starts sending it for USA/Ireland it is silently ignored.
|
|
116
|
+
- **⚠ OPEN (pre-existing HIGH) — unguarded unit access after the validation block throws a 500.**
|
|
117
|
+
Code that runs *after* the validation block reads
|
|
118
|
+
`serviceRequestUnits[0]->unit->serialNumber` / `->assetTag` without guarding for a missing
|
|
119
|
+
`serviceRequestUnits`. A payload with no units throws an **uncaught non-validation
|
|
120
|
+
`_Exception` → HTTP 500** (which dumps globals in debug mode) and, because it is not an
|
|
121
|
+
`_Exception_Validation`, it **skips the rejection-alert email** entirely. Fix: guard the unit
|
|
122
|
+
access (or validate `serviceRequestUnits` presence inside the collected-error block) so it
|
|
123
|
+
fails as a 400 with an alert instead of a silent 500. Tracked as a follow-up.
|
|
112
124
|
|
|
113
125
|
## Change history
|
|
126
|
+
- 2026-07-28 — `prePost` now **collects all validation errors** (was fail-fast) and, on any
|
|
127
|
+
failure, enqueues the internal rejection-alert worker task before throwing one
|
|
128
|
+
`_Exception_Validation` with all errors (400 unchanged). See
|
|
129
|
+
`service-request-rejection-alert-email.md`. Also recorded the open HIGH: unguarded
|
|
130
|
+
`serviceRequestUnits[0]->unit` access after the validation block throws a 500 and skips the
|
|
131
|
+
alert. (bala)
|
|
114
132
|
- 2026-07-24 — Added `validateCustomer()` as the first check in `prePost`: rejects a create when
|
|
115
133
|
`customer.uuid` is missing/null/blank (`trim(... ?? '') === ''`) via `_Exception_Validation`
|
|
116
134
|
(→ HTTP 400), stopping customer-less/unroutable REQs at the boundary. Previously a blank uuid
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Prudential: Service Request rejection alert email"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: prudential
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-28
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php
|
|
13
|
+
- _underscore/Model/Prudential/ServiceRequest.php
|
|
14
|
+
related:
|
|
15
|
+
- service-request-address-validation.md
|
|
16
|
+
- ../../../2.0/apps/_underscore/features/email-send-pipeline.md
|
|
17
|
+
- ../profile.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
When a Prudential ServiceNow→TOGa service-request submission (`POST /v2/service-requests`)
|
|
23
|
+
is **rejected by validation**, TOGa now sends a real-time internal alert email so staff know
|
|
24
|
+
the instant a REQ fails and can tell Prudential exactly what to resubmit. The
|
|
25
|
+
`_Model_Prudential_ServiceRequest::prePost()` interceptor collects **every** validation error
|
|
26
|
+
(not fail-fast), enqueues a worker task carrying the REQ number + reason + full payload, then
|
|
27
|
+
throws a single `_Exception_Validation` — the 400 returned to ServiceNow is unchanged. The
|
|
28
|
+
worker action `_Worker_Client_Prudential_reports_ReqRejectionEmail::sendRejectionEmail` sends
|
|
29
|
+
the alert.
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
- **Validation rejections only.** The MySQL 1062 duplicate-key race on `ServiceRequests.number`
|
|
34
|
+
(a duplicate SR-number re-POST) is deliberately **out of scope** — it is an internal defect
|
|
35
|
+
routed to engineering, not a "resubmit this" message for Prudential.
|
|
36
|
+
|
|
37
|
+
## Key files / entry points
|
|
38
|
+
|
|
39
|
+
- `_underscore/Model/Prudential/ServiceRequest.php` — `prePost()` collects all validator errors
|
|
40
|
+
and, on any failure, enqueues the alert via
|
|
41
|
+
`_Worker::runTask('Client/Prudential/reports/ReqRejectionEmail/sendRejectionEmail', {reqNumber, reason, payload})`
|
|
42
|
+
before throwing one `_Exception_Validation` carrying all errors.
|
|
43
|
+
- `worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php` —
|
|
44
|
+
`sendRejectionEmail(string $reqNumber, string $reason, object|array $payload): ...` builds and
|
|
45
|
+
sends a **plain-text** email (REQ# + reason + full JSON payload) to a class-constant recipient
|
|
46
|
+
list (currently `vburks@togatech.com`, `adiamond@togatech.com`).
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
1. `prePost()` runs every validator, **accumulating** errors instead of throwing on the first.
|
|
51
|
+
2. If the error list is non-empty, it enqueues one `ReqRejectionEmail/sendRejectionEmail` worker
|
|
52
|
+
task with the REQ number, the joined reason string, and the full inbound payload.
|
|
53
|
+
3. It then throws a single `_Exception_Validation` holding all errors → `Controller/Index.php`
|
|
54
|
+
maps it to **HTTP 400** to ServiceNow (unchanged behavior).
|
|
55
|
+
4. The worker action assembles the plain-text body and sends it via `_Email`. Because sends run
|
|
56
|
+
through the 2.0 email-send pipeline, the message lands within ~1 minute (see
|
|
57
|
+
`email-send-pipeline.md`).
|
|
58
|
+
|
|
59
|
+
## Send guards (why a queue failure can't turn the 400 into a 500)
|
|
60
|
+
|
|
61
|
+
- `setIsHtml(false)` — plain-text intent (see caveat below).
|
|
62
|
+
- `setClientIdentifier('Prudential')` and a fixed From of `noreply@togatech.com`.
|
|
63
|
+
- `json_encode(..., JSON_INVALID_UTF8_SUBSTITUTE)` with a false-fallback so a bad byte can't
|
|
64
|
+
fatal the encode; control characters stripped from `reqNumber`.
|
|
65
|
+
- The **whole dispatch is wrapped in a `\Throwable` catch** that `error_log`s an
|
|
66
|
+
identifier-only message (never PII) — so if the queue/email dispatch fails, the original
|
|
67
|
+
validation 400 is still what ServiceNow receives; the alert failure never escalates to a 500.
|
|
68
|
+
|
|
69
|
+
## Accepted residual risk (data-owner decision)
|
|
70
|
+
|
|
71
|
+
The **full inbound payload (including PII) is emailed** and persisted to `Logs_Prudential.Email`.
|
|
72
|
+
This was an explicit data-owner decision to maximize the staff alert's usefulness, accepted as a
|
|
73
|
+
known residual risk.
|
|
74
|
+
|
|
75
|
+
## Gotchas / known issues
|
|
76
|
+
|
|
77
|
+
- **A non-validation exception skips the alert.** The alert only fires for
|
|
78
|
+
`_Exception_Validation`. A payload that throws a *different* uncaught `_Exception` before the
|
|
79
|
+
validation block completes (e.g. the unguarded `serviceRequestUnits[0]->unit` access — see
|
|
80
|
+
`service-request-address-validation.md`) produces a **500 and no alert**. Fix that guard to
|
|
81
|
+
keep the alert reliable.
|
|
82
|
+
- **"Plain text" is transmitted HTML anyway.** `setIsHtml(false)` is honored by `_Email` when it
|
|
83
|
+
queues the row, but the worker2 `Infrastructure/Email/Send` action calls `IsHTML(true)`
|
|
84
|
+
unconditionally, so the message is sent HTML-mode regardless (see `email-send-pipeline.md`).
|
|
85
|
+
|
|
86
|
+
## Change history
|
|
87
|
+
- 2026-07-28 — Created: real-time internal alert on `POST /v2/service-requests` validation
|
|
88
|
+
rejection. `prePost()` now collects all validation errors and enqueues the
|
|
89
|
+
`ReqRejectionEmail/sendRejectionEmail` worker task (REQ# + reason + full payload) before
|
|
90
|
+
throwing one `_Exception_Validation` (400 unchanged). Recipients are a class constant
|
|
91
|
+
(`vburks@`, `adiamond@`). Whole dispatch wrapped in `\Throwable` catch so a queue failure can't
|
|
92
|
+
turn the 400 into a 500. Scope: validation rejections only (the 1062 SR-number duplicate race
|
|
93
|
+
is routed to engineering). Accepted residual risk: full payload PII is emailed + persisted to
|
|
94
|
+
`Logs_Prudential.Email`. (bala)
|
|
@@ -7,17 +7,19 @@ apps:
|
|
|
7
7
|
- dbchanges2
|
|
8
8
|
- websocket
|
|
9
9
|
- worker
|
|
10
|
+
- worker2
|
|
10
11
|
- library
|
|
11
12
|
project: _Underscore
|
|
12
13
|
client: prudential
|
|
13
14
|
type: profile
|
|
14
15
|
status: active
|
|
15
|
-
updated: 2026-07-
|
|
16
|
+
updated: 2026-07-28
|
|
16
17
|
owners: ["jcardinal", "rgirish", "bala"]
|
|
17
18
|
files: []
|
|
18
19
|
related:
|
|
19
20
|
- features/dell-asn-units-interceptor.md
|
|
20
21
|
- features/service-request-address-validation.md
|
|
22
|
+
- features/service-request-rejection-alert-email.md
|
|
21
23
|
- features/dell-lch-iop-transmissions.md
|
|
22
24
|
- features/device-information-import-and-contact-linking.md
|
|
23
25
|
---
|
|
@@ -41,6 +43,23 @@ order-status transmissions.
|
|
|
41
43
|
POST to Dell's LCH `LCHRequestV2` endpoint via the 1.0 `App_Api_Delllch` client. See the
|
|
42
44
|
Dell LCH IOP transmissions feature doc.
|
|
43
45
|
|
|
46
|
+
## Reference data (enums)
|
|
47
|
+
|
|
48
|
+
`Client_Prudential.PurchaseOrders.purchaseOrderStageId` — PO lifecycle stage:
|
|
49
|
+
|
|
50
|
+
| id | Stage | id | Stage |
|
|
51
|
+
|----|---------------|----|---------------|
|
|
52
|
+
| 3 | Open | 4 | Configuration |
|
|
53
|
+
| 9 | Accepted | 5 | Ready To Ship |
|
|
54
|
+
| 7 | In Progress | 2 | Received |
|
|
55
|
+
| 6 | POD | 1 | Rejected |
|
|
56
|
+
| 8 | Cancelled | | |
|
|
57
|
+
|
|
58
|
+
- PO lifecycle is tracked by `purchaseOrderStageId`. The `dtAcknowledged` column is **unused
|
|
59
|
+
(always NULL)** — do not key logic on it.
|
|
60
|
+
|
|
61
|
+
`ServiceRequests.serviceRequestTypeId`: **1 = New Hire, 2 = Breakfix, 3 = Refresh, 5 = Reclaim**.
|
|
62
|
+
|
|
44
63
|
## Gotchas
|
|
45
64
|
- Dell will not change their payload shape — see the Dell ASN units interceptor feature doc for the
|
|
46
65
|
PRE/POST translation that keeps their feed working after the tracking-number bridge migration.
|
package/package.json
CHANGED