toga-ai 1.0.620 → 1.0.622
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/library/INDEX.md +1 -0
- package/knowledge/1.0/apps/library/features/email-queue-attachments.md +112 -0
- package/knowledge/1.0/apps/test/INDEX.md +1 -1
- package/knowledge/1.0/apps/test/features/static-no-db-regression-harness.md +15 -1
- package/knowledge/1.0/apps/togadesk/INDEX.md +1 -1
- package/knowledge/1.0/apps/togadesk/features/notifications.md +22 -2
- package/knowledge/1.0/standards/backend-php.md +29 -1
- package/knowledge/2.0/apps/_underscore/features/tracking-number-bridges.md +22 -1
- package/knowledge/2.0/apps/api2/INDEX.md +2 -1
- package/knowledge/2.0/apps/api2/architecture.md +15 -4
- package/knowledge/2.0/apps/api2/features/cxml-shipnotice-gateway.md +186 -0
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +55 -1
- package/knowledge/2.0/apps/api2/workflows/environment-configuration-and-provisioning.md +35 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-canada/INDEX.md +2 -1
- package/knowledge/clients/compass-canada/features/grand-and-toy-asn-import.md +132 -1
- package/knowledge/clients/compass-canada/profile.md +19 -3
- package/knowledge/clients/compass-canada/workflows/grand-and-toy-asn-backfill.md +101 -0
- package/knowledge/clients/compass-usa/features/asn-to-item-fulfillment.md +35 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Cron Execution Monitoring (App_Framework check-in/out → CronJobExecutions)](features/cron-execution-monitoring.md) | `App_Framework::cronInitialization()` / `App_Framework::cronFinished()` (in `library/app/framework.php`) give every 1.0 (`App_`) cron job a check-in/check-out l | library/app/framework.php |
|
|
10
10
|
| [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | Two "View Recommended Services" buttons exist in the TOGa Refresh 2026 SR view: 1. | library/app/model/toga/diagnostic.php, library/app/model/servicerequest.php |
|
|
11
11
|
| [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
|
|
12
|
+
| [App_Email Queued Sending & Attachments (Common.EmailsQueued)](features/email-queue-attachments.md) | `App_Email::send()` can either send **inline** (PHPMailer talks to SES right there) or **queue** the message: `base64(serialize($this))` is inserted into `Commo | library/app/email.php, library/phpmailer/class.phpmailer.php, worker/crons/notifications/infrastructure/send_emails.php, worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php, togadesk/desk/includes/functions.php |
|
|
12
13
|
| [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
|
|
13
14
|
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/api/toga2.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
14
15
|
| [HTTP 500 Error Monitor (App_SystemMonitor_500Error) — and why its \"Error Type\" is not a diagnosis](features/http-500-error-monitor.md) | `App_SystemMonitor_500Error` (`library/app/systemmonitor/500error.php`, title **"HTTP 500 Error Alert"**) is the 1.0 system monitor that watches **`Logs.Api` fo | library/app/systemmonitor/500error.php, worker/crons/infrastructure/system_monitors.php, api2/Controller/Index.php, _underscore/Error.php |
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: App_Email Queued Sending & Attachments (Common.EmailsQueued)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: library
|
|
5
|
+
project: Library
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/email.php
|
|
13
|
+
- library/phpmailer/class.phpmailer.php
|
|
14
|
+
- worker/crons/notifications/infrastructure/send_emails.php
|
|
15
|
+
- worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php
|
|
16
|
+
- togadesk/desk/includes/functions.php
|
|
17
|
+
related:
|
|
18
|
+
- ./email-templates.md
|
|
19
|
+
- ../architecture.md
|
|
20
|
+
- ../../togadesk/features/notifications.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`App_Email::send()` can either send **inline** (PHPMailer talks to SES right there) or **queue**
|
|
26
|
+
the message: `base64(serialize($this))` is inserted into `Common.EmailsQueued` and drained later
|
|
27
|
+
by a **different app on a different host** — `worker/crons/notifications/infrastructure/send_emails.php`,
|
|
28
|
+
scheduled `* * * * *`.
|
|
29
|
+
|
|
30
|
+
Two facts dominate everything else here:
|
|
31
|
+
|
|
32
|
+
1. **Attachments used to be queued as filesystem *paths*.** The drain host has no such path, and
|
|
33
|
+
PHPMailer 5 silently swallows an unreadable attachment, so the mail arrived with the body
|
|
34
|
+
intact and the attachment gone — **no error, no log**. Fixed 2026-08-20 (TRUE-80952) by
|
|
35
|
+
embedding the file **bytes** at queue time.
|
|
36
|
+
2. **Queued email does not queue outside production.** `setQueueForSending()` is gated on
|
|
37
|
+
`!App_Registry::inTestMode()`, and every non-prod config has `internal.test_mode = 1`. So on
|
|
38
|
+
alpha/beta/local the message is sent **inline** off local disk and attachments always work —
|
|
39
|
+
any test there is a **false pass**.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
- `App_Email::setQueueForSending($bool)` (`library/app/email.php` ~L161):
|
|
44
|
+
`$this->queueForSending = (($bool && !App_Registry::inTestMode()) ? … : false);`
|
|
45
|
+
- `send()` with queueing on serializes `$this` and inserts into `Common.EmailsQueued`
|
|
46
|
+
(`serializedEmailObject`, LONGTEXT).
|
|
47
|
+
- Before serializing, `App_Email::embedAttachmentsForQueue()` (private, added TRUE-80952) reads
|
|
48
|
+
each attachment into **base64 bytes**. On drain, embedded entries go out via PHPMailer's
|
|
49
|
+
`AddStringAttachment()`; legacy bare-string path entries still use `AddAttachment()`.
|
|
50
|
+
- **Size budget:** `MAX_QUEUED_ATTACHMENT_BYTES = 7340032` (7 MB raw ≈ 9.8 MB base64, under the
|
|
51
|
+
SES 10 MB message cap). Over budget, embedding returns false and `send()` **falls back to an
|
|
52
|
+
inline send** instead of queueing — except when outbound email is disabled, where there is no
|
|
53
|
+
inline fallback and the case is logged.
|
|
54
|
+
- The file read is wrapped in a **local `set_error_handler` → `ErrorException` with a `finally`
|
|
55
|
+
restore**. This is mandatory, not stylistic: in 1.0 any PHP warning routes through
|
|
56
|
+
`App_Error::handleError` → `handleException` → `exit()`, so an unguarded `file_get_contents`
|
|
57
|
+
warning would kill the `send_emails` cron **mid-loop** and strand every queued email behind it.
|
|
58
|
+
`is_file()`/`is_readable()` are not sufficient — the file can vanish between check and read (TOCTOU).
|
|
59
|
+
|
|
60
|
+
## Blast radius (verified 2026-08-20)
|
|
61
|
+
|
|
62
|
+
Only **two** 1.0 call sites both queue *and* attach: TOGa Desk's `sendEmail()`
|
|
63
|
+
(`togadesk/desk/includes/functions.php`) and
|
|
64
|
+
`worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php` (~L44 + L82, bare
|
|
65
|
+
string path — it had the identical bug and is fixed by the same change). worker's other 16
|
|
66
|
+
`setQueueForSending` sites pass `false`; togaview's two queueing sites (`mvc/signup/post.php`,
|
|
67
|
+
`mvc/reset_password_success/post.php`) attach nothing; `tools` and `walmarttechservices` never
|
|
68
|
+
attach. Inline sending and bare-string path attachments are unchanged.
|
|
69
|
+
|
|
70
|
+
## Deploy ordering (asymmetric — and so is rollback)
|
|
71
|
+
|
|
72
|
+
The queue payload format changed, and the queueing app and the draining app deploy separately.
|
|
73
|
+
|
|
74
|
+
- worker **new** + togadesk **old** → togadesk queues paths, new worker still handles paths. Safe.
|
|
75
|
+
- worker **old** + togadesk **new** → togadesk queues `['data' => …]`, the old worker looks for
|
|
76
|
+
`['path']`, finds none, and **drops the attachment** — same symptom, new cause.
|
|
77
|
+
|
|
78
|
+
Therefore: **deploy `worker` before `togadesk`.** Roll back in the inverse order — roll back
|
|
79
|
+
togadesk first, let the queue drain empty, *then* worker.
|
|
80
|
+
|
|
81
|
+
## Gotchas / known issues
|
|
82
|
+
|
|
83
|
+
- **You cannot reproduce or validate a queued-email bug outside production.** `test_mode = 1` on
|
|
84
|
+
alpha, beta and every dev config collapses `setQueueForSending(true)` to `false`. This is the
|
|
85
|
+
most likely reason TRUE-75813 and TRUE-79924 were "verified" and closed while prod stayed
|
|
86
|
+
broken — both corrected the upload path *inside togadesk*, which was already working; the file
|
|
87
|
+
was lost one hop later, at the queue. Locally the queueing app and the drain also share one
|
|
88
|
+
filesystem, so even with queueing forced on the path resolves — you must **delete the source
|
|
89
|
+
file between queue and drain** to simulate the drain host.
|
|
90
|
+
- **PHPMailer 5 hides a missing attachment.** `AddAttachment()`
|
|
91
|
+
(`library/phpmailer/class.phpmailer.php` ~L1358) treats an unreadable path as `STOP_CONTINUE`
|
|
92
|
+
with `$exceptions` off, and sends the message anyway. Absence of an error proves nothing.
|
|
93
|
+
- **The drain cron's exclusion filter is dead code.** `send_emails.php` ~L34 carries
|
|
94
|
+
`AND EmailsQueued.serializedEmailObject NOT LIKE '%App_Email%'`. The column holds base64, in
|
|
95
|
+
which the literal `App_Email` never appears (verified with a PHP round-trip) — the filter
|
|
96
|
+
matches and excludes nothing. **Not changed**: "fixing" it would stop TOGa Desk mail entirely.
|
|
97
|
+
Needs a decision from whoever wrote it before anyone touches it.
|
|
98
|
+
- **OPEN RISK — `max_allowed_packet` on the prod Common cluster is unverified.** Rows can now
|
|
99
|
+
carry ~9.8 MB of base64. The column is LONGTEXT so it is fine, but MySQL 5.7 defaults
|
|
100
|
+
`max_allowed_packet` to 4 MB (8.0: 64 MB). If prod is below the budget the INSERT fails as an
|
|
101
|
+
`App_Query` exception on a live ticket reply — worse than a missing attachment. Check it, and
|
|
102
|
+
lower `MAX_QUEUED_ATTACHMENT_BYTES` beneath it if needed (the oversized path already degrades
|
|
103
|
+
to an inline send, which is the correct behavior).
|
|
104
|
+
- Regression coverage: `test/@Mark/TRUE-80952/test_queued_email_attachments.php` — 26 assertions,
|
|
105
|
+
no DB, no SMTP, confirmed failing against pre-fix HEAD.
|
|
106
|
+
|
|
107
|
+
## Change history
|
|
108
|
+
- 2026-08-20 — TRUE-80952: queued attachments now embed base64 bytes (`embedAttachmentsForQueue()`
|
|
109
|
+
+ `AddStringAttachment`) instead of paths that do not exist on the drain host; added
|
|
110
|
+
`MAX_QUEUED_ATTACHMENT_BYTES` with inline-send fallback and a scoped warning guard around the
|
|
111
|
+
file read; recorded the `test_mode` no-queue trap, the dead drain filter, and the
|
|
112
|
+
worker-before-togadesk deploy order (mhammontree)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
|
|
11
11
|
| [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
|
|
12
12
|
| [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
|
|
13
|
-
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, library/app/database.php, library/app/api/toga2.php |
|
|
13
|
+
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, test/@Mark/TRUE-80952/test_queued_email_attachments.php, library/app/database.php, library/app/api/toga2.php |
|
|
14
14
|
| [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
|
|
15
15
|
| [Talos Knowledge Base Pipeline (Uploader + Processor)](features/talos-kb-pipeline.md) | `team/talos/` holds the two-script web tooling that feeds the **TOGa Talos** (TOGa IQ) AI knowledge bases. | test/team/talos/kb_uploader.php, test/team/talos/kb_processor.php, test/team/talos/kb_processor.ini |
|
|
16
16
|
| [TOGa 2.0 Client Onboarding SQL Generator](features/toga2-client-onboarding-sql.md) | > **Superseded by the browser wizard.** The generation logic here was extracted into the reusable > `OnboardingSqlGenerator` class and wrapped in a local browse | test/team/generate_toga2_onboarding_sql.php |
|
|
@@ -6,10 +6,11 @@ project: Test
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- test/@Mark/AIG/test_multi_email.php
|
|
13
|
+
- test/@Mark/TRUE-80952/test_queued_email_attachments.php
|
|
13
14
|
- library/app/database.php
|
|
14
15
|
- library/app/api/toga2.php
|
|
15
16
|
related:
|
|
@@ -75,6 +76,17 @@ method**, which is a large share of `library`.
|
|
|
75
76
|
entirely. That was out of scope because it changes production code; prefer it when you are
|
|
76
77
|
already editing the method.
|
|
77
78
|
|
|
79
|
+
## Second worked example — private methods via `ReflectionMethod`
|
|
80
|
+
|
|
81
|
+
`test/@Mark/TRUE-80952/test_queued_email_attachments.php` (26 assertions, PHP 7.2, no DB, no SMTP)
|
|
82
|
+
extends the same pattern to a **private** method: it `require_once`s `library/app/email.php`
|
|
83
|
+
directly and invokes `App_Email::embedAttachmentsForQueue()` through `ReflectionMethod` with
|
|
84
|
+
`setAccessible(true)`. It covers the embed + serialize round-trip **with the source file deleted**
|
|
85
|
+
(the real production failure mode), a missing file, a directory path, an empty file, the legacy
|
|
86
|
+
bare-string path form, idempotent re-embed, exact-limit and aggregate-limit budget checks, and
|
|
87
|
+
that the scoped error handler is restored. Confirmed failing against pre-fix HEAD — a 1.0
|
|
88
|
+
regression test is only credible if you have run it against the broken code.
|
|
89
|
+
|
|
78
90
|
## Gotchas / known issues
|
|
79
91
|
|
|
80
92
|
- **A mirrored test proves the mirror, not production.** The drift guard is what makes it
|
|
@@ -102,6 +114,8 @@ already editing the method.
|
|
|
102
114
|
**`worker2` / `_underscore` enforce 8.1** — a helper shared between the two must satisfy 7.2.
|
|
103
115
|
|
|
104
116
|
## Change history
|
|
117
|
+
- 2026-08-20 — added the TRUE-80952 `ReflectionMethod` variant (private-method coverage, source
|
|
118
|
+
file deleted to simulate the queue-drain host) as a second worked example (mhammontree)
|
|
105
119
|
|
|
106
120
|
- 2026-08-12 — Sharpened the 7.2 lint gotcha into a real verification procedure: the default CLI is
|
|
107
121
|
PHP 8.x, so `php -l` there proves nothing; use an actual 7.2 binary, add `-n` if its `php.ini` is
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk ingests support email into tickets through a cron-driven IMAP poller (`crons/tickets.php`) plus a postfix pipe variant (`crons/pipe.php`). | crons/tickets.php, crons/tickets_prod.php, crons/pipe.php, desk/includes/classes/class.ticket.php, vendor/classes/class.imap.php |
|
|
7
7
|
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | The **central** subsystem is TOGa Desk's field-service dispatch domain: repair-order lifecycle, technician scheduling, onsite vs depot service, parts, and shipm | desk/includes/controllers/actions/central/, desk/includes/classes/class.repair.php, desk/includes/classes/class.repairhistory.php, desk/_/browser/datatable/central.php, desk/template/pages/central.php, desk/template/pages/central/view.php, desk/template/modals/central/addTracking.php, library/app/model/togadesk/repairorder.php, library/app/model/togadesk/repairordertracking.php, library/app/api/carrier/ups.php |
|
|
8
8
|
| [Managed Service Order Create Flow (New MSO modal → tickets/add)](features/managed-service-order-create.md) | The **Managed Service Order (MSO) create flow** is how a TOGa Desk user manually creates a managed service order: pick an End User (or a Client Location), choos | desk/includes/controllers/actions/tickets/add.php, desk/template/modals/tickets/addNew.php, desk/template/pages/managed.php, desk/template/pages/getinfo.php, desk/template/footer.php, library/app/model/togadesk/repairorder.php |
|
|
9
|
-
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, crons/tickets.php, crons/tickets_prod.php |
|
|
9
|
+
| [Ticket Email Notifications (notifications table)](features/notifications.md) | Which TOGa Desk emails fire for a given client is driven **entirely by data**, not code: the `TOGaDeskSupport.notifications` table holds one row per `(clientid, | desk/includes/classes/class.notification.php, desk/includes/functions.php, desk/includes/classes/class.ticket.php, crons/tickets.php, crons/tickets_prod.php |
|
|
10
10
|
| [Per-Client Hostname Restriction (getRestrictedClient)](features/per-client-host-restriction.md) | TOGa Desk supports **per-client branded URLs** (e.g. | desk/includes/functions.php, desk/template/header.php, _/browser/datatable/tickets, desk/includes/classes/class.ticket.php, ebs/setup_phpini.php, ebs/http_to_https.php, desk/template/modals/kb/viewDocument.php, desk/template/modals/files/aws-view.php, desk/template/modals/files/contract-view.php, template/pages/documents/view.php, template/pages/tasks/view.php |
|
|
11
11
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk exposes a programmatic API at `desk/api/`. | desk/api/index.php, desk/api/resources/tickets.php, desk/api/resources/assets.php, desk/api/resources/authenticate.php, desk/includes/classes/class.apikey.php, desk/includes/functions.php |
|
|
12
12
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | The SMB contracts page (`/desk/?route=toga/smbcontracts&togaClientId=<id>`) edits `TOGA_*.SMBContracts` rows via a modal. | desk/template/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/modals/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/smbContract.php, desk/includes/controllers/actions/toga/smbcontracts/edit.php |
|
|
@@ -6,13 +6,16 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["ajean"]
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["ajean", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/classes/class.notification.php
|
|
13
|
+
- desk/includes/functions.php
|
|
14
|
+
- desk/includes/classes/class.ticket.php
|
|
13
15
|
- crons/tickets.php
|
|
14
16
|
- crons/tickets_prod.php
|
|
15
17
|
related:
|
|
18
|
+
- 1.0/apps/library/features/email-queue-attachments.md
|
|
16
19
|
- 1.0/apps/togadesk/features/ticket-lifecycle.md
|
|
17
20
|
- 1.0/apps/togadesk/features/email-to-ticket-intake.md
|
|
18
21
|
- 1.0/apps/togadesk/architecture.md
|
|
@@ -55,6 +58,20 @@ nothing if the `STAFF_*` row is absent.
|
|
|
55
58
|
- `TOGaDeskSupport.people_departments` — peopleid ↔ departmentid mapping.
|
|
56
59
|
|
|
57
60
|
## Gotchas / known issues
|
|
61
|
+
- **Outbound attachments were dropped at the email queue, not in TOGa Desk.** `sendEmail()`
|
|
62
|
+
queues via `App_Email` into `Common.EmailsQueued`; until TRUE-80952 attachments were serialized
|
|
63
|
+
as local **paths** that do not exist on the worker host that drains the queue, and PHPMailer 5
|
|
64
|
+
silently sent the mail without them. Two earlier tickets (TRUE-75813, TRUE-79924) "fixed" the
|
|
65
|
+
upload path inside TOGa Desk — which was never the problem — and passed testing because
|
|
66
|
+
**alpha/beta/local run `test_mode = 1`, which disables queueing entirely and sends inline**.
|
|
67
|
+
Never validate an attachment/email change for TOGa Desk on a non-prod environment. See
|
|
68
|
+
[App_Email queued sending & attachments](../../library/features/email-queue-attachments.md).
|
|
69
|
+
- **The AIG branch of `sendEmail()` never attaches anything (open bug, separate from TRUE-80952).**
|
|
70
|
+
`desk/includes/functions.php` ~L961-975 and the AIG arm of `addReply()` in
|
|
71
|
+
`class.ticket.php` build `App_Email_AigTemplates_SendAigSupportTicketReplyNotification` and pass
|
|
72
|
+
`$attachments` only to `logEmail()` — **`addAttachment()` is never called**. Staples Protection
|
|
73
|
+
ticket replies therefore lose attachments for a different reason, and the TRUE-80952 fix does
|
|
74
|
+
not help them. Needs its own ticket.
|
|
58
75
|
- **Silent disable by omission.** A missing `(clientid, type)` row produces no email and no log
|
|
59
76
|
line — diagnose "missing notification" reports by querying `notifications` for that client's
|
|
60
77
|
rows first, before suspecting recipient flags or code.
|
|
@@ -70,6 +87,9 @@ nothing if the `STAFF_*` row is absent.
|
|
|
70
87
|
notification still depends on a `STAFF_NEW_TICKET` row existing for the ticket's client.
|
|
71
88
|
|
|
72
89
|
## Change history
|
|
90
|
+
- 2026-08-20 — TRUE-80952: recorded that outbound attachments were lost at the `App_Email` queue
|
|
91
|
+
(not in TOGa Desk), that non-prod `test_mode` disables queueing and yields false passes, and the
|
|
92
|
+
separate AIG-arm bug where `$attachments` never reaches `addAttachment()` (mhammontree)
|
|
73
93
|
- 2026-06-24 — documented the `notifications` `(clientid, type)` data model and staff-recipient
|
|
74
94
|
resolution; recorded the clientid-178 missing-`STAFF_*`-rows incident (ajean)
|
|
75
95
|
|
|
@@ -5,11 +5,12 @@ project: Library
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
8
|
+
updated: 2026-08-20
|
|
9
9
|
owners: [jcardinal, rgirish, mhammontree, ajean]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/library/architecture.md
|
|
13
|
+
- ../apps/library/features/email-queue-attachments.md
|
|
13
14
|
- ../apps/library/features/error-capture-1-0.md
|
|
14
15
|
- ../../2.0/standards/backend-php.md
|
|
15
16
|
---
|
|
@@ -306,6 +307,14 @@ Two sanctioned patterns, in order of preference:
|
|
|
306
307
|
|
|
307
308
|
Never leave either one in effect beyond the block that needs it.
|
|
308
309
|
|
|
310
|
+
**Worked example — reading a file you are about to serialize (TRUE-80952).**
|
|
311
|
+
`App_Email::embedAttachmentsForQueue()` wraps its `file_get_contents` in a local
|
|
312
|
+
`set_error_handler` → `ErrorException` with a `finally` restore. Guarding with
|
|
313
|
+
`is_file()`/`is_readable()` alone is **not** sufficient: the file can vanish between the
|
|
314
|
+
check and the read (TOCTOU), and the resulting warning would `exit()` the
|
|
315
|
+
`send_emails` cron **mid-loop**, stranding every queued email behind it. Any file read
|
|
316
|
+
inside a loop that processes a queue takes pattern 1.
|
|
317
|
+
|
|
309
318
|
### Start the session before App_Error's fatal handler — and set the handler in code, not INI
|
|
310
319
|
|
|
311
320
|
On EB PHP 8.5 / AL2023, the `.so` extension and cookie flags aside, two rules make Redis
|
|
@@ -501,6 +510,25 @@ Additional verified detail:
|
|
|
501
510
|
> (~L112-123), `clients/prudential/profile.md` (~L75). Prefer the local-handler pattern
|
|
502
511
|
> from the canonical section above for anything new.
|
|
503
512
|
|
|
513
|
+
### Test mode silently disables queued email — non-prod cannot validate a queue bug
|
|
514
|
+
|
|
515
|
+
`App_Email::setQueueForSending()` (`library/app/email.php` ~L161) is gated on
|
|
516
|
+
`!App_Registry::inTestMode()`:
|
|
517
|
+
|
|
518
|
+
$this->queueForSending = (($bool && !App_Registry::inTestMode()) ? … : false);
|
|
519
|
+
|
|
520
|
+
`inTestMode()` is `config['internal']['test_mode'] > 0`, and **every non-prod config is 1**
|
|
521
|
+
(verified for togadesk: prod 0; alpha 1; beta 1; all `config.dev-*.ini` 1). So outside
|
|
522
|
+
production `setQueueForSending(true)` collapses to `false` and the email is sent **inline**,
|
|
523
|
+
off the local filesystem, by the same process that built it. Anything that only breaks on the
|
|
524
|
+
queue path — a serialization gap, a resource the drain host cannot reach — **passes on
|
|
525
|
+
alpha/beta/local**. TRUE-75813 and TRUE-79924 were both verified and closed this way while
|
|
526
|
+
production stayed broken.
|
|
527
|
+
|
|
528
|
+
Rule: a change to queued email is not verified until it is verified in production, or against
|
|
529
|
+
a harness that exercises the serialize → drain boundary with the queueing host's local state
|
|
530
|
+
removed. Treat a non-prod pass on queue code as no evidence at all.
|
|
531
|
+
|
|
504
532
|
## Security Best Practices
|
|
505
533
|
|
|
506
534
|
### Input Validation and Sanitization
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "dfranks", "apeterson", "bala", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -29,6 +29,7 @@ files:
|
|
|
29
29
|
- dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql
|
|
30
30
|
related:
|
|
31
31
|
- recursive-item-fulfillments.md
|
|
32
|
+
- ../../api2/features/cxml-shipnotice-gateway.md
|
|
32
33
|
---
|
|
33
34
|
|
|
34
35
|
## Summary
|
|
@@ -120,6 +121,20 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
120
121
|
|
|
121
122
|
## Gotchas / known issues
|
|
122
123
|
|
|
124
|
+
- **⚠ The three ASN bridge `trackingNumberId` fields do NOT share a `childPolicy`, and a new client
|
|
125
|
+
must be provisioned per-API.** Core defaults: **1437** (unit bridge) `MATCH_CREATE`, **1441** (item
|
|
126
|
+
bridge) `MATCH`, **1445** (header bridge) `MATCH`. Only the unit bridge may mint a `TrackingNumbers`
|
|
127
|
+
row on its own. A producer that posts tracking at the item or header level (the cXML ShipNotice
|
|
128
|
+
translator does, since 2026-06-11) therefore needs `MATCH_CREATE` **overrides** in that tenant's
|
|
129
|
+
`Apis_RecordFields` for the writing API, or the write fails **EV-12 on field `trackingNumber`**.
|
|
130
|
+
Compass USA has all three overridden (`apiId 4`); Compass Canada had none, which rejected 100% of
|
|
131
|
+
its ship notices for 67 days (2026-06-11 → 2026-08-20). **Add these three override rows to the
|
|
132
|
+
onboarding checklist for any client whose ASNs arrive by cXML** — and ship them as a
|
|
133
|
+
`dbchanges2/Client_<Tenant>/` migration, since a manual patch dies on the next reseed. Do **not**
|
|
134
|
+
flip the Core defaults: Core ids are shared by every tenant (the same reason the IF-side 2184 flip
|
|
135
|
+
was rejected). Mechanics:
|
|
136
|
+
[nested-relationship writes](../../api2/features/nested-relationship-writes.md) and the
|
|
137
|
+
[cXML ShipNotice gateway](../../api2/features/cxml-shipnotice-gateway.md).
|
|
123
138
|
- **Tracking is now an inherent child, not a scalar.** Any producer that sent a flat `trackingNumber`
|
|
124
139
|
on a unit must nest it under the unit's `*TrackingNumbers` child array, or it is silently dropped.
|
|
125
140
|
- **Pruning a unit/item means deleting its bridge rows FIRST — the bridge FKs are `RESTRICT`, not
|
|
@@ -238,6 +253,12 @@ returnTrackingNumber: {...} }]`, and an IFIU's as `itemFulfillmentItemUnitTracki
|
|
|
238
253
|
for record 41.
|
|
239
254
|
|
|
240
255
|
## Change history
|
|
256
|
+
- 2026-08-20 — Recorded the **`childPolicy` asymmetry across the three ASN tracking bridges** (1437
|
|
257
|
+
`MATCH_CREATE`, 1441/1445 `MATCH`) and the resulting per-API `Apis_RecordFields` provisioning
|
|
258
|
+
requirement for any client posting item- or header-level tracking. Compass Canada had zero override
|
|
259
|
+
rows, so every cXML ship notice was rejected EV-12 on `trackingNumber` from the 2026-06-11
|
|
260
|
+
ShipControl rework until 2026-08-20. Added to the new-client onboarding checklist; Core defaults
|
|
261
|
+
must not be flipped. (bala)
|
|
241
262
|
- 2026-08-18 - **Measured which bridge each client actually populates, and it is not what "copy
|
|
242
263
|
Compass" assumes.** Added the 318-vs-319 row-count table (Compass 4,312/10,144; CompassCanada
|
|
243
264
|
169/138; Elite 0/109; Prudential 0/380; Quad 0/633; NYCHH 0/unit-level) and the rule to
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
6
|
| [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Rate/Entitlement.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql, dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql |
|
|
7
7
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, _underscore/Model/Core/Record.php, worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
8
|
+
| [cXML ShipNotice Gateway (ASN ingestion, carrier resolution, per-client provisioning)](features/cxml-shipnotice-gateway.md) | `_Component_Api_Cxml` accepts a supplier `ShipNoticeRequest` and translates it into a `POST /v2/advance-shipping-notices` on the V2 JSON engine. | api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php |
|
|
8
9
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
9
10
|
| [ENVIRONMENT (not the EB environment name) decides the _underscore branch and Config file](features/environment-variable-drives-underscore-branch.md) | An api2 Elastic Beanstalk instance decides **which `_underscore` branch it clones** and **which `Config/<env>.ini` it loads** from the EB environment property * | api2/.ebextensions/git.php, api2/.ebextensions/php_include_underscore.config, api2/.ebextensions/git.sandbox-dev.json, api2/.ebextensions/git.sandbox-client.json, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, api2/Component/Api/V2/V2.php |
|
|
10
11
|
| [Health-check endpoint (/health liveness short-circuit)](features/health-check-endpoint.md) | `_Controller_Index::api()` short-circuits **liveness/health-probe** requests to an HTTP 200 **before** any routing, DB bootstrap, or V2 engine work runs. | api2/Controller/Index.php |
|
|
@@ -25,4 +26,4 @@
|
|
|
25
26
|
| [V2 REST query contract (params, where grammar, encoding, ACL behavior)](features/v2-rest-query-contract.md) | What an **HTTP client** has to get right to query the Toga v2 REST API: which query params are recognized, the exact `where` grammar, how the query string is (n | api2/Component/Api/V2/V2.php |
|
|
26
27
|
| [V2 reverse hasMany collections must be named in the fetch fields whitelist](features/v2-reverse-hasmany-fields-whitelist.md) | In the V2 JSON engine, a **reverse hasMany** relationship — the collection of child records that foreign-key back to a parent (e.g. | api2/Component/Api/V2/V2.php |
|
|
27
28
|
| [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, api2/.platform/hooks/prebuild/git.sh, api2/.ebextensions/git.php, api2/.ebextensions/git.sandbox-dev.json |
|
|
28
|
-
| [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 |
|
|
29
|
+
| [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/composer.json, api2/composer.lock, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, dbchanges2/Logs/, _underscore/Route.php |
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: [jcardinal, bala, mhammontree, dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Controller/Index.php
|
|
@@ -14,7 +14,8 @@ files:
|
|
|
14
14
|
- api2/Component/Api/Cxml/Cxml.php
|
|
15
15
|
- api2/Component/Api/V2/Response/Response.php
|
|
16
16
|
- api2/Config/
|
|
17
|
-
related:
|
|
17
|
+
related:
|
|
18
|
+
- features/cxml-shipnotice-gateway.md
|
|
18
19
|
---
|
|
19
20
|
|
|
20
21
|
## Summary
|
|
@@ -146,8 +147,18 @@ Core `ClientApiIdentities` → dispatch: `OrderRequest` builds a Sales Order and
|
|
|
146
147
|
`/sales-orders`; `ShipNoticeRequest` → `/advance-shipping-notices`; `PunchOutSetupRequest`
|
|
147
148
|
creates a Vision (1.0) quote, Store (1.0) cart, or TOGa Commerce session. **cXML rides on
|
|
148
149
|
top of the V2 JSON engine** (it authenticates and POSTs via `_ApiRequest`), inheriting ACL,
|
|
149
|
-
interceptors, and logging. PunchOut paths write directly to legacy **Vision/Store**
|
|
150
|
-
DBs (`DB_VISION_1` / `DB_STORE_1`).
|
|
150
|
+
interceptors, and logging. PunchOut paths write directly to legacy **Vision/Store**
|
|
151
|
+
(1.0) DBs (`DB_VISION_1` / `DB_STORE_1`).
|
|
152
|
+
|
|
153
|
+
**Critical rule — every internal call made from this component must run as the client resolved from
|
|
154
|
+
the document's `Sender > Credential`, never as a hardcoded tenant.** A hardcoded client uuid/api
|
|
155
|
+
credential here is a silent cross-tenant data leak: the read side uses `DB_CLIENT` and looks
|
|
156
|
+
correct while writes land in the wrong client DB (found 2026-08-20 in
|
|
157
|
+
`findOrCreateShippingCarrier()`, which wrote Compass Canada carriers into `Client_Compass`).
|
|
158
|
+
Second gateway-wide trap: whether a nested child (e.g. an ASN `trackingNumber`) is **created** or
|
|
159
|
+
only **looked up** is per-API DB provisioning — `Client_<x>.Apis_RecordFields.overrideChildPolicy` —
|
|
160
|
+
so a fully-provisioned client can still reject 100% of its documents. Details:
|
|
161
|
+
[cXML ShipNotice Gateway](features/cxml-shipnotice-gateway.md).
|
|
151
162
|
|
|
152
163
|
## Multi-database architecture
|
|
153
164
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: cXML ShipNotice Gateway (ASN ingestion, carrier resolution, per-client provisioning)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/Cxml/Cxml.php
|
|
13
|
+
- api2/Component/Api/V2/V2.php
|
|
14
|
+
- api2/Controller/Index.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- nested-relationship-writes.md
|
|
18
|
+
- ../../_underscore/features/tracking-number-bridges.md
|
|
19
|
+
- ../../../clients/compass-canada/features/grand-and-toy-asn-import.md
|
|
20
|
+
- ../../../clients/compass-usa/features/asn-to-item-fulfillment.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`_Component_Api_Cxml` accepts a supplier `ShipNoticeRequest` and translates it into a
|
|
26
|
+
`POST /v2/advance-shipping-notices` on the V2 JSON engine. Six clients use this gateway today
|
|
27
|
+
(Compass USA, Compass Canada, PC Matic B2B, IM Houston and others). The translator resolves the
|
|
28
|
+
authenticating client from the document's `Sender > Credential`, resolves/creates the shipping
|
|
29
|
+
carrier, and builds the nested ASN payload including the three tracking bridges.
|
|
30
|
+
|
|
31
|
+
Two things make this path fragile, and both bit production in 2026:
|
|
32
|
+
|
|
33
|
+
1. **Nested tracking-number creation is per-API provisioning, not code.** Whether a nested
|
|
34
|
+
`trackingNumber` is *created* or only *looked up* comes from
|
|
35
|
+
`Client_<x>.Apis_RecordFields.overrideChildPolicy` for the authenticating API. A client can be
|
|
36
|
+
fully provisioned for ASNs and still reject **100%** of ship notices because that table is empty.
|
|
37
|
+
2. **The gateway is multi-tenant but was not written multi-tenant everywhere.** One helper
|
|
38
|
+
authenticated its internal API call as a hardcoded tenant, so one client's traffic wrote rows
|
|
39
|
+
into another client's database.
|
|
40
|
+
|
|
41
|
+
## Key files / entry points
|
|
42
|
+
|
|
43
|
+
- `api2/Component/Api/Cxml/Cxml.php` — `execute()` authenticates the `Sender > Credential`,
|
|
44
|
+
dispatches `ShipNoticeRequest`, and builds the ASN payload.
|
|
45
|
+
`findOrCreateShippingCarrier()` resolves the carrier named in the document.
|
|
46
|
+
- `api2/Component/Api/V2/V2.php` — `getChildPolicy()` (~8556) decides link-vs-create for each
|
|
47
|
+
nested object; see [nested-relationship-writes](nested-relationship-writes.md).
|
|
48
|
+
- Host routing: `cxml` / `cxml1-3` / `cxml-writer` (see [architecture](../architecture.md)).
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
1. The document arrives as `text/xml`. The `Sender > Credential` (Identity + SharedSecret) **is**
|
|
53
|
+
the authentication — there is no `Authorization` header on a cXML POST. So a stored document can
|
|
54
|
+
be replayed as-is without the caller handling any credential.
|
|
55
|
+
2. The credential resolves to a client + API (`Core.ClientApiIdentities` and the tenant's `Apis`
|
|
56
|
+
row). Everything downstream must run **as that client**.
|
|
57
|
+
3. Carrier: the header's `CarrierIdentifier` / `CarrierName` is matched against the tenant's
|
|
58
|
+
`ShippingCarriers` (`code = X OR name = X`), and created if absent.
|
|
59
|
+
4. The ASN payload is POSTed to `/v2/advance-shipping-notices` through the V2 engine, so it inherits
|
|
60
|
+
ACL, interceptors and logging. The per-client ASN interceptor (Core record 55) then builds the
|
|
61
|
+
ItemFulfillment chain.
|
|
62
|
+
5. The raw request and response are retained in full in `Logs_<Client>.Api`
|
|
63
|
+
(`direction = IN`, `route = '/'`, `method = POST`) — including **rejected** documents. That log is
|
|
64
|
+
the replay source for any backfill.
|
|
65
|
+
|
|
66
|
+
## Per-client provisioning — the three tracking-bridge child policies
|
|
67
|
+
|
|
68
|
+
A nested `trackingNumber` inside an ASN POST is only *created* when the owning bridge field's
|
|
69
|
+
effective child policy is `MATCH_CREATE`. The `Core.RecordFields` defaults are **not uniform**:
|
|
70
|
+
|
|
71
|
+
| Bridge field | RecordField | Core default `childPolicy` |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| ASN **unit** bridge `trackingNumberId` | 1437 | `MATCH_CREATE` |
|
|
74
|
+
| ASN **item** bridge `trackingNumberId` | 1441 | `MATCH` |
|
|
75
|
+
| ASN **header** bridge `trackingNumberId` | 1445 | `MATCH` |
|
|
76
|
+
|
|
77
|
+
`getChildPolicy()` prefers `Client_<x>.Apis_RecordFields.overrideChildPolicy` for the
|
|
78
|
+
authenticating API and only falls back to the Core default. So:
|
|
79
|
+
|
|
80
|
+
- **Compass USA** cXML API (`apiId 4`) has all three overridden to `MATCH_CREATE` — its ASNs work.
|
|
81
|
+
- **Compass Canada** "Grand & Toy (Cxml)" API (`apiId 3`) had **zero** rows, so 1441/1445 stayed
|
|
82
|
+
`MATCH`, the nested tracking number was looked up instead of created, and every ship notice was
|
|
83
|
+
rejected **EV-12 on field `trackingNumber`**. Fixed 2026-08-20 by inserting three rows
|
|
84
|
+
(`apiId 3`, `recordFieldId` 1437 / 1441 / 1445, `overrideChildPolicy = MATCH_CREATE`).
|
|
85
|
+
|
|
86
|
+
> **Onboarding rule — this is the step that gets missed.** When enabling cXML ASN ingestion for a
|
|
87
|
+
> new client, provisioning the ASN records and ACLs is **not enough**. Insert the three
|
|
88
|
+
> `Apis_RecordFields` overrides for that client's cXML API, as a `dbchanges2/Client_<Tenant>/`
|
|
89
|
+
> migration. Verify with
|
|
90
|
+
> `SELECT apiId, recordFieldId, overrideChildPolicy FROM Client_<x>.Apis_RecordFields`.
|
|
91
|
+
>
|
|
92
|
+
> **Known gap today: PC Matic B2B** has the ASN records provisioned and **zero**
|
|
93
|
+
> `Apis_RecordFields` rows, so it will fail the same way on its first ship notice. IM Houston is
|
|
94
|
+
> worth the same check.
|
|
95
|
+
|
|
96
|
+
Do **not** "fix" this by flipping `Core.RecordFields` 1441/1445 to `MATCH_CREATE`: Core ids are
|
|
97
|
+
identical in every tenant, so that grants mint capability to every client at once. The equivalent
|
|
98
|
+
change on the IF-side field (2184) was already proposed and rejected under CTO review. Per-API
|
|
99
|
+
overrides are the sanctioned lever. Note also the sibling trap documented in
|
|
100
|
+
[nested-relationship-writes](nested-relationship-writes.md): a row added **only** to set a child
|
|
101
|
+
policy, leaving `overrideIsIdentifier` NULL, silently strips that field as an identifier.
|
|
102
|
+
|
|
103
|
+
## Authenticate as the resolved Sender's client — never a hardcoded tenant
|
|
104
|
+
|
|
105
|
+
`findOrCreateShippingCarrier()` had two defects (fixed 2026-08-20; code is in the working tree and
|
|
106
|
+
**not yet committed**):
|
|
107
|
+
|
|
108
|
+
- **Cross-tenant write.** The carrier *lookup* correctly used `DB_CLIENT`, but the carrier *create*
|
|
109
|
+
call was authenticated with a **hardcoded Compass USA client uuid plus api credentials**, so a
|
|
110
|
+
carrier created for any other tenant was written into `Client_Compass`. Evidence found in prod:
|
|
111
|
+
15 `PRECI`, 5 `GANDT` and 3 `XXXX` junk carrier rows in `Client_Compass` created by **Compass
|
|
112
|
+
Canada** traffic, plus 4 Canada tracking numbers left with a NULL carrier.
|
|
113
|
+
- **Wrong response key.** It read the create response from `data->shippingCarrier` (singular) while
|
|
114
|
+
the API returns `data->shippingCarriers` (plural), so the new uuid came back `null` even on a
|
|
115
|
+
successful `201`.
|
|
116
|
+
|
|
117
|
+
Fix: store the authenticated client uuid and api uuid/secret on the component when the cXML `Sender`
|
|
118
|
+
credential is resolved, authenticate the internal call as **that** client, read the plural key with
|
|
119
|
+
the singular kept as a fallback, and add `findShippingCarrierUuidByCode()` as a DB read-back if
|
|
120
|
+
neither key parses. Verified locally end to end. Affects all six cXML gateway clients, not just
|
|
121
|
+
Compass Canada.
|
|
122
|
+
|
|
123
|
+
> **Rule for any new cXML helper that calls the API: take the client/api context from the resolved
|
|
124
|
+
> Sender credential.** A hardcoded tenant constant in this component is a cross-tenant data leak,
|
|
125
|
+
> and it fails silently, because the read side uses `DB_CLIENT` and therefore looks correct.
|
|
126
|
+
|
|
127
|
+
## Carrier resolution constraints
|
|
128
|
+
|
|
129
|
+
- `ShippingCarriers.code` holds **one** code per carrier, and the lookup is `code = X OR name = X`.
|
|
130
|
+
Several vendor codes therefore cannot map onto one carrier without duplicate rows — this is why
|
|
131
|
+
`Client_Compass` legitimately carries **7 separate FedEx rows**.
|
|
132
|
+
- `TrackingNumbers.shippingMethodId` is **nullable** and cXML never sends a shipping method, so the
|
|
133
|
+
cXML path needs **no** `ShippingMethods` row per carrier. The "every carrier needs its own Ground
|
|
134
|
+
method" requirement came from the retired CSV cron only — do not carry it into cXML provisioning.
|
|
135
|
+
|
|
136
|
+
## Gotchas / known issues
|
|
137
|
+
|
|
138
|
+
- **⚠ No alerting on cXML gateway 4xx/5xx.** The gateway emails nobody on rejection. The retired
|
|
139
|
+
Compass Canada CSV cron emailed the team on success, failure, wrong column count and empty file;
|
|
140
|
+
the cXML path that replaced it is silent. This is why a **67-day, 100%-rejection outage went
|
|
141
|
+
unnoticed** on Compass Canada (see the
|
|
142
|
+
[G&T ASN import doc](../../../clients/compass-canada/features/grand-and-toy-asn-import.md)).
|
|
143
|
+
Highest-value remaining fix on this path: alert on any non-2xx `Logs_<Client>.Api` row for the
|
|
144
|
+
cXML route.
|
|
145
|
+
- **`Apis.maxAuthsPerHour` throttles replays.** The gateway authenticates **once per document** and
|
|
146
|
+
the limit is `255`/hour, so a bulk cXML replay needs roughly **20-second spacing**. The JSON
|
|
147
|
+
`/v2/advance-shipping-notices` route reuses one Bearer token and needs no pacing.
|
|
148
|
+
- **A rejected document is fully retained**, so any cXML outage is recoverable by replay from
|
|
149
|
+
`Logs_<Client>.Api` — dedupe on the vendor's shipment/order/tracking identifiers, never on the
|
|
150
|
+
log row id.
|
|
151
|
+
|
|
152
|
+
## Testing the gateway locally (the only option while beta is down)
|
|
153
|
+
|
|
154
|
+
Beta api2 is unusable at composer bootstrap (see
|
|
155
|
+
[environment configuration and provisioning](../workflows/environment-configuration-and-provisioning.md)),
|
|
156
|
+
so gateway changes are verified on a local Laragon machine:
|
|
157
|
+
|
|
158
|
+
1. The api2 vhost already carries `ServerAlias cxml` and `*.cxml`, so `POST http://cxml/` serves the
|
|
159
|
+
local api2 checkout and the router's host parser resolves the host part to `cxml` and takes the
|
|
160
|
+
cXML branch.
|
|
161
|
+
2. `Core.DatabaseHosts` maps the `dev` environment to `localhost`, and `[api] _` points at
|
|
162
|
+
`http://api2/v2`, so internal API calls stay local.
|
|
163
|
+
3. The **local** `vendor/`'s `platform_check.php` requires only PHP 7.2.5, so it boots on Laragon's
|
|
164
|
+
PHP 8.1.x (unlike the deployed beta bundle).
|
|
165
|
+
4. **Insert the client's `Apis_RecordFields` overrides locally first.** A fresh local
|
|
166
|
+
`Client_<x>.Apis_RecordFields` is empty, so without them the test fails EV-12 for the *original*
|
|
167
|
+
provisioning reason and the change under test is never exercised.
|
|
168
|
+
|
|
169
|
+
## Change history
|
|
170
|
+
- 2026-08-20 — Documented the cXML ShipNotice path as its own subject after a 67-day Compass Canada
|
|
171
|
+
outage. Root cause: nested tracking-number creation is governed by per-API
|
|
172
|
+
`Apis_RecordFields.overrideChildPolicy`, and Canada's cXML API had zero rows while the Core
|
|
173
|
+
defaults for 1441/1445 are `MATCH` (only 1437 is `MATCH_CREATE`) — every ASN rejected EV-12 on
|
|
174
|
+
`trackingNumber`. Also fixed two cross-tenant defects in `findOrCreateShippingCarrier()` (hardcoded
|
|
175
|
+
Compass USA auth wrote carriers into `Client_Compass`; the response was read from the singular
|
|
176
|
+
`shippingCarrier` key instead of `shippingCarriers`). Recorded the onboarding provisioning rule
|
|
177
|
+
(PC Matic B2B has the same gap today), the carrier `code` and nullable-`shippingMethodId`
|
|
178
|
+
constraints, the missing 4xx/5xx alerting, the `maxAuthsPerHour` replay pacing, and the local
|
|
179
|
+
Laragon test procedure. (bala)
|
|
180
|
+
|
|
181
|
+
## Related docs
|
|
182
|
+
- [api2 architecture](../architecture.md) — host dispatch and the cXML branch.
|
|
183
|
+
- [Nested-relationship writes and child matching](nested-relationship-writes.md) — the
|
|
184
|
+
`Apis_RecordFields` override layer in full.
|
|
185
|
+
- [Tracking-number bridges](../../_underscore/features/tracking-number-bridges.md) — the bridge
|
|
186
|
+
records these policies apply to.
|
|
@@ -6,13 +6,14 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: ["bala", "mhammontree", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Client/ContactEmailAddress.php
|
|
14
14
|
related:
|
|
15
15
|
- ../architecture.md
|
|
16
|
+
- cxml-shipnotice-gateway.md
|
|
16
17
|
- ../../../clients/aig/features/entitlement-intake.md
|
|
17
18
|
- ../../ai-bdr/features/web-funnel-app.md
|
|
18
19
|
- ../../worker2/features/vapi-webhook-handler.md
|
|
@@ -169,6 +170,49 @@ columns matter:
|
|
|
169
170
|
> Correct the data with an explicit `overrideIsIdentifier = 1`, or (systemic fix, not yet done)
|
|
170
171
|
> change the code so only an explicit `0` removes an identifier and `NULL` means "no override".
|
|
171
172
|
|
|
173
|
+
### `overrideChildPolicy` decides create-vs-lookup — and a MISSING row is a silent 100% outage
|
|
174
|
+
|
|
175
|
+
`getChildPolicy()` (`V2.php` ~8556) resolves the effective policy as
|
|
176
|
+
**`Client_<x>.Apis_RecordFields.overrideChildPolicy` for the authenticating API, falling back to
|
|
177
|
+
`Core.RecordFields.childPolicy`**. Two consequences that are easy to miss:
|
|
178
|
+
|
|
179
|
+
- **The same payload behaves differently per API within one tenant.** Provisioning a record and its
|
|
180
|
+
ACLs is not enough; the API doing the writing needs its own override rows.
|
|
181
|
+
- **`Core` defaults are not uniform across sibling fields**, so "it works for the other client" is
|
|
182
|
+
not evidence the metadata is right. The ASN tracking bridges are the canonical example:
|
|
183
|
+
|
|
184
|
+
| Bridge field | RecordField | Core default |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| ASN **unit** bridge `trackingNumberId` | 1437 | `MATCH_CREATE` |
|
|
187
|
+
| ASN **item** bridge `trackingNumberId` | 1441 | `MATCH` |
|
|
188
|
+
| ASN **header** bridge `trackingNumberId` | 1445 | `MATCH` |
|
|
189
|
+
|
|
190
|
+
Compass USA's cXML API (`apiId 4`) overrides all three to `MATCH_CREATE`; Compass Canada's cXML API
|
|
191
|
+
(`apiId 3`) had **zero** rows, so its nested `trackingNumber` was looked up instead of created and
|
|
192
|
+
**100% of its ship notices were rejected EV-12 on field `trackingNumber` for 67 days** (root-caused
|
|
193
|
+
2026-08-20). Fixed by inserting the three override rows for `apiId 3`.
|
|
194
|
+
|
|
195
|
+
**Do not "fix" this class of bug by editing `Core.RecordFields`.** Core ids are identical in every
|
|
196
|
+
client DB, so flipping a base `childPolicy` to `MATCH_CREATE` grants mint capability to every tenant
|
|
197
|
+
at once — the reason the equivalent proposal on the IF-side field (2184) was rejected under CTO
|
|
198
|
+
review. Per-API overrides are the sanctioned lever. Full ASN case:
|
|
199
|
+
[cXML ShipNotice Gateway](cxml-shipnotice-gateway.md).
|
|
200
|
+
|
|
201
|
+
### `MATCH_UPSERT` on a nested FK creates the related record — including deep grandchildren
|
|
202
|
+
|
|
203
|
+
`MATCH_UPSERT` means "match on an identifier, otherwise create". So a nested object two levels deep
|
|
204
|
+
is created by the same write. Measured on an ASN POST (prod, 2026-08-20): RecordField **335**
|
|
205
|
+
(`advance-shipping-notice-item-units.unitId`) is `MATCH_UPSERT`, so a nested `unit` that matches
|
|
206
|
+
nothing is **created** and linked to the ASN item unit — no separate `/units` call is needed, and
|
|
207
|
+
interceptor 55 then propagates it to `ItemFulfillmentItemUnits`.
|
|
208
|
+
|
|
209
|
+
The trap is one level deeper: **`units.itemId` is itself `MATCH_UPSERT`**. Referencing the item by a
|
|
210
|
+
non-identifier value (e.g. a `partNumber`) or by a mistyped uuid **silently creates a junk `Items`
|
|
211
|
+
row** rather than failing. Always reference an existing parent record **by `uuid`**, and resolve
|
|
212
|
+
business keys to uuids in the caller. (Also note `Units.itemId` is NOT NULL, so the item cannot be
|
|
213
|
+
omitted; and the `Units` identifier set is `uuid`/`itemId`/`serialNumber`/`assetTag`/`vendorId`/
|
|
214
|
+
`macAddress`, so item + serialNumber is what gives find-or-create on a device.)
|
|
215
|
+
|
|
172
216
|
### Fix it with a migration — a manual one-row data patch on non-prod DOES NOT SURVIVE
|
|
173
217
|
|
|
174
218
|
The TRUE-79978 beta fix was applied by hand as a single `UPDATE` on 2026-07-09. The **identical
|
|
@@ -260,6 +304,16 @@ anywhere — no `messages[]`, nothing in the client `Logs` or the core/writer `L
|
|
|
260
304
|
|
|
261
305
|
## Change history
|
|
262
306
|
|
|
307
|
+
- 2026-08-20 — Documented that **`overrideChildPolicy` (not just `overrideIsIdentifier`) is per-API
|
|
308
|
+
provisioning**, and that a *missing* `Apis_RecordFields` row is a silent 100%-rejection outage:
|
|
309
|
+
Compass Canada's cXML API had zero rows and the Core defaults for the ASN **item**/**header**
|
|
310
|
+
tracking bridges (1441/1445) are `MATCH` while the **unit** bridge (1437) is `MATCH_CREATE`, so
|
|
311
|
+
every ship notice failed EV-12 on `trackingNumber` for 67 days. Added the rule that this class of
|
|
312
|
+
bug must be fixed with per-API overrides, never by editing `Core.RecordFields` (Core ids are shared
|
|
313
|
+
by every tenant). Also documented `MATCH_UPSERT` deep creation — a nested `unit` on an ASN POST is
|
|
314
|
+
created outright (RecordField 335), and because `units.itemId` is *also* `MATCH_UPSERT`, a
|
|
315
|
+
non-identifier or mistyped item reference silently creates a junk `Items` row, so parents must be
|
|
316
|
+
referenced by `uuid`. (bala)
|
|
263
317
|
- 2026-08-17 (later pass) — Two new measured behaviors from the TRUE-80562 verification run on beta
|
|
264
318
|
`Client_Aig`. (1) **CREATE-path re-parenting:** a nested forward singular FK
|
|
265
319
|
(`primaryContactEmailAddress` / `primaryContactPhoneNumber`) is matched **tenant-wide by VALUE**,
|
|
@@ -6,10 +6,12 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Config/<environment>.ini
|
|
13
|
+
- api2/composer.json
|
|
14
|
+
- api2/composer.lock
|
|
13
15
|
- api2/Controller/Index.php
|
|
14
16
|
- dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql
|
|
15
17
|
- dbchanges2/Logs/
|
|
@@ -103,7 +105,38 @@ Diagnostic order:
|
|
|
103
105
|
`Logs` schema was the *only* thing missing. Don't chase seed/config/connectivity when only base `Logs`
|
|
104
106
|
is absent.
|
|
105
107
|
|
|
108
|
+
## Edge case — a deployed `vendor/` built against the wrong PHP version kills the whole env
|
|
109
|
+
|
|
110
|
+
**Symptom (beta api2, live 2026-08-20): every request fails, with no routing at all.**
|
|
111
|
+
`vendor/composer/platform_check.php` demands **PHP >= 8.5.0** while the instance runs **8.1.12**, so
|
|
112
|
+
it fatals inside the composer autoload at `Controller/Index.php:41` — **before** any routing, logging
|
|
113
|
+
or error envelope. No Sentry event that looks like a request, nothing in `Logs.Api`.
|
|
114
|
+
|
|
115
|
+
**It is not a code bug and not a config bug — the deployed artifact does not match the repo.**
|
|
116
|
+
`composer.json` pins `php >= 8.2`, and `composer.lock` is **byte-identical** on `_beta` and
|
|
117
|
+
`_production` (same content-hash, `phpmailer v7.1.1`, nothing requiring 8.5). So the deployed
|
|
118
|
+
`vendor/` was **not built from the committed lock**.
|
|
119
|
+
|
|
120
|
+
Diagnosis order when an environment fails with *no* log line at all:
|
|
121
|
+
1. `head vendor/composer/platform_check.php` on the instance and compare its required version
|
|
122
|
+
against `php -v`.
|
|
123
|
+
2. Compare `composer.lock` content-hash across branches; if they match, suspect the build, not the
|
|
124
|
+
repo.
|
|
125
|
+
|
|
126
|
+
**Fix is DevOps, not application code:** raise the EB platform to at least PHP 8.2 **and** rebuild
|
|
127
|
+
`vendor/` with `composer install` (never `composer update`, which would re-resolve against the
|
|
128
|
+
builder's PHP and can re-introduce an 8.5 constraint). Until beta is repaired, gateway/API changes
|
|
129
|
+
have to be verified locally — see
|
|
130
|
+
[cXML ShipNotice Gateway → testing locally](../features/cxml-shipnotice-gateway.md).
|
|
131
|
+
|
|
106
132
|
## Change history
|
|
133
|
+
- 2026-08-20 — Recorded that **beta api2 is down at composer bootstrap**:
|
|
134
|
+
`vendor/composer/platform_check.php` requires PHP >= 8.5.0 while the instance runs 8.1.12, fataling
|
|
135
|
+
in `Controller/Index.php:41` before any routing, so all beta api2 requests fail with nothing
|
|
136
|
+
logged. `composer.json` pins `php >= 8.2` and `composer.lock` is byte-identical on `_beta` and
|
|
137
|
+
`_production`, so the deployed `vendor/` was not built from the committed lock. Fix is DevOps
|
|
138
|
+
(raise the EB platform to >= 8.2 and rebuild with `composer install`, not `update`). Added the
|
|
139
|
+
no-log-at-all diagnosis order. (bala)
|
|
107
140
|
- 2026-07-20 — `sandbox-client` API 500'd on every request (Route.php:525 view fatal). Root cause: the
|
|
108
141
|
base `Logs` schema (`DB_LOGS`, `Core.Databases` id 11; tables Api/Error/Webhook) was missing on the
|
|
109
142
|
all-in-one cluster although every `Logs_<Client>` existed. `_Controller_Index::api()` queries
|
package/knowledge/INDEX.md
CHANGED
|
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
4
4
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
|
-
- **library** (Library) _(framework core)_ —
|
|
7
|
+
- **library** (Library) _(framework core)_ — 19 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
8
|
- **worker** (Worker) — 25 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
@@ -20,7 +20,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
20
20
|
|
|
21
21
|
- **_underscore** (_Underscore) _(framework core)_ — 60 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 53 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
|
-
- **api2** (API) —
|
|
23
|
+
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
24
|
- **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 7 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [French (fr-CA) Item Feature Translations (Compass Canada)](features/french-item-feature-translations.md) | 2.0 | Renders item **feature** text on the Compass Canada French storefront — feature names, feature-group headers (e.g. | _underscore/Model/Compass/Canada/Feature.php, _underscore/Model/Compass/Canada/ItemCategoryFeatureGroup.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, worker2/Worker/Etilize/ItemTranslations.php, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql, dbchanges2/Client_CompassCanada/2026-07-13 - FeatureAttributeCustomFields.sql, dbchanges2/Client_CompassCanada/2026-07-13 - DedupeItemFeaturesAndGroups.sql, dbchanges2/Client_CompassCanada/2026-07-13 - SeedFrenchFeatureTranslations.sql |
|
|
6
6
|
| [French (fr-CA) Order Email Localization (Compass Canada)](features/french-order-email-localization.md) | 2.0 | Compass Canada order emails (order requested, manager-approval request, approved/rejected, in-transit, delivered, reminders) are sent in **each recipient's own | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/String.php, library/app/client/compasscanada.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_cancel_pending_approval_orders.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_email_previews_prod.php |
|
|
7
|
-
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
7
|
+
| [Grand & Toy ASN Import (Compass Canada)](features/grand-and-toy-asn-import.md) | 2.0 | Imports Grand & Toy (G&T) Advance Shipping Notices for Compass Canada. | api2/Component/Api/Cxml/Cxml.php, worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php, worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php, worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/Canada/AdvanceShippingNotice.php, dbchanges2/Client_CompassCanada/ |
|
|
8
8
|
| [Compass Canada](profile.md) | 2.0 | Compass Canada is the Canadian arm of the Compass account — a separate TOGA tenant, related to but distinct from Compass USA. | |
|
|
9
|
+
| [Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)](workflows/grand-and-toy-asn-backfill.md) | 2.0 | How to recover Compass Canada shipments whose ASN never landed — used on 2026-08-20 to backfill the 67-day Grand & Toy outage (**518 shipments**: 171 via cXML r | worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php, api2/Component/Api/Cxml/Cxml.php, _underscore/Model/Compass/AdvanceShippingNotice.php |
|
|
@@ -5,17 +5,22 @@ project: _Underscore
|
|
|
5
5
|
client: compass-canada
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-08-20
|
|
9
9
|
owners: ["bala"]
|
|
10
10
|
files:
|
|
11
|
+
- api2/Component/Api/Cxml/Cxml.php
|
|
11
12
|
- worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php
|
|
12
13
|
- worker/crons/toga2/compasscanada/workflow/import_grand_and_toy_asn_from_file.php
|
|
14
|
+
- worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php
|
|
13
15
|
- worker/schedules/cron.worker.sync.json
|
|
14
16
|
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
15
17
|
- _underscore/Model/Compass/Canada/AdvanceShippingNotice.php
|
|
16
18
|
- dbchanges2/Client_CompassCanada/
|
|
17
19
|
related:
|
|
18
20
|
- ../profile.md
|
|
21
|
+
- ../workflows/grand-and-toy-asn-backfill.md
|
|
22
|
+
- ../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
|
|
23
|
+
- ../../compass-usa/features/asn-to-item-fulfillment.md
|
|
19
24
|
---
|
|
20
25
|
|
|
21
26
|
## Summary
|
|
@@ -25,6 +30,40 @@ G&T's ASN CSV from a mailbox, posts each shipped line to the 2.0 API as an Advan
|
|
|
25
30
|
an in-transit email in English or French. It mirrors the Compass USA ODP / Strategic-Systems ASN
|
|
26
31
|
flow, adapted for Canadian carriers and bilingual email.
|
|
27
32
|
|
|
33
|
+
**The live path today is cXML, not the CSV cron.** G&T transmits ship notices to the api2 cXML
|
|
34
|
+
gateway (`cxml-writer`), which translates them into the same `POST /v2/advance-shipping-notices`.
|
|
35
|
+
The 24-column CSV cron below is **retired** — it remains documented because it is the only source of
|
|
36
|
+
serial numbers and the reference for the field mapping.
|
|
37
|
+
|
|
38
|
+
> ### Outage 2026-03 to 2026-08-20 — two stacked defects, 518 shipments lost
|
|
39
|
+
>
|
|
40
|
+
> **100% of G&T ship notices were rejected from 2026-06-11**, and most since March. Nothing alerted,
|
|
41
|
+
> because the cXML gateway emails nobody on a 4xx (the retired CSV cron did).
|
|
42
|
+
>
|
|
43
|
+
> 1. **Before 2026-06-11** the translator sent G&T's carrier **CODE** in a field matched against
|
|
44
|
+
> `ShippingCarriers.NAME`. Only `UPS` and `DHL` resolved (code happens to equal the name); every
|
|
45
|
+
> other carrier failed **EV-12 on field `shippingCarrier`**. So the feed was already losing most
|
|
46
|
+
> shipments while looking healthy.
|
|
47
|
+
> 2. **On 2026-06-11** the ShipControl rework (api2 commits `9259a10`, `8c987b0`) moved tracking onto
|
|
48
|
+
> the item/header bridges (RecordFields 1441/1445) and dropped the carrier from the header
|
|
49
|
+
> fallback. Those two bridge fields default to `childPolicy = MATCH` in Core and Compass Canada's
|
|
50
|
+
> cXML API had **no** `Apis_RecordFields` overrides, so the nested `trackingNumber` was looked up
|
|
51
|
+
> instead of created and every ASN failed **EV-12 on field `trackingNumber`**. Failure became
|
|
52
|
+
> total. Root-cause mechanics:
|
|
53
|
+
> [cXML ShipNotice Gateway](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md).
|
|
54
|
+
>
|
|
55
|
+
> Recovery procedure: [G&T ASN Backfill](../workflows/grand-and-toy-asn-backfill.md).
|
|
56
|
+
|
|
57
|
+
> ### ⚠ MIGRATION DEBT — the 2026-08-20 fixes exist ONLY in production
|
|
58
|
+
>
|
|
59
|
+
> The three `Client_CompassCanada.Apis_RecordFields` rows (`apiId 3`, `recordFieldId` 1437/1441/1445,
|
|
60
|
+
> `overrideChildPolicy = MATCH_CREATE`) and the carrier code/row changes were applied **directly to
|
|
61
|
+
> the production database**. There is **no `dbchanges2/Client_CompassCanada/` migration yet**, so
|
|
62
|
+
> beta, dev-sandbox, and any rebuilt or reseeded environment still fail for the original reason.
|
|
63
|
+
> Proven this session: the local `Client_CompassCanada.Apis_RecordFields` is empty and reproduced the
|
|
64
|
+
> June failure exactly. Write the migration before anything else touches this feed. (A manual
|
|
65
|
+
> non-prod patch also evaporates on the next tenant reseed — the same lesson as AIG TRUE-80562.)
|
|
66
|
+
|
|
28
67
|
## Key files / entry points
|
|
29
68
|
- `worker/crons/toga2/compasscanada/workflow/4_import_grand_and_toy_advance_shipping_notices.php`
|
|
30
69
|
— the scheduled cron (1.0 worker tier). Reads mailbox `compasscanada.status@togatech.com`
|
|
@@ -74,11 +113,84 @@ flow, adapted for Canadian carriers and bilingual email.
|
|
|
74
113
|
- Interceptor wiring: `Core.RecordScripts` + `Core.ApiPayloadInterceptors`, `recordId 55`
|
|
75
114
|
(AdvanceShippingNotice). The `sendEmail` POST record script is `Core.RecordScripts recordId 202`.
|
|
76
115
|
|
|
116
|
+
## The cXML documents G&T actually sends
|
|
117
|
+
|
|
118
|
+
Measured across **all 1,074** stored documents in `Logs_CompassCanada.Api` — the shape never varies:
|
|
119
|
+
|
|
120
|
+
- **No `ShipControl` block at all.** The ShipControl parser never fires for this vendor. Carrier and
|
|
121
|
+
tracking arrive on `ShipNoticeHeader` as `CarrierIdentifier` > `CarrierName` and `TrackingID`.
|
|
122
|
+
- **No `Extrinsic` of any kind** — so no per-line tracking and no `TrackingURL`.
|
|
123
|
+
- **No serial numbers and no asset tags.**
|
|
124
|
+
- **All `operation="new"`.**
|
|
125
|
+
- **One `TrackingID` even when `TotalPackages > 1`.**
|
|
126
|
+
|
|
127
|
+
**Consequence:** the retired CSV feed was the only source of serial numbers, so ongoing cXML ASNs
|
|
128
|
+
create **no `Units`**. If Compass Canada expect per-device serial visibility, that needs a business
|
|
129
|
+
decision — it cannot be recovered from the cXML.
|
|
130
|
+
|
|
131
|
+
## Carriers (cXML)
|
|
132
|
+
|
|
133
|
+
G&T's complete carrier code set, harvested from all 1,074 documents:
|
|
134
|
+
|
|
135
|
+
| Code | Carrier |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `UPS` | UPS |
|
|
138
|
+
| `FDXH` | FedEx |
|
|
139
|
+
| `PUROL` | Purolator |
|
|
140
|
+
| `PRECI` | Precision |
|
|
141
|
+
| `NATEX` | Nationex |
|
|
142
|
+
| `GANDT` | Grand & Toy own van |
|
|
143
|
+
| `DHL` | DHL |
|
|
144
|
+
| `XXXX` | placeholder — its `TrackingID` is the literal string `DIRECT` |
|
|
145
|
+
|
|
146
|
+
Applied 2026-08-20: added the missing `code` values `PRECI` (Precision) and `NATEX` (Nationex), and
|
|
147
|
+
created a **Grand & Toy** carrier row in `Client_CompassCanada.ShippingCarriers`.
|
|
148
|
+
|
|
149
|
+
- `ShippingCarriers.code` holds **one** code per carrier and the lookup matches `code = X OR
|
|
150
|
+
name = X`, so several vendor codes cannot map onto one carrier without duplicate rows (this is why
|
|
151
|
+
`Client_Compass` has 7 separate FedEx rows).
|
|
152
|
+
- **No Ground `ShippingMethod` is needed for the cXML path.** `TrackingNumbers.shippingMethodId` is
|
|
153
|
+
nullable and cXML never sends a method. The "each carrier needs its own Ground method" rule below
|
|
154
|
+
came from the retired CSV cron only.
|
|
155
|
+
|
|
156
|
+
## Serial numbers in the same ASN POST (no separate /units call)
|
|
157
|
+
|
|
158
|
+
A nested `unit` object inside the ASN POST creates the `Units` row outright: RecordField **335**
|
|
159
|
+
(`advance-shipping-notice-item-units.unitId`) is `MATCH_UPSERT`, so a unit that does not match is
|
|
160
|
+
created and linked to the ASNIU, and interceptor **55** then propagates it to
|
|
161
|
+
`ItemFulfillmentItemUnits`. Verified in prod (Units row created + linked, IFIU populated).
|
|
162
|
+
|
|
163
|
+
Constraints:
|
|
164
|
+
- `Units.itemId` is **NOT NULL**, so the item must be supplied.
|
|
165
|
+
- The `Units` identifiers are `uuid`, `itemId`, `serialNumber`, `assetTag`, `vendorId`, `macAddress`
|
|
166
|
+
— so **item + serialNumber** gives find-or-create on that pair.
|
|
167
|
+
- `units.itemId` is itself `MATCH_UPSERT`, so **reference the item by `uuid`, never by
|
|
168
|
+
`partNumber`** — a typo silently creates a junk `Items` row.
|
|
169
|
+
- Resolve the item **PO-scoped**: `PurchaseOrderItems` → `VendorItems.vendorPartNumber` →
|
|
170
|
+
`Items.uuid`, because the G&T "Part Number" column holds the **vendor** part number.
|
|
171
|
+
|
|
77
172
|
## Client variations
|
|
78
173
|
Compass-Canada-specific; parallels Compass USA's ODP / Strategic-Systems ASN import. Canada sends
|
|
79
174
|
a bilingual EN/FR in-transit email (USA is English only) and uses Canadian carriers.
|
|
80
175
|
|
|
81
176
|
## Gotchas / known issues
|
|
177
|
+
- **⚠ `PurchaseOrderItems_SalesOrderItems` is EMPTY across ALL 865 Compass Canada purchase orders —
|
|
178
|
+
this is NORMAL and must NOT be "repaired".** `_Model_Compass_AdvanceShippingNotice::postPost`
|
|
179
|
+
resolves PO to SO without that bridge for this client. Inserting bridge rows to make one ASN
|
|
180
|
+
succeed would change fulfilment behaviour for that PO. If an ASN fails, the cause is elsewhere
|
|
181
|
+
(child policy, carrier, or the already-tracked-line `EO-1` below).
|
|
182
|
+
- **⚠ Nothing alerts on a rejected ship notice.** The cXML gateway emails nobody on 4xx/5xx; the
|
|
183
|
+
retired CSV cron emailed `vburks@togatech.com` + `devTeam@togatech.com` on success, failure, wrong
|
|
184
|
+
column count and empty file. That asymmetry is why this outage ran 67 days unnoticed, and adding
|
|
185
|
+
alerting is the highest-value remaining fix. See the
|
|
186
|
+
[cXML gateway doc](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md).
|
|
187
|
+
- **A line whose only `ItemFulfillmentItem` already carries item-level tracking makes the ASN POST
|
|
188
|
+
return 500 `EO-1`, not a validation error.** `remaining = 0` routes to the reconcile path,
|
|
189
|
+
`itemFulfillmentItemHasTracking()` skips the IFI, and the handler then fails instead of completing
|
|
190
|
+
cleanly. Deterministic (reproduced three times, both transports; PO `50305097-1` line 4, Sentry
|
|
191
|
+
`AV-30` / `69-68`). It is in the **shared Compass parent**, so Office Depot re-shipments on
|
|
192
|
+
already-tracked lines hit it too — see
|
|
193
|
+
[Compass ASN to ItemFulfillment](../../compass-usa/features/asn-to-item-fulfillment.md).
|
|
82
194
|
- **CSV "Part Number" is the VENDOR part number (`VendorItems.vendorPartNumber`), NOT
|
|
83
195
|
`Items.partNumber` directly.** Resolve the item through the `VendorItems` join. (e.g. vendor
|
|
84
196
|
`IMFP3260` → `Items.partNumber` `C2CY5UC#ABA`.)
|
|
@@ -116,6 +228,22 @@ a bilingual EN/FR in-transit email (USA is English only) and uses Canadian carri
|
|
|
116
228
|
before `fetchOne()`.
|
|
117
229
|
|
|
118
230
|
## Change history
|
|
231
|
+
- 2026-08-20 — **Root-caused and fixed a 67-day, 100%-rejection outage on the G&T cXML ASN feed and
|
|
232
|
+
backfilled 518 shipments.** Root cause: nested tracking-number creation is governed by the
|
|
233
|
+
per-API `Client_<x>.Apis_RecordFields.overrideChildPolicy`, and Canada's "Grand & Toy (Cxml)" API
|
|
234
|
+
(`apiId 3`) had **zero** rows while the Core defaults for RecordFields 1441 (item bridge) and 1445
|
|
235
|
+
(header bridge) are `MATCH` (only 1437 is `MATCH_CREATE`) — so every ASN failed EV-12 on
|
|
236
|
+
`trackingNumber` after the 2026-06-11 ShipControl rework moved tracking onto those bridges. Before
|
|
237
|
+
that date a separate defect sent the carrier CODE into a NAME match, so only UPS and DHL resolved.
|
|
238
|
+
Applied to prod: three `Apis_RecordFields` `MATCH_CREATE` rows, the missing `PRECI`/`NATEX` carrier
|
|
239
|
+
codes, and a Grand & Toy carrier row. Also fixed the cross-tenant `findOrCreateShippingCarrier()`
|
|
240
|
+
defect in api2 (carriers for other tenants were written into `Client_Compass`). Backfilled 518
|
|
241
|
+
shipments (536 ASNs / 512 TrackingNumbers / 406 ItemFulfillments / 332 Units). Documented G&T's
|
|
242
|
+
invariant cXML shape (no ShipControl, no Extrinsic, no serials, one TrackingID per shipment), the
|
|
243
|
+
in-POST serial-number path (RecordField 335 `MATCH_UPSERT`), the empty
|
|
244
|
+
`PurchaseOrderItems_SalesOrderItems` invariant, the missing gateway alerting, and the outstanding
|
|
245
|
+
`EO-1` already-tracked-line bug. **⚠ All DB changes are prod-only — no dbchanges2 migration yet.**
|
|
246
|
+
(bala)
|
|
119
247
|
- 2026-06-22 — Full variable rename (no abbreviations); moved PO/carrier/shipping-method lookup maps
|
|
120
248
|
outside the CSV row loop (was causing N full-table queries per file); fixed ASNIU→TN link to run
|
|
121
249
|
unconditionally for new ASNIUs (not only when TN was new); added PHP 8 null-coalesce on carrier→
|
|
@@ -127,3 +255,6 @@ a bilingual EN/FR in-transit email (USA is English only) and uses Canadian carri
|
|
|
127
255
|
|
|
128
256
|
## Related docs
|
|
129
257
|
- [Compass Canada profile](../profile.md)
|
|
258
|
+
- [G&T ASN Backfill workflow](../workflows/grand-and-toy-asn-backfill.md)
|
|
259
|
+
- [cXML ShipNotice Gateway (api2)](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md)
|
|
260
|
+
- [Compass ASN to ItemFulfillment (shared handler)](../../compass-usa/features/asn-to-item-fulfillment.md)
|
|
@@ -16,11 +16,14 @@ project: _Underscore
|
|
|
16
16
|
client: compass-canada
|
|
17
17
|
type: profile
|
|
18
18
|
status: active
|
|
19
|
-
updated: 2026-08-
|
|
19
|
+
updated: 2026-08-20
|
|
20
20
|
owners: [jcardinal, bala, tcox, apeterson]
|
|
21
21
|
files: []
|
|
22
22
|
related:
|
|
23
23
|
- ../compass-usa/profile.md
|
|
24
|
+
- features/grand-and-toy-asn-import.md
|
|
25
|
+
- workflows/grand-and-toy-asn-backfill.md
|
|
26
|
+
- ../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
|
|
24
27
|
- ../compass-usa/features/approval-decision-flow.md
|
|
25
28
|
- ../compass-usa/features/isfulfillable-data-quality-and-type-rule.md
|
|
26
29
|
- features/french-order-email-localization.md
|
|
@@ -47,8 +50,21 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
47
50
|
- **Grand & Toy (G&T)** — primary hardware vendor. SOs flow toga → MITS → PO to G&T; G&T sends
|
|
48
51
|
back ASNs. ASN ingestion (email CSV + the auto-created ItemFulfillment chain + bilingual
|
|
49
52
|
in-transit email) is documented in [Grand & Toy ASN Import](features/grand-and-toy-asn-import.md).
|
|
50
|
-
Vendor uuid: `App_Client_CompassCanada::UUID_VENDOR__GRAND_TOY`.
|
|
51
|
-
|
|
53
|
+
Vendor uuid: `App_Client_CompassCanada::UUID_VENDOR__GRAND_TOY`. **The live ASN path is cXML**
|
|
54
|
+
(api2 `cxml-writer`), not the retired 24-column CSV cron; carrier codes G&T sends are `UPS`,
|
|
55
|
+
`FDXH`, `PUROL`, `PRECI`, `NATEX`, `GANDT`, `DHL`, `XXXX` (placeholder, TrackingID = `DIRECT`).
|
|
56
|
+
The cXML path needs **no** Ground `ShippingMethod` per carrier (`TrackingNumbers.shippingMethodId`
|
|
57
|
+
is nullable and cXML sends no method) — that requirement came from the CSV cron only.
|
|
58
|
+
- **⚠ The G&T ASN feed rejected 100% of ship notices for 67 days (2026-06-11 → 2026-08-20) and
|
|
59
|
+
nothing alerted.** Root cause: nested tracking-number creation is governed by
|
|
60
|
+
`Client_CompassCanada.Apis_RecordFields.overrideChildPolicy` per API, and the "Grand & Toy (Cxml)"
|
|
61
|
+
API (`apiId 3`) had zero rows. 518 shipments were backfilled. **The fix is prod-only — there is no
|
|
62
|
+
`dbchanges2/Client_CompassCanada/` migration yet, so beta / dev-sandbox / any reseed still fails.**
|
|
63
|
+
See [G&T ASN Import](features/grand-and-toy-asn-import.md) and the
|
|
64
|
+
[backfill workflow](workflows/grand-and-toy-asn-backfill.md).
|
|
65
|
+
- **`PurchaseOrderItems_SalesOrderItems` is empty for all 865 Canada POs — that is normal for this
|
|
66
|
+
client.** `postPost` resolves PO to SO without it; never insert bridge rows to "repair" an ASN
|
|
67
|
+
failure here.
|
|
52
68
|
- Worker crons for the G&T flow live under `worker/crons/toga2/compasscanada/workflow/`
|
|
53
69
|
(`1_…` transmit SOs to MITS, `2_…` transmit POs to vendors, `3_…` status from G&T cXML,
|
|
54
70
|
`4_…` import G&T ASNs).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Grand & Toy ASN Backfill (cXML replay + CSV-to-JSON)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: _Underscore
|
|
5
|
+
client: compass-canada
|
|
6
|
+
type: workflow
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-08-20
|
|
9
|
+
owners: ["bala"]
|
|
10
|
+
files:
|
|
11
|
+
- worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php
|
|
12
|
+
- api2/Component/Api/Cxml/Cxml.php
|
|
13
|
+
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
14
|
+
related:
|
|
15
|
+
- ../features/grand-and-toy-asn-import.md
|
|
16
|
+
- ../profile.md
|
|
17
|
+
- ../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
How to recover Compass Canada shipments whose ASN never landed — used on 2026-08-20 to backfill the
|
|
23
|
+
67-day Grand & Toy outage (**518 shipments**: 171 via cXML replay + 347 via JSON; result **536 ASNs,
|
|
24
|
+
512 TrackingNumbers, 406 ItemFulfillments, 332 Units**). Two complementary sources, because neither
|
|
25
|
+
alone is complete: the stored cXML documents cover what G&T actually transmitted, and the G&T CSV
|
|
26
|
+
covers everything else (and is the only source of serial numbers).
|
|
27
|
+
|
|
28
|
+
**Run the root-cause fix first.** A backfill against an unprovisioned tenant just re-creates the
|
|
29
|
+
original EV-12 rejections — see
|
|
30
|
+
[G&T ASN Import](../features/grand-and-toy-asn-import.md) and the
|
|
31
|
+
[cXML gateway provisioning rule](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md).
|
|
32
|
+
|
|
33
|
+
## Route A — replay the stored cXML
|
|
34
|
+
|
|
35
|
+
1. **Source.** Every rejected document is retained complete in `Logs_CompassCanada.Api`
|
|
36
|
+
(`direction = IN`, `route = '/'`, `method = POST`). Nothing was lost by the outage.
|
|
37
|
+
2. **Dedupe** on `shipmentID` + `orderID` + `TrackingID`. G&T re-sends, and the log holds every
|
|
38
|
+
attempt.
|
|
39
|
+
3. **Re-POST** the document body verbatim to `cxml-writer.togahub.com` as `text/xml`.
|
|
40
|
+
The `Sender` credential is **inside the document**, so no `Authorization` header is needed or read
|
|
41
|
+
— never extract or handle the credential.
|
|
42
|
+
4. **Pace it ~20 seconds per document.** `Apis.maxAuthsPerHour` is `255` and the gateway
|
|
43
|
+
authenticates once per document.
|
|
44
|
+
|
|
45
|
+
## Route B — build JSON ASNs from the G&T CSV
|
|
46
|
+
|
|
47
|
+
1. POST to `/v2/advance-shipping-notices` with a Bearer token (one token for the whole run; no
|
|
48
|
+
pacing needed, the auth limit does not apply).
|
|
49
|
+
2. **The payload must carry all three tracking bridges** or the partial in-transit email loses its
|
|
50
|
+
per-package item breakdown:
|
|
51
|
+
- `advanceShippingNoticeTrackingNumbers` (header)
|
|
52
|
+
- `advanceShippingNoticeItemTrackingNumbers` (item)
|
|
53
|
+
- `advanceShippingNoticeItemUnits[].advanceShippingNoticeItemUnitTrackingNumbers` (unit)
|
|
54
|
+
3. Serial numbers can be created **inside the same POST** (nested `unit` object) — no separate
|
|
55
|
+
`/units` call. Resolve the item **PO-scoped** through `PurchaseOrderItems` →
|
|
56
|
+
`VendorItems.vendorPartNumber` → `Items.uuid`, because the G&T "Part Number" column holds the
|
|
57
|
+
**vendor** part number, and reference the item **by uuid only**. See the ASN-import doc's
|
|
58
|
+
serial-number section for the constraints.
|
|
59
|
+
|
|
60
|
+
## Dedupe and safety guards
|
|
61
|
+
|
|
62
|
+
- **`TrackingNumbers.number` carries a UNIQUE key**, so a replayed tracking number `MATCH`es rather
|
|
63
|
+
than duplicating. **But a repeated document still creates a duplicate ASN.** Always skip any
|
|
64
|
+
shipment whose tracking number already exists before posting.
|
|
65
|
+
- **Suppress stale customer emails.** Stamp `TrackingNumbers.c_dtInTransitEmailSent`, keyed off
|
|
66
|
+
`AdvanceShippingNotices.dateShipped` older than **3 days**, so a genuinely fresh shipment still
|
|
67
|
+
emails the customer while months-old backfilled shipments stay silent.
|
|
68
|
+
- Verify by row count afterwards (ASNs / TrackingNumbers / ItemFulfillments / Units), never by HTTP
|
|
69
|
+
status — a 201 only proves the parent was written.
|
|
70
|
+
|
|
71
|
+
## Systems involved
|
|
72
|
+
|
|
73
|
+
- `Logs_CompassCanada.Api` (cXML source of truth), `cxml-writer.togahub.com` (gateway),
|
|
74
|
+
`/v2/advance-shipping-notices` (JSON route), `Client_CompassCanada` (ASN/IF/Units/TrackingNumbers),
|
|
75
|
+
the shared `_Model_Compass_AdvanceShippingNotice::postPost` interceptor.
|
|
76
|
+
- Script: `worker/crons/toga2/compasscanada/workflow_beta/import_grand_and_toy_asn_from_file.php`.
|
|
77
|
+
|
|
78
|
+
## Edge cases
|
|
79
|
+
|
|
80
|
+
- **A line whose only ItemFulfillmentItem already carries item-level tracking makes `postPost`
|
|
81
|
+
return 500 `EO-1`** (not a validation error) — the reconcile path skips the IFI and the handler
|
|
82
|
+
then fails instead of completing. Deterministic; hit three times across both transports (PO
|
|
83
|
+
`50305097-1` line 4; Sentry `AV-30`, `69-68`). Skip and record those shipments; the open bug is
|
|
84
|
+
tracked on the [shared Compass ASN handler doc](../../compass-usa/features/asn-to-item-fulfillment.md).
|
|
85
|
+
- **G&T `XXXX` carrier code** carries the literal `DIRECT` as its `TrackingID` — it is a placeholder,
|
|
86
|
+
not a real tracking number.
|
|
87
|
+
- **One `TrackingID` even when `TotalPackages > 1`** — do not synthesize per-package numbers.
|
|
88
|
+
|
|
89
|
+
## Change history
|
|
90
|
+
- 2026-08-20 — Documented the two-route backfill after recovering the 67-day G&T outage (518
|
|
91
|
+
shipments / 536 ASNs / 512 TrackingNumbers / 406 ItemFulfillments / 332 Units). Recorded the cXML
|
|
92
|
+
replay source and the ~20s `maxAuthsPerHour` pacing, the three-bridge JSON payload requirement,
|
|
93
|
+
the tracking-number-exists dedupe guard, the `c_dtInTransitEmailSent` + 3-day email suppression,
|
|
94
|
+
and the `EO-1` already-tracked-line edge case. (bala)
|
|
95
|
+
|
|
96
|
+
## Related docs
|
|
97
|
+
- [Grand & Toy ASN Import](../features/grand-and-toy-asn-import.md) — the ongoing feed and its root
|
|
98
|
+
cause history.
|
|
99
|
+
- [cXML ShipNotice Gateway](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md) — provisioning
|
|
100
|
+
and replay mechanics.
|
|
101
|
+
</content>
|
|
@@ -5,7 +5,7 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
8
|
+
updated: 2026-08-20
|
|
9
9
|
owners: [jcardinal, bala]
|
|
10
10
|
files:
|
|
11
11
|
- _underscore/Model/Compass/AdvanceShippingNotice.php
|
|
@@ -20,6 +20,8 @@ files:
|
|
|
20
20
|
- dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql
|
|
21
21
|
- dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql
|
|
22
22
|
related:
|
|
23
|
+
- ../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md
|
|
24
|
+
- ../../compass-canada/features/grand-and-toy-asn-import.md
|
|
23
25
|
- ../../../2.0/apps/_underscore/features/recursive-item-fulfillments.md
|
|
24
26
|
- ../../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
|
|
25
27
|
- ../../../2.0/apps/_underscore/features/model-save-parent-cascade-stored-field-deadlock.md
|
|
@@ -111,6 +113,29 @@ Compass Canada (`Model/Compass/Canada/`) is a separate sub-client. The Compass c
|
|
|
111
113
|
(Core `ClientApiIdentities` identity `153531108`, clientId 2) holds roles 1 and 3.
|
|
112
114
|
|
|
113
115
|
## Gotchas / known issues
|
|
116
|
+
- **⚠ OPEN BUG — an already-tracked, fully-fulfilled line makes `postPost` return 500 `EO-1`
|
|
117
|
+
(2026-08-20).** When an ASN item resolves to a SalesOrderItem whose **only**
|
|
118
|
+
`ItemFulfillmentItem` already carries item-level tracking, `remaining = 0` routes to the reconcile
|
|
119
|
+
path, `itemFulfillmentItemHasTracking()` (added 2026-06-18) skips that IFI, and the handler then
|
|
120
|
+
**fails** instead of completing cleanly — so the caller gets an operation error, not the intended
|
|
121
|
+
no-op, and the ASN is lost. Deterministic: reproduced three times across both the cXML and the JSON
|
|
122
|
+
transports (repro: PO `50305097-1` line 4; Sentry ids `AV-30`, `69-68`). Found on Compass Canada
|
|
123
|
+
but it lives in the **shared Compass parent**, so Office Depot re-shipments on already-tracked
|
|
124
|
+
lines hit it too. This recurs whenever a vendor sends a second package for a line that already has
|
|
125
|
+
tracking. Fix direction: the skip should complete the request successfully (nothing to attach), not
|
|
126
|
+
fall through to the failure path.
|
|
127
|
+
- **The cXML API needs `MATCH_CREATE` overrides on all three ASN tracking bridges.** Compass USA's
|
|
128
|
+
cXML API (`apiId 4`) has `Client_Compass.Apis_RecordFields` rows overriding RecordFields
|
|
129
|
+
**1437/1441/1445** to `MATCH_CREATE`. Only 1437 defaults to `MATCH_CREATE` in Core, so without the
|
|
130
|
+
1441/1445 overrides the nested `trackingNumber` is looked up instead of created and every ship
|
|
131
|
+
notice fails EV-12 — exactly what happened to Compass Canada for 67 days. Do not delete or
|
|
132
|
+
"normalize" these rows. See
|
|
133
|
+
[cXML ShipNotice Gateway](../../../2.0/apps/api2/features/cxml-shipnotice-gateway.md).
|
|
134
|
+
- **`findOrCreateShippingCarrier()` was writing other tenants' carriers into `Client_Compass`
|
|
135
|
+
(fixed 2026-08-20, not yet committed).** The cXML helper authenticated its carrier-create call with
|
|
136
|
+
hardcoded Compass USA credentials, so Compass **Canada** traffic minted carriers here: 15 `PRECI`,
|
|
137
|
+
5 `GANDT` and 3 `XXXX` junk `ShippingCarriers` rows in `Client_Compass` are **not** Compass USA
|
|
138
|
+
data and should be cleaned up.
|
|
114
139
|
- **ASN arriving AFTER the SO is already fulfilled (fixed 2026-06-15):** when an SO is fulfilled
|
|
115
140
|
out-of-band (NetSuite/toga2-supply creates `F`-numbered IFs) **before** its ASN arrives,
|
|
116
141
|
`postPost` previously found zero unfulfilled ASN items and skipped the **entire** propagation
|
|
@@ -221,6 +246,15 @@ Compass Canada (`Model/Compass/Canada/`) is a separate sub-client. The Compass c
|
|
|
221
246
|
|
|
222
247
|
## Change history
|
|
223
248
|
Dated one-liners, newest first.
|
|
249
|
+
- 2026-08-20 — Recorded an **open, deterministic 500 `EO-1`** in `postPost`: an ASN item whose
|
|
250
|
+
SalesOrderItem has one already-tracked `ItemFulfillmentItem` takes the reconcile path, the IFI is
|
|
251
|
+
skipped by `itemFulfillmentItemHasTracking()`, and the handler then fails rather than completing —
|
|
252
|
+
so a vendor's second package on an already-tracked line errors instead of no-op'ing. Found while
|
|
253
|
+
fixing the Compass Canada G&T cXML feed (shared parent, so Office Depot is exposed too). Also
|
|
254
|
+
recorded that Compass USA's cXML API (`apiId 4`) relies on `Apis_RecordFields` `MATCH_CREATE`
|
|
255
|
+
overrides for RecordFields 1437/1441/1445 (Core defaults 1441/1445 to `MATCH`), and that the
|
|
256
|
+
cross-tenant `findOrCreateShippingCarrier()` defect left 23 junk `Client_Compass.ShippingCarriers`
|
|
257
|
+
rows created by Compass Canada traffic. (bala)
|
|
224
258
|
- 2026-08-13 — Diagnosed the recurring MySQL 1213 deadlocks on `POST /v2/advance-shipping-notices`
|
|
225
259
|
(Office Depot cXML feed): 100% were the `_total` UPDATE emitted by the `_Model::save()` parent
|
|
226
260
|
cascade re-saving the PurchaseOrder on each ASN insert, deadlocking under concurrent same-PO ASN
|
package/package.json
CHANGED