toga-ai 1.0.634 → 1.0.635

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.
@@ -5,7 +5,7 @@
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config, worker/crons/infrastructure/execute_dbchanges.php, worker/.ebextensions/030_dbchanges.config |
6
6
  | [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
7
7
  | [Compass Manager Approval Reminder Emails (1.0 worker crons)](features/compass-manager-approval-reminder-emails.md) | Two 1.0 worker crons nag approvers about sales orders still waiting on a decision — one per Compass tenant. | worker/crons/toga2/compass/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/schedules/cron.worker.sync.json, worker1.5/crons/toga2/compass/compass_email_reminders.php, worker1.5/schedules/cron.worker.json |
8
- | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.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/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
8
+ | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compass/send_delivered_email.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/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
9
9
  | [Elite TOGA 2.0 → TOGaDeskSupport Standalone Attachment Sync](features/elite-togadesk-attachment-sync.md) | `sync_togadesk_elite_attachments.php` is a standalone cron (every 5 minutes) that syncs file attachments from TOGA 2.0 into TOGaDeskSupport for Elite. | worker/crons/toga2/elite/sync_togadesk_elite_attachments.php, worker/crons/toga2/elite/test_sync_togadesk_elite_attachments.php |
10
10
  | [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, worker2/Component/Forecast/SaleImport/SaleImport.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/reconcile_drift_2023plus.php, test/@dave/probe_invoice_gap_2026.php, test/@dave/probe_creditmemo_gap_detail.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
11
11
  | [NetSuite Sales Order Sales Rep Sourcing (Staples & ODP EDI orders)](features/netsuite-sales-order-sales-rep-sourcing.md) | How the **sales rep** on a NetSuite Sales Order is determined for the two 1.0 `worker` EDI order-creation integrations (Staples cXML and Compass/ODP EDI). | worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/crons/sync/staples/sync_staples_cxml.php, test/@Mark/NetSuite/TRUE_80451_customer_salesrep_diag.php |
@@ -6,10 +6,11 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-03
9
+ updated: 2026-08-24
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
13
+ - worker/crons/toga2/compass/send_delivered_email.php
13
14
  - worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php
14
15
  - worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php
15
16
  - worker/crons/toga2/compasscanada/send_delivered_email.php
@@ -133,9 +134,26 @@ the tracking number as emailed (it retries next run).
133
134
  - **Pick the template from the language of the person actually being emailed.** The manager-approval
134
135
  reminder cron (`compass_email_reminders.php`) selected the template from the **requester's**
135
136
  language even though the mail goes to the **manager**. Fixed 2026-08-03.
137
+ - **⚠ Conflict with the new 2.0 centralized tracking-status refresh.** The delivered-email cron
138
+ (`worker/crons/toga2/compass/send_delivered_email.php`) polls UPS itself, sends the delivered
139
+ template (uuid `fe961c7c-277b-4a89-be1d-3d318d8c7718`) via api2 `GET /email-templates/sendEmail`,
140
+ and writes `TrackingNumbers.status` with a **direct DB UPDATE** guarded by
141
+ `WHERE status <> 'DELIVERED'`. The new platform-wide
142
+ [tracking-status refresh (worker2)](../../../2.0/apps/worker2/features/tracking-status-refresh.md)
143
+ writes `status` via api2 PUT for **all** clients. If the refresh marks a Compass row `DELIVERED`
144
+ **first**, this cron's guard **skips** it and the delivered email **never sends** — a real conflict
145
+ to resolve before the refresh is enabled for Compass. There is **no `postPut` interceptor** on
146
+ tracking-numbers today; the plan (not built) is to move this email logic into a Compass `postPut`
147
+ interceptor and retire this 1.0 cron.
136
148
 
137
149
  ## Change history
138
150
 
151
+ - 2026-08-24 — Recorded the **conflict with the new 2.0 platform-wide tracking-status refresh**: that
152
+ refresh writes `TrackingNumbers.status` via api2 PUT for all clients, and this cron's delivered
153
+ UPDATE is guarded by `WHERE status <> 'DELIVERED'` — so if the refresh marks a Compass row
154
+ `DELIVERED` first, the delivered email never sends. Noted there is no `postPut` interceptor on
155
+ tracking-numbers and the plan to move this email logic into a Compass `postPut` interceptor and
156
+ retire this cron. Added `send_delivered_email.php` to the file list. (bala)
139
157
  - 2026-08-03 — Compass Canada **item rows are now localized**: titles resolved from the
140
158
  `ItemTranslations` sidecar (two-column select + PHP fallback, never a mixed-collation SQL
141
159
  `COALESCE`) and translated `N/P` / `Qté` labels; language resolution moved onto the shared 1.0
@@ -11,7 +11,7 @@
11
11
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
12
12
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
13
13
  | [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql |
14
- | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
14
+ | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
15
15
  | [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
16
16
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
17
17
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Api/Oneuptime/Oneuptime.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
@@ -6,8 +6,8 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-11
10
- owners: [mhammontree, tcox]
9
+ updated: 2026-08-24
10
+ owners: [mhammontree, tcox, bala]
11
11
  files:
12
12
  - test/@Mark/true-80824-fedex-inflate-test.php
13
13
  - dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql
@@ -18,6 +18,7 @@ files:
18
18
  - _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php
19
19
  - _underscore/Component/Library/Carriers/Ups/Ups.php
20
20
  - _underscore/Component/Library/Carriers/Fedex/Fedex.php
21
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
21
22
  - _underscore/Trait/Netsuite/ItemFulfillment.php
22
23
  - _underscore/Trait/Netsuite/SalesOrder.php
23
24
  - _underscore/Component/Library/NetSuite/NetSuite.php
@@ -275,6 +276,26 @@ including **4 fuzz-discovered `fp:` cases whose only job is to fail if the UTF-8
275
276
  class for **every** integration, but that is the framework's shared HTTP client and needs its own
276
277
  review. **The UPS client has the same latent gap.**
277
278
 
279
+ ## Batch tracking-status polling — `getStatuses()` (implemented 2026-08-24)
280
+
281
+ Alongside the single-shipment `getStatus()`, the carrier libs gained **batch** status lookups to
282
+ support the platform-wide
283
+ [tracking-status refresh](../../worker2/features/tracking-status-refresh.md):
284
+
285
+ - `_Component_Library_Carriers_Fedex::getStatuses(array)` (~**30 numbers/call**) and
286
+ `_Component_Library_Carriers_Usps::getStatuses(array)` (~**10 numbers/call**) reuse the **same
287
+ parsers** as `getStatus()` via extracted private helpers `statusFromFedexTrackResult()` /
288
+ `statusFromUspsDetail()`. Results are keyed by the tracking number the carrier echoes back.
289
+ - **UPS has no batch** — it stays single-call (`getStatus()`); the refresh loops it per number.
290
+
291
+ ### ⚠ UPS `getStatus()` gained `$forceNoLogging` (default `false`) — do not flip the shipment callers
292
+ Bulk tracking polls set **`$forceNoLogging = true`** so UPS request logging is suppressed. With prod
293
+ `[database]` set, an unsuppressed poll logs **every** call → log bloat **and** an
294
+ `"Exactly 1 row expected"` throw. `$forceNoLogging` is also **propagated into `getOAuthToken()`**, so
295
+ an OAuth cache-miss during a no-log poll does not leave the token request logged. **Shipment and
296
+ address-validation callers keep the default (logging ON)** — only the bulk tracking poll passes
297
+ `true`.
298
+
278
299
  ## `FIELD_STORAGE` mechanics (reference)
279
300
 
280
301
  > **⚠ `TrackingNumbers.labelPdfFile` has NO physical column in any `Client_*` schema in any
@@ -423,6 +444,14 @@ still discards the unsaved in-memory value.
423
444
  bill) — expect a hard error rather than a silently-wrong bill in those cases.
424
445
 
425
446
  ## Change history
447
+ - 2026-08-24 — Added **batch tracking-status polling** to the carrier libs for the platform-wide
448
+ [tracking-status refresh](../../worker2/features/tracking-status-refresh.md):
449
+ `Fedex::getStatuses()` (~30/call) and `Usps::getStatuses()` (~10/call), reusing the single-shipment
450
+ parsers via extracted `statusFromFedexTrackResult()` / `statusFromUspsDetail()` helpers (UPS stays
451
+ single-call, no batch). Gave UPS `getStatus()` a `$forceNoLogging` param (default false, also
452
+ propagated into `getOAuthToken()`) so bulk polls suppress request logging — an unsuppressed poll on
453
+ prod `[database]` bloats logs and throws `"Exactly 1 row expected"`; shipment + address-validation
454
+ callers keep logging on. (bala)
426
455
  - 2026-08-11 — Flagged that `TrackingNumbers.labelPdfFile` has **no physical column in any
427
456
  `Client_*` schema in any environment** (only the Core `RecordFields` metadata was ever inserted),
428
457
  so `GET /tracking-numbers` 500s once a query returns rows — storage fields hydrate lazily per row,
@@ -44,6 +44,7 @@
44
44
  | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | > **DB-DRIVEN AI-MODEL ROUTING (2026-07-29).** Which knowledge base a transcript is cleaned > into is now decided by the **meeting organizer's "home" AI model** | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql, dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql, dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.sql, dbchanges2/Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a, dbchanges2/Team/2026-07-10a, dbchanges2/Team/2026-07-28a - TranscriptProcessingRetryAttempts.sql, dbchanges2/Team/2026-07-28b - TranscriptPromptTemplateConverseModel.sql, dbchanges2/Core/2026-07-28a - TeamsTranscriptRetryCron.sql |
45
45
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php, _underscore/Model/Team/Sprint.php, dbchanges2/Core/CronJobs (SprintLockScheduled seed) |
46
46
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
47
+ | [Centralized Tracking-Status Refresh (worker2 Sync cron, all clients)](features/tracking-status-refresh.md) | A single platform-wide cron keeps `TrackingNumbers.status` current for **every provisioned client** until each shipment reaches a terminal state, by polling Fed | worker2/Worker/Sync/TrackingNumbers.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql, dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql, dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql, dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql |
47
48
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php, worker2/Controller/Index.php |
48
49
  | [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
49
50
  | [PHP Runtime Upgrade on Elastic Beanstalk (worker2 8.3 → 8.5 + PhpSpreadsheet 1.x → 3.x)](workflows/php-runtime-upgrade-dependency-audit.md) | The procedure used to move worker2 from **PHP 8.3 to PHP 8.5** on Elastic Beanstalk, and the dependency work that had to land first. | worker2/composer.json, worker2/composer.lock, worker2/Worker/Team/Sprint.php, worker2/Worker/Client/TowFoundation/ProcessReceipts.php, worker2/Worker/Forecast/Import.php |
@@ -0,0 +1,208 @@
1
+ ---
2
+ title: Centralized Tracking-Status Refresh (worker2 Sync cron, all clients)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-24
10
+ owners: ["bala"]
11
+ files:
12
+ - worker2/Worker/Sync/TrackingNumbers.php
13
+ - _underscore/Component/Library/Carriers/Fedex/Fedex.php
14
+ - _underscore/Component/Library/Carriers/Usps/Usps.php
15
+ - _underscore/Component/Library/Carriers/Ups/Ups.php
16
+ - _underscore/Model/Client/TrackingNumber.php
17
+ - dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql
18
+ - dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql
19
+ - dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql
20
+ - dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql
21
+ related:
22
+ - ../architecture.md
23
+ - ../workflows/php-runtime-upgrade-dependency-audit.md
24
+ - ../workflows/running-worker2-locally.md
25
+ - ../../_underscore/features/carrier-shipping-labels.md
26
+ - ../../_underscore/features/tracking-number-bridges.md
27
+ - ../../api2/features/api-payload-interceptors.md
28
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
29
+ ---
30
+
31
+ ## Summary
32
+
33
+ A single platform-wide cron keeps `TrackingNumbers.status` current for **every provisioned
34
+ client** until each shipment reaches a terminal state, by polling FedEx/USPS/UPS and writing the
35
+ mapped status back through **api2 PUT**. There is **no hardcoded client list** and **no new table**:
36
+ the working set is bounded, carrier load is decoupled from cron frequency by a per-row re-check
37
+ cursor, and concurrency/observability ride the existing per-client `Parameters` key/value table.
38
+
39
+ The abstract worker class is `_Worker_Sync_TrackingNumbers` in
40
+ `worker2/Worker/Sync/TrackingNumbers.php`. The Core cron
41
+ (`Sync/TrackingNumbers/Refresh`, `*/10 * * * *`, `maxExecutionTime 120`) fires a **dispatcher**;
42
+ the dispatcher fans out one child task per client.
43
+
44
+ > **⚠ NOT LIVE — go-live is gated on a platform bug.** The write path is api2 PUT, and
45
+ > **worker2 → api2 calls hard-crash on PHP 8.5** (see the go-live gate below). Until that is
46
+ > resolved on the prod worker box, this feature must not run for real (writes will crash).
47
+
48
+ ## How it works
49
+
50
+ ### Dispatcher → per-client child (no hardcoded client list)
51
+ - `Refresh(?clientIdentifier = null, isDryRun = true)` is the dispatcher. With **no**
52
+ `clientIdentifier` it **enumerates provisioned clients** (`Clients` JOIN `Databases` JOIN
53
+ `DatabaseHosts` JOIN `Environments` on slug) and `_Worker::runTask`s one
54
+ `Sync/TrackingNumbers/RefreshClient` child per client. With a `clientIdentifier` it runs that one
55
+ client inline.
56
+ - Each `RefreshClient` child: registers the client DB, claims a per-client run (lease, below),
57
+ resolves a write key (capability-based, below), fetches **one bounded page** of due rows, polls
58
+ carriers, writes changed status via api2 PUT, stamps each row's next-check cursor, and
59
+ **self-continues** to the next page if more remain.
60
+
61
+ ### Re-check cadence is a per-row cursor (carrier load ≠ cron frequency)
62
+ - `TrackingNumbers.c_dtTrackingCheckNext` is the cursor. A row is selected **only when it is NULL
63
+ or `<= NOW()`**.
64
+ - On a good check the cursor is stamped forward: **+1h** when delivery is imminent
65
+ (`OUT_FOR_DELIVERY` / `AWAITING_PICKUP` / `PICKUP`), else **+6h**, each with a few minutes of
66
+ `RAND()` **jitter** to avoid a carrier stampede.
67
+ - **Terminal statuses (`DELIVERED` / `RETURNED_TO_SENDER`) set the cursor NULL** → never
68
+ re-checked.
69
+ - A **45-day `dtCreated` age cutoff** bounds the working set. Steady-state carrier load is therefore
70
+ `working set / cadence`, **independent of how often the cron fires**.
71
+
72
+ ### Batches bounded to finish under the worker request timeout
73
+ - `PAGE_CAP = 50`, `SOFT_TIME_BUDGET_SECONDS = 40`. The budget is checked **between carrier calls
74
+ and between the two batch prefetches**.
75
+ - **⚠ This was a real bug:** an earlier **200s** budget let jobs get **hard-killed at ~75s** (the
76
+ worker's hard request timeout). Keep the soft budget comfortably under ~75s.
77
+
78
+ ### Carrier polling
79
+ - **FedEx** and **USPS** are polled with **one batch call each** (`getStatuses`), keyed by the
80
+ tracking number the carrier echoes back. **UPS is single-call only.** Anything a batch call misses
81
+ falls back to a per-number single lookup. (Carrier-library batch support lives in `_underscore` —
82
+ see [carrier-shipping-labels](../../_underscore/features/carrier-shipping-labels.md).)
83
+ - Carrier is resolved from the stored `ShippingCarrier` first (normalized), else from the
84
+ tracking-number pattern. **Unsupported carrier** (DHL / freight) is **parked**; **undetectable**
85
+ carrier is **deferred 3 days**; **malformed** number is **parked**.
86
+
87
+ ### Status mapping (coarse carrier status → our 12-status vocabulary)
88
+ - The coarse carrier status is refined by the latest scan-event text via a `match(true)` keyword
89
+ scan.
90
+ - **⚠ Two guards that must not be removed:**
91
+ - Scan text can **never** mark `DELIVERED` unless the carrier's coarse status already agrees —
92
+ stops a misleading scan note from producing a false terminal.
93
+ - `RETURNED_TO_SENDER` only comes from the specific phrase **`return to sender`**.
94
+
95
+ ### Concurrency + observability WITHOUT a new table (Parameters convention)
96
+ State lives in each client's existing `Parameters` (key/value) table, mirroring the established
97
+ `NETSUITE_EXECUTION_MODE` / `NETSUITE_LAST_SYNC_DATETIME_*` convention:
98
+ - `TRACKING_REFRESH_EXECUTION_MODE` — a **lease-based claim** holding `RUNNING:<unixStartTime>:<runId>`.
99
+ A claim wins only if idle **or** the lease (`RUN_LEASE_SECONDS = 120`) has expired. **Release is
100
+ owner-scoped** — only the `runId` that owns the lease can free it, so a stale prior run cannot
101
+ clear a newer run's claim. Single row + unique key → deadlock-free.
102
+ - `TRACKING_REFRESH_LAST_SYNC_DATETIME` — the "caught up through" watermark, stamped **only when the
103
+ client's due-set fully drains**.
104
+ - The run body is wrapped in **`try/finally`** so the lease is always released even on a throw.
105
+
106
+ ### Write key resolution is CAPABILITY-BASED, not by name
107
+ `findApiKeyAllowedToWriteStatus()` selects the active `Apis` key **whose role has
108
+ `AclRecordPermissions.allowUpdate` on record 62 (tracking-numbers) AND
109
+ `AclFieldPermissions.isWritable` on field 357 (status)**. Keying off the ACL grant itself means the
110
+ key selection can never drift from the permission, and no per-client key renaming is needed. If **no
111
+ capable key exists**, the client is **skipped** and a **fail-loud alert email** is sent **at most
112
+ once per day** (throttled via a `Parameters` key).
113
+
114
+ ## Data model — the internal `c_` bookkeeping columns
115
+
116
+ `dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql` adds five columns to
117
+ `TrackingNumbers` plus a composite sweep index: `c_dtTrackingCheckNext`, `c_dtTrackingChecked`,
118
+ `c_trackingCheckFailures`, `c_dtTrackingParked`, `c_trackingParkReason`. They are declared as model
119
+ fields on `_Model_Client_TrackingNumber` (`_underscore/Model/Client/TrackingNumber.php`).
120
+
121
+ **The full custom-field convention (learned here).** A first-class `c_` custom field needs **four**
122
+ things, not one: (1) the `ALTER TABLE`, (2) a `CustomRecordFields` registration on the record
123
+ (here record 62), (3) an `AclCustomFieldPermissions` grant to the roles that use it (Base + API
124
+ here), and (4) a **model property declaration**. **Internal-only variant used here:** these five are
125
+ registered with **no `Apis_CustomRecordFields` link and no `*CustomRecordFieldSettings`**, so they
126
+ are usable internally but **never surface in the API payload or the UI** — the right pattern for
127
+ private bookkeeping columns.
128
+
129
+ ## Migrations
130
+
131
+ - `dbchanges2/Client/2026-07-30 - TrackingNumbers_RefreshColumns.sql` — the five `c_` columns +
132
+ sweep index + `CustomRecordFields` (record 62) + `AclCustomFieldPermissions` (Base + API roles),
133
+ internal-only.
134
+ - `dbchanges2/Client/2026-07-30 - Acl_TrackingStatus_Grant.sql` — gap-fill: grants the API role write
135
+ on tracking-numbers.status (record 62 / field 357) wherever missing, **plus two UPDATEs that HEAL
136
+ clients whose grant rows exist but with the flags off** (`allowUpdate = 0` / `isWritable = 0`) — the
137
+ `NOT EXISTS` inserts skip those rows, so the heal is required (this was the **GroWrk** case).
138
+ - `dbchanges2/Client_Tdsynnex/2026-07-30 - Apis_TdsynnexKey.sql` — creates the integration key for
139
+ Tdsynnex, the only provisioned client with no key. It lives in the **per-client** folder (not a
140
+ `Client/` fan-out) because a `Client/` file cannot reference a specific tenant DB — the dbchanges2
141
+ cluster-isolation rule.
142
+ - `dbchanges2/Core/2026-07-30 - TrackingRefresh_Cron.sql` — registers the dispatcher cron.
143
+
144
+ ## Decisions
145
+
146
+ - **Write path = api2 PUT, not direct-DB (deliberate).** Direct-DB was **rejected** so that a future
147
+ **`postPut` interceptor** on tracking-numbers can fire the in-transit / delivered emails on a
148
+ status change. There is no such interceptor today (see the interceptor doc). This is a departure
149
+ from other worker2 crons (e.g. NYCHH asset-tag backfill) that write direct-DB to dodge the PHP-8.5
150
+ crash — here the interceptor requirement wins, which is exactly why the crash below is a hard gate.
151
+
152
+ ## Gotchas / known issues
153
+
154
+ ### ⚠ GO-LIVE GATE — worker2 → api2 hard-crashes on PHP 8.5
155
+ Writes go through api2 PUT, and a worker2 → api2 call **hard-crashes the request on PHP 8.5
156
+ (OpenSSL 3.5.5): a process-level 500 with no catchable trace, and the shutdown handler never
157
+ fires.** The prod worker box was bumped to PHP 8.5 on **2026-07-29** and this path was **never
158
+ re-verified there**. **api2 itself is fine** — on local PHP 8.1, auth + `PUT /tracking-numbers`
159
+ succeeded and api2 understands the new `c_` columns (GET + PUT returned `isSuccess` with no
160
+ unknown-column error). So the fix is **platform-level (worker TLS/curl on 8.5)**, not code. Until
161
+ resolved, this feature cannot run for real. See the causal
162
+ [PHP 8.3 → 8.5 runtime-upgrade workflow](../workflows/php-runtime-upgrade-dependency-audit.md) and
163
+ the same symptom in [NYCHH asset-tag backfill](nychh-asset-tag-backfill.md).
164
+
165
+ ### ⚠ Confirm the worker box's `ENVIRONMENT` before trusting enumeration on beta
166
+ `resolveEnvironment()` — `(substr(_Environment::$name, 0, 4) === 'dev-') ? 'dev' : name` — is
167
+ **byte-identical** to api2's `V2.php` / `Index.php`. On beta the worker box's `ENVIRONMENT` resolves
168
+ to slug **`beta`** or **`sandbox-dev`**, **not `dev`**, and those enumerate a **different/empty**
169
+ client set than `dev`. Confirm the box's actual `ENVIRONMENT` value before trusting the dispatcher's
170
+ client enumeration on beta.
171
+
172
+ ### ⚠ Conflict with the 1.0 Compass delivered-email cron
173
+ The in-transit / delivered emails **today** come from a separate **1.0** Compass worker cron
174
+ (`worker/crons/toga2/compass/send_delivered_email.php`) that polls UPS itself, sends via api2
175
+ `GET /email-templates/sendEmail`, and writes `TrackingNumbers.status` via a **direct DB UPDATE**
176
+ guarded by `WHERE status <> 'DELIVERED'`. If this refresh marks a Compass row `DELIVERED` **first**,
177
+ that cron's guard skips it and the delivered email **never sends**. This is a real conflict to
178
+ resolve before enabling the refresh for Compass — see the
179
+ [Compass in-transit/delivered emails doc](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
180
+ Plan (not built): move the email logic into a Compass `postPut` interceptor and retire the 1.0 cron.
181
+
182
+ ## Verification status
183
+
184
+ - **Beta (dev-sandbox):** all **33** provisioned clients resolve a capable write key via the
185
+ capability query (GroWrk healed, Tdsynnex provisioned). Dry-run (`isDryRun = true`) is fully
186
+ read-only (no writes, no api2 PUT, no emails) and is the safe smoke test.
187
+ - **Local:** worker2's worker endpoint is the **ROOT** (`POST http://worker2/`), **not** `/worker`
188
+ — see [running worker2 locally](../workflows/running-worker2-locally.md).
189
+ - **Prod (before go-live):** apply the three client/core migrations, re-run the 33/33 capable-key
190
+ check, and **resolve the PHP-8.5 worker → api2 crash** (the gate).
191
+
192
+ ## Change history
193
+ - 2026-08-24 — Built the centralized tracking-status refresh: dispatcher + per-client child
194
+ (`_Worker_Sync_TrackingNumbers`), per-row `c_dtTrackingCheckNext` re-check cursor (+1h imminent /
195
+ +6h else, jitter, terminal → NULL, 45-day cutoff), `PAGE_CAP=50`/`SOFT_TIME_BUDGET_SECONDS=40`
196
+ bounding (fixing a 200s budget that got jobs hard-killed at ~75s), FedEx/USPS batch + UPS
197
+ single-call polling, coarse→12-status mapping with the DELIVERED-agreement and literal
198
+ `return to sender` guards, and a `Parameters`-table lease (owner-scoped release) + watermark
199
+ reusing the NetSuite convention (no new table). Write key is capability-based
200
+ (`AclRecordPermissions.allowUpdate` rec 62 + `AclFieldPermissions.isWritable` field 357), skip +
201
+ once-daily alert if none. Added the five internal-only `c_` bookkeeping columns (4-part custom-field
202
+ registration, no API/UI surface) and the cron/ACL/Tdsynnex-key migrations. **Decided write path =
203
+ api2 PUT (not direct-DB)** to enable a future `postPut` email interceptor. Recorded the **PHP-8.5
204
+ worker → api2 hard-crash go-live gate**, the beta `ENVIRONMENT`-slug enumeration caveat, and the
205
+ conflict with the 1.0 Compass delivered-email cron's `WHERE status <> 'DELIVERED'` guard.
206
+ Verified 33/33 capable keys on beta; NOT yet live. (bala)
207
+ </content>
208
+ </invoke>
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-29
10
- owners: [jcardinal]
9
+ updated: 2026-08-24
10
+ owners: [jcardinal, bala]
11
11
  files:
12
12
  - worker2/composer.json
13
13
  - worker2/composer.lock
@@ -16,6 +16,8 @@ files:
16
16
  - worker2/Worker/Forecast/Import.php
17
17
  related:
18
18
  - ../architecture.md
19
+ - ../features/tracking-status-refresh.md
20
+ - ../features/nychh-asset-tag-backfill.md
19
21
  - ../features/team-sprint-management.md
20
22
  - ../features/platform-cache-cleanup.md
21
23
  - ../../../1.0/apps/tools/workflows/deploy-to-elastic-beanstalk-al2023.md
@@ -160,6 +162,24 @@ that production will hit next.
160
162
  Post-upgrade verification: `Platform/Cache/Truncate` confirmed working on PHP 8.5 (see
161
163
  [Platform Cache Cleanup](../features/platform-cache-cleanup.md)).
162
164
 
165
+ ## ⚠ PHP 8.5 regression discovered after the bump — worker2 → api2 hard-crash
166
+
167
+ **Not caught during this upgrade; surfaced later (2026-08).** After the prod worker box moved to
168
+ PHP 8.5 (OpenSSL 3.5.5), an outbound **worker2 → api2 HTTPS call hard-crashes the request: a
169
+ process-level 500 with no catchable trace, and the shutdown handler never fires.** It is a
170
+ **platform-level** fault (worker TLS/curl on 8.5), **not** application code — **api2 itself is fine**
171
+ (verified on local PHP 8.1: auth + `PUT /tracking-numbers` succeeded). Because the prod worker was
172
+ bumped **2026-07-29** and worker → api2 was **never re-verified there**, any worker2 feature that
173
+ writes through api2 is currently blocked on this.
174
+
175
+ Practical consequences:
176
+ - Existing worker2 crons dodge it by writing **direct-DB instead of api2** (e.g.
177
+ [NYCHH asset-tag backfill](../features/nychh-asset-tag-backfill.md)).
178
+ - The [tracking-status refresh](../features/tracking-status-refresh.md) **cannot** — it deliberately
179
+ writes via api2 PUT for a future email interceptor, so this crash is its **go-live gate**.
180
+ - **After any worker2 runtime bump, re-verify the worker → api2 round-trip on the actual target box**
181
+ — a green local run on an older PHP proves nothing about the deployed tier.
182
+
163
183
  ## Open risk — pre-existing composer advisories (own ticket)
164
184
 
165
185
  **Unrelated to this work and not introduced by it.** `composer audit` reports **16
@@ -187,6 +207,13 @@ of the 8.5 branch to keep it a single-concern change — **needs its own ticket.
187
207
  - **Code first (dual-runtime), runtime second (isolated).**
188
208
 
189
209
  ## Change history
210
+ - 2026-08-24 — Recorded a **PHP 8.5 regression that surfaced after this bump**: worker2 → api2
211
+ outbound HTTPS **hard-crashes** (process-level 500, no trace, shutdown handler never fires) on
212
+ 8.5 / OpenSSL 3.5.5. Platform-level (worker TLS/curl), **not** code — api2 verified fine on local
213
+ 8.1. Prod worker went 8.5 on 2026-07-29 and this path was never re-verified there, so it gates
214
+ every worker2 feature that writes through api2 (the reason the tracking-status refresh writes via
215
+ api2 PUT is blocked; other crons use direct-DB to avoid it). Added the rule to re-verify the
216
+ worker → api2 round-trip on the target box after any runtime bump. (bala)
190
217
  - 2026-07-29 — Initial capture. worker2 upgraded phpspreadsheet 1.30.2 → 3.10.7 (`^3.10`) and
191
218
  root require `>=8.2` → `>=8.2 <8.6`; 856 `*ByColumnAndRow` call sites migrated to the array
192
219
  coordinate form across `Team/Sprint.php`, `Client/TowFoundation/ProcessReceipts.php`, and
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 61 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 54 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 55 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.634",
3
+ "version": "1.0.635",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",