toga-ai 1.0.608 → 1.0.610

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.
@@ -4,6 +4,7 @@
4
4
  |-----|---------|-------|
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
+ | [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 |
7
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
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 |
9
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 |
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Compass Manager Approval Reminder Emails (1.0 worker crons)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-18
10
+ owners: ["bala"]
11
+ files:
12
+ - worker/crons/toga2/compass/compass_email_reminders.php
13
+ - worker/crons/toga2/compasscanada/compass_email_reminders.php
14
+ - worker/schedules/cron.worker.sync.json
15
+ - worker1.5/crons/toga2/compass/compass_email_reminders.php
16
+ - worker1.5/schedules/cron.worker.json
17
+ related:
18
+ - compass-partial-in-transit-delivered-emails.md
19
+ - ../../../clients/compass-usa/features/approval-decision-flow.md
20
+ - ../../../clients/compass-canada/features/french-order-email-localization.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ Two 1.0 worker crons nag approvers about sales orders still waiting on a decision — one per
26
+ Compass tenant. They build an `orderUrl` (`$managerApprovalUrl`) and post it to api2's
27
+ `/email-templates/sendEmail` scripted route, which substitutes it into the stored
28
+ `EmailTemplates` body.
29
+
30
+ **These are the only two files in the `worker` repo that pass an order link to `sendEmail`,** and
31
+ both currently send a **bare supply homepage URL with no order uuid** — so the manager lands on the
32
+ supply order list and has to hunt for the order manually. Both crons already `SELECT
33
+ SalesOrders.uuid`; the value is simply never used in the link. The 2.0 side of the same feature was
34
+ migrated to a real deep link on 2026-08-18 (see
35
+ [Compass approval-decision flow](../../../clients/compass-usa/features/approval-decision-flow.md));
36
+ **these two were not**, so the two tiers now disagree.
37
+
38
+ ## Key files / entry points
39
+
40
+ - **`worker/crons/toga2/compass/compass_email_reminders.php`** — Compass USA. Selects
41
+ `SalesOrders.uuid` (~L73); sets `$managerApprovalUrl = "https://compass.togasupply.com"` (~L138).
42
+ - **`worker/crons/toga2/compasscanada/compass_email_reminders.php`** — Compass Canada. Selects
43
+ `SalesOrders.uuid` (~L90); sets `$managerApprovalUrl = "https://compasscanada.togasupply.com"`
44
+ (~L154).
45
+ - **`worker/schedules/cron.worker.sync.json`** — both are live: Canada at ~L336, USA at ~L486.
46
+
47
+ ## How it works
48
+
49
+ 1. Query open approvals (branching on `salesOrderStageId`) and collect the requester/manager
50
+ details plus `SalesOrders.uuid` and `number`.
51
+ 2. Build `$managerApprovalUrl` — **today a bare host literal, no uuid**.
52
+ 3. Pick the email template. Note the language rule already recorded on the in-transit doc: the
53
+ template must be chosen from the language of the **manager being emailed**, not the requester's
54
+ (that was a live bug, fixed 2026-08-03).
55
+ 4. `POST /email-templates/sendEmail` with `orderUrl` in the payload.
56
+
57
+ ## Gotchas / known issues
58
+
59
+ - **⚠ The link has no uuid, and the fix is already available.** To bring these in line with 2.0,
60
+ append the supply order route to the host:
61
+ `/sales-orders?sales-orders=<SalesOrders.uuid>`. **Confirm the route actually opens the order
62
+ first** — as of 2026-08-18 that is unverified and there is contrary evidence in `toga2-supply`;
63
+ see the verification note on
64
+ [Compass approval-decision flow](../../../clients/compass-usa/features/approval-decision-flow.md).
65
+ Do not copy the *old* 2.0 query-string format
66
+ (`/?sales-orders_recordsPerPage=10&sales-orders_page=1&Orders=orders-view&uuid=…`) — it was
67
+ retired because it was producing an error page.
68
+ - **⚠ Fixing the code is not enough — the stored template bodies also hardcode the host.** Several
69
+ `EmailTemplates` rows show `compass.togasupply.com` as **visible link text** while the `href` is
70
+ `{orderUrl}`, so the email still displays a production hostname after any code change. Counts and
71
+ row ids are on the approval-decision-flow doc.
72
+ - **⚠ `worker1.5` holds an unscheduled duplicate of the USA cron — decide whether to delete it.**
73
+ `worker1.5/crons/toga2/compass/compass_email_reminders.php` is **dead code**:
74
+ `worker1.5/schedules/cron.worker.json` schedules only 5 crons and this is not one of them. It
75
+ contains (a) the same bare-host URL at ~L63 and (b) an **inline HTML email body** at ~L211 with a
76
+ hardcoded `href='https://compass.togasupply.com'` — so it does not even read the stored template.
77
+ Its commented-out commerce URL also passes `$number` where `uuid=` is expected. If this copy is
78
+ ever revived it will email a broken link, so it should be deleted rather than left to rot.
79
+ - **This file is where the "something went wrong in togasupply" workaround is documented in
80
+ comments.** `worker1.5/.../compass_email_reminders.php` ~L57-63 carries the old deep link
81
+ commented out with the note *"Updating the url to point to generic compass.togasupply.com to
82
+ check if this will be fixing the issue of something went wrong in togasupply"*. That comment is
83
+ the historical evidence that the old query-string deep link was broken and was **downgraded to
84
+ the bare homepage as a workaround**, not that a homepage link was ever the intent.
85
+
86
+ ## Change history
87
+
88
+ - 2026-08-18 — Documented as its own subject while migrating the **2.0** Compass/Quad approval
89
+ emails to the new `/sales-orders?sales-orders=<uuid>` supply route. Recorded that these two
90
+ `worker` crons are the **only** worker files passing an `orderUrl` to `sendEmail`, that both
91
+ still send a bare supply host with **no uuid** even though both already select
92
+ `SalesOrders.uuid`, that the 1.0 and 2.0 tiers therefore now disagree, and that the
93
+ `worker1.5` copy is unscheduled dead code carrying both a bare-host URL and an inline template
94
+ with a hardcoded prod host. No code changed in `worker` or `worker1.5` this session. (bala)
@@ -16,6 +16,7 @@
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 |
18
18
  | [_Config group access — the two-argument "optional" form does NOT rescue a missing GROUP](features/config-group-access.md) | `_Config::<group>('<property>')` reads a value out of the active `Config/<ENVIRONMENT>.ini`, and **it throws when the requested group is absent from the ini.** | _underscore/Config.php, _underscore/Component/Api/Paypal/Paypal.php, _underscore/Worker.php, api2/Config/beta.ini, api2/Config/sandbox-dev.ini |
19
+ | [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the **authoritative registry of which host serves which client's app in which environment**. | _underscore/Model/Core/Domain.php, api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, worker/config.beta.ini |
19
20
  | [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
20
21
  | [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
21
22
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
@@ -39,6 +40,7 @@
39
40
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
40
41
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
41
42
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
43
+ | [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
42
44
  | [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
43
45
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql |
44
46
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | _underscore/Model/Client/ServiceRequest.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql |
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: Core.Domains — the app host registry (and why you cannot derive a host)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-18
10
+ owners: ["bala"]
11
+ files:
12
+ - _underscore/Model/Core/Domain.php
13
+ - api2/Component/Api/CrossClient/CrossClient.php
14
+ - api2/Component/Api/V2/V2.php
15
+ - worker/config.beta.ini
16
+ related:
17
+ - ../../api2/workflows/environment-configuration-and-provisioning.md
18
+ - ../../toga2-supply/workflows/client-host-scoping.md
19
+ - ../../api2/features/encrypted-user-uuid-auth-handoff.md
20
+ - ../../../../clients/compass-usa/features/approval-decision-flow.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `Core.Domains` is the **authoritative registry of which host serves which client's app in which
26
+ environment**. Anything that needs to build a link into a front-end app (supply, commerce, hub,
27
+ view) for a given client and environment must **read this table** — the host is *not* derivable
28
+ from the environment slug, and it is *not* derivable from the api host serving the request. Both
29
+ of those shortcuts look correct and are wrong for at least one real registered host (see
30
+ Gotchas).
31
+
32
+ The table is also what api2 gates `Origin` against for the auth handoff, and what the supply
33
+ front-end's `/domains` lookup resolves for SSO-vs-password login. So it is read on hot paths by
34
+ several apps; treat its data as production-critical.
35
+
36
+ ## Key files / entry points
37
+
38
+ - **`_underscore/Model/Core/Domain.php`** — `_Model_Core_Domain` (`DATABASE = DB_CORE`,
39
+ `TABLE = 'Domains'`). It already exists; use it rather than hand-writing a query where a model
40
+ load will do. It also exposes name constants `APP_SUPPLY|COMMERCE|HUB|RETAIL|VIEW` and
41
+ `ENVIRONMENT_DEV|ALPHA|BETA|TEST|STAGE|HOTFIX|PRODUCTION|DEMO`.
42
+ - **`api2/Component/Api/CrossClient/CrossClient.php`** — `resolveClientOriginDomain(int $clientId)`
43
+ (~L372-387) is the existing lookup precedent. Read it before writing a new one, and read the
44
+ caveat on it below.
45
+ - **`api2/Component/Api/V2/V2.php`** — `$api->response->authority` is set in `execute()` (~L167)
46
+ from `$_SERVER['SERVER_NAME']`.
47
+
48
+ ## How it works
49
+
50
+ ### Schema, and the one thing everybody gets wrong
51
+
52
+ Columns: `id`, `uuid`, `clientId`, `appId`, `environmentId`, `domain`.
53
+
54
+ **`domain` already includes the scheme.** Registered values look like
55
+ `https://compass.togasupply.com` and, on dev, `http://compass.togasupply`. Do **not** prepend
56
+ `https://` to a value read from this table — that is the first bug anyone writes against it.
57
+
58
+ Resolve the other three columns by join, not by hardcoded id:
59
+
60
+ - `Core.Apps.slug` — `supply` (appId `1`) and `commerce` (appId `5`) are the two that matter for
61
+ order links.
62
+ - `Core.Environments.slug` — e.g. `production`, `beta`, `sandbox-dev`, `sandbox-client`,
63
+ `client-beta`, `qa-*`, `qc-*`.
64
+ - `Core.Clients` for `clientId`.
65
+
66
+ ### Caveat on the existing precedent lookup
67
+
68
+ `CrossClient::resolveClientOriginDomain()` collapses any `dev-*` environment name to `dev` and
69
+ returns `null` when nothing is registered — both good behaviours to copy. But it filters on
70
+ `clientId` + environment **only, never on `appId`**, and takes `ORDER BY Domains.id ASC LIMIT 1`.
71
+ For its own purpose (any origin belonging to this client) that is fine. **Copied as-is into an
72
+ app-specific lookup it will happily return a commerce domain for a supply link.** Always add the
73
+ `appId` join.
74
+
75
+ ## Gotchas / known issues
76
+
77
+ - **⚠ You cannot derive a site host from the environment slug.** The general rule for site hosts
78
+ is: split the `Core.Environments` slug on `-`, **reverse** the parts, join with dots
79
+ (`sandbox-client` → `client.sandbox`, `qa-beta` → `beta.qa`, `qc-security` → `security.qc`);
80
+ `production` contributes no segment. **`client-beta` breaks the rule**: the registered supply
81
+ site is `https://compass.beta.client.togasupply.com` while the api host for the same
82
+ environment is `https://api.client.beta.togahub.com/v2` — the two labels are in **opposite
83
+ order**. A rule tuned to one is wrong for the other, so a "clever" transform is guaranteed to be
84
+ wrong somewhere. Read the table.
85
+ - **⚠ You cannot derive a site host from the api host either.** Beyond the `client-beta` label
86
+ reversal, production runs **per-client api hosts** (`compass.api.togahub.com`,
87
+ `nychh.api.togahub.com`, `spglobal.api.togahub.com`) *alongside* `api.client.togahub.com`, so
88
+ the label layout differs per client. And `$api->response->authority` is **absent for CLI/cron
89
+ runs** — it is set from `$_SERVER['SERVER_NAME']`, so any code depending on it silently gets
90
+ nothing in a worker/cron context. (It *is* set before routing, so it is available to every model
91
+ interceptor on a real request.)
92
+ - **⚠ `(clientId, appId, environmentId)` is NOT unique and there is no unique index.** Duplicate
93
+ rows exist, so any lookup must decide deterministically which row wins. As of **2026-08-18**, 22
94
+ `*.dev.sandbox.*` rows are mis-filed under the `beta` and `dev` environments instead of
95
+ `sandbox-dev`.
96
+ - **Agreed remediation (approved 2026-08-18, NOT executed — needs a DBA and a prod write):**
97
+ move the 12 `https` rows currently under `beta` to `environmentId 23` (`sandbox-dev`) — ids
98
+ `625, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 728`.
99
+ - The 10 local-TLD rows under `dev` (e.g. id `639` = `http://compass.dev.sandbox.togasupply`)
100
+ are **deliberately left alone** — moving them would recreate the duplicate.
101
+ - The correct convention is proven by existing rows: id `725`
102
+ `elite.dev.sandbox.togasupply.com` under `sandbox-dev`, id `709`
103
+ `compass.client.sandbox.togasupply.com` under `sandbox-client`.
104
+ - **Coverage is uneven per client — assume a row is missing until you have looked.** As of
105
+ 2026-08-18: `Compass_Usa` has the fullest set (production, beta, alpha, gamma, demo, stage, dev,
106
+ sandbox-client, client-alpha/beta/gamma, all `qa-*` and `qc-*`). `Compass_Canada` has only
107
+ production, beta, gamma, dev — **no `sandbox-client` row**. `Quad` has only production, beta,
108
+ dev. This is exactly why an environment-aware link builder is riskier than it sounds: for most
109
+ clients, most environments simply are not registered.
110
+ - **The worker tier's "beta" is not the beta api.** `worker/config.beta.ini` sets
111
+ `[api] _ = "https://api.client.sandbox.togahub.com/v2"`, so the environment *named* beta calls
112
+ the **client.sandbox** api. Do not infer the environment from the config file's name. Three dev
113
+ laptop configs have **no `[api] _` key at all** (`config.dev-chadwimberly-mactop.ini`,
114
+ `config.dev-markhammontree-laptop.ini`, `config.dev-nkeshavamurthy-laptop.ini`).
115
+ - **Rows can pre-exist without a `dbchanges2` migration.** Elite's rows existed for every
116
+ environment with no migration that created them — check the table before writing one (see
117
+ [supply client host-scoping](../../toga2-supply/workflows/client-host-scoping.md)).
118
+
119
+ ## Change history
120
+
121
+ - 2026-08-18 — Documented `Core.Domains` as the authoritative app-host registry after an
122
+ environment-aware email-link builder was designed against it and then **deliberately dropped**
123
+ (see [Compass approval-decision flow](../../../../clients/compass-usa/features/approval-decision-flow.md)).
124
+ Recorded that `domain` already carries the scheme; that `(clientId, appId, environmentId)` is not
125
+ unique and 22 `*.dev.sandbox.*` rows are mis-filed under `beta`/`dev` (with the approved,
126
+ unexecuted id list to move to `sandbox-dev`); that the env-slug → subdomain reverse-and-join rule
127
+ **breaks on `client-beta`**, where the site and api hosts order their labels oppositely; that
128
+ `CrossClient::resolveClientOriginDomain()` does not filter on `appId` and so can return a
129
+ commerce domain for a supply lookup; that `$api->response->authority` is unset for CLI/cron; and
130
+ the per-client environment coverage gaps (Compass Canada and Quad have only 3-4 environments
131
+ registered). (bala)
@@ -0,0 +1,171 @@
1
+ ---
2
+ title: TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-18
10
+ owners: [bala]
11
+ files:
12
+ - _underscore/Model/Client/TableView.php
13
+ - dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql
14
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
15
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
16
+ related:
17
+ - ../../api2/features/tableview-field-metadata.md
18
+ - ./page-meta-context-field-settings.md
19
+ - ./tableview-hyperlink-columns.md
20
+ - ../../api2/features/tableview-apiwhereclause-row-filtering.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its
26
+ base record. `_Model_Client_TableView::meta()` (`_underscore/Model/Client/TableView.php`) turns each
27
+ row into one entry of `table.joins` (`table`, `alias`, `type`, `onA`, `onB`). Two things about that
28
+ translation are load-bearing and non-obvious:
29
+
30
+ - **`onB` is emitted with the RAW TABLE NAME of the parent field's record, never with an alias.**
31
+ - **`joinOnTableViewJoinId` does not affect the emitted ON clause at all.**
32
+
33
+ Together those mean a **two-hop (chained) join works only because the first join of a table is
34
+ aliased to the plain table name**. Get the order or the duplication wrong and the view emits SQL
35
+ referencing a table that was never joined.
36
+
37
+ ## How it works
38
+
39
+ The joins for a view are read with (paraphrased, ~line 55-73):
40
+
41
+ ```sql
42
+ SELECT TableViewJoins.id, joinRecordId, joinOnRecordFieldId, joinOnTableViewJoinId,
43
+ parentRecordFieldId, parentCustomRecordFieldId, `type`
44
+ FROM TableViewJoins
45
+ INNER JOIN AclRecordPermissions ON AclRecordPermissions.recordId = TableViewJoins.joinRecordId
46
+ WHERE TableViewJoins.tableViewId = <id> AND AclRecordPermissions.roleId IN (<user roles>)
47
+ ORDER BY TableViewJoins.sortOrder ASC, TableViewJoins.id ASC
48
+ ```
49
+
50
+ Then, per join:
51
+
52
+ - **Alias.** A counter (`$tableViewAliasOrds`) starts at `65` (`A`) and is seeded with the view's
53
+ **base** table. The first join of a given table gets `ord == 65`, which means its alias is the
54
+ **plain table name**. Any later join of that same table becomes `<Table>_<letter>`.
55
+ - **ON clause.**
56
+ - `onA` = `` `<this join's alias>`.`<joinOnRecordField.field>` ``
57
+ - `onB` = `` `<($parentRecord->model)::TABLE>`.`<parentRecordField.field>` `` — the **raw table
58
+ name** of the record that owns `parentRecordFieldId`.
59
+ - `joinOnTableViewJoinId` is used for exactly one thing: setting `joinOnTableAlias` on the join
60
+ object. The current code **never emits `joinOnTableAlias` into `onA`/`onB`**. Treat it as
61
+ documentation of the chain, not the mechanism.
62
+
63
+ ## Chained (multi-hop) joins
64
+
65
+ Example shipped 2026-08-18 on the `inventory_units` view (base record **Units**, 31): to show
66
+ `Manufacturers.name` you need **Units → Items → Manufacturers**.
67
+
68
+ - Hop 1: join **Items** (record 21) `ON Items.id (105) = Units.itemId (238)`
69
+ - Hop 2: join **Manufacturers** (record 20) `ON Manufacturers.id (104) = Items.manufacturerId (107)`
70
+
71
+ Hop 2's `onB` renders as `` `Items`.`manufacturerId` ``. That resolves **only** because hop 1 is the
72
+ first Items join on the view and therefore aliased `Items`. So:
73
+
74
+ - **The parent hop must sort first.** Joins are ordered `sortOrder ASC, id ASC`, so give the parent
75
+ hop the lower `sortOrder` or the SQL references a table that has not been joined yet.
76
+ - **A chain breaks if its parent table is joined more than once on the same view.** The extra join
77
+ gets a suffixed alias while `onB` still says the bare table name, so the chain silently binds to
78
+ the first join — or fails outright if the bare-name join is not the one you meant.
79
+ - Before this change there were **zero** rows with a non-null `joinOnTableViewJoinId` anywhere in
80
+ `Client_Elite`, so chained joins were previously unused in these schemas.
81
+
82
+ ## Look for an existing unused join before adding one
83
+
84
+ The `inventory_units` view already carried a Units → Items join
85
+ (`TableViewJoins.uuid = '7894941a-d00b-49db-af5f-63835c29b1a4'`, joinRecordId 21, joinOnRecordFieldId
86
+ 105 `Items.id`, parentRecordFieldId 238 `Units.itemId`, `OUTER`, `sortOrder 0`) in **both** clients
87
+ and in **both** prod and client-sandbox — referenced by **zero** `TableViewFields`. It came in with
88
+ the all-clients template `dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql`.
89
+ Only the second hop had to be created. Always check for a usable join first; a duplicate Items join
90
+ would also have broken the chain above.
91
+
92
+ ## The joined record needs an AclRecordPermissions row
93
+
94
+ The joins query **INNER JOINs `AclRecordPermissions` on `joinRecordId`**. With no row for the
95
+ caller's role, the join is silently dropped from the result set, and any `TableViewFields` row
96
+ pointing at it then throws:
97
+
98
+ ```
99
+ There is no AclRecordPermissions record permitting access to TableViewJoins.id = N for this user
100
+ ```
101
+
102
+ So a new joined table (here `Manufacturers`, record 20) needs its `AclRecordPermissions` grant in the
103
+ same migration.
104
+
105
+ ## Context RecordFieldSettings are OPTIONAL for a joined column to render
106
+
107
+ `meta()` looks up `contextRecordFieldId`-scoped settings (`DefaultRecordFieldSettings` plus the
108
+ Client / Persona / User / Language `RecordFieldSettings` tables) only to decide **which settings row
109
+ supplies the column label**. When no context row exists for `(recordFieldId, contextRecordFieldId)`,
110
+ it nulls `contextTable` / `contextField` and falls back to the field's own settings. A new joined
111
+ column therefore does **not** require adding `RecordFieldSettings` rows just to appear — those rows
112
+ are only needed to control the **header text** (see
113
+ [page-meta-context-field-settings](./page-meta-context-field-settings.md), which is also where the
114
+ "header renders as a camelCase slug" fallback is explained).
115
+
116
+ ## Authoring the migration (house style used here)
117
+
118
+ The 2026-08-18 change added Part Number (`Items.partNumber` 108), Manufacturer
119
+ (`Manufacturers.name` 103) and Item Description (`Items.description` 109) to `inventory_units`
120
+ (`TableViews.uuid 86b3f376-2a15-4a46-ac0a-9b25146e40ac`), positioned directly after Asset Tag,
121
+ mirroring how `inventory_items` surfaces the same three fields. Applied to **prod and
122
+ client-sandbox for both Elite and NYCHH** as one file per client with identical content.
123
+
124
+ - **Key everything off `uuid`, never off `id`.** `TableViews.id` happened to be 21 in both clients,
125
+ but the pre-existing `TableViewJoins.id` differed (Elite 142, NYCHH 165). Resolve ids with
126
+ `(SELECT id FROM TableViewJoins WHERE uuid = ...)`.
127
+ - **One idempotent file beats an insert file plus a follow-up reorder file.** The insert had already
128
+ run in all four targets when the reposition was requested; rather than ship a second migration, the
129
+ same file was rewritten as `INSERT ... ON DUPLICATE KEY UPDATE` keyed on `uuid` for the join and the
130
+ three fields, followed by an unconditional renumber. On an already-applied schema the net effect is
131
+ just the reorder — no duplicate rows, no row-id churn.
132
+ - **`@` session variables mean the file must run in ONE connection.** Splitting the statements across
133
+ connections (or across two client-tool tabs) silently loses the variables and the renumber writes
134
+ garbage.
135
+ - Reordering columns has its own hazard — `TableViewFields` has `UNIQUE (tableViewId, index)`, so a
136
+ naive `index = index + N` can collide mid-statement. The safe park-shift-pull pattern is documented
137
+ in [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
138
+
139
+ Core-level reference ids used (identical across all clients and environments): **Records** — Units
140
+ 31, Items 21, Manufacturers 20. **RecordFields** — `Units.itemId` 238, `Items.id` 105,
141
+ `Items.partNumber` 108, `Items.description` 109, `Items.manufacturerId` 107, `Manufacturers.id` 104,
142
+ `Manufacturers.name` 103.
143
+
144
+ ## Gotchas
145
+
146
+ - **`onB` never uses an alias.** Any chain depends on the parent hop owning the bare table name.
147
+ - **`joinOnTableViewJoinId` is inert in the emitted SQL** — setting it does not make a chain work,
148
+ and omitting it does not break one. It is metadata for humans until the ON-clause builder starts
149
+ using `joinOnTableAlias`.
150
+ - **Alias suffixes are not `_B` for the first duplicate.** The counter is incremented both inside the
151
+ `isset` branch and again at the end of the loop, so the second join of a non-base table lands on
152
+ `chr(67)` → `<Table>_C`; `<Table>_B` only appears when the view joins its own **base** table. Do
153
+ not hand-write an alias from memory — check the counter logic before relying on one.
154
+ - **A missing `AclRecordPermissions` row for the joined record drops the join, not just the column** —
155
+ and the resulting error names a `TableViewJoins.id`, which is why it reads like config corruption
156
+ rather than an ACL gap.
157
+ - **An unused join can already exist.** Confirm before inserting; a duplicate join of the same table
158
+ is what breaks chained joins.
159
+
160
+ ## Change history
161
+
162
+ - 2026-08-18 — Documented how `TableViewJoins` compile into SQL after adding a chained
163
+ **Units → Items → Manufacturers** join for the Part Number / Manufacturer / Item Description columns
164
+ on `inventory_units` (Elite + NYCHH): `onB` is emitted with the **raw table name** (so a chain only
165
+ works off the first, unaliased join of a table), `joinOnTableViewJoinId` feeds only
166
+ `joinOnTableAlias` and is **not** in the ON clause, joins are ordered `sortOrder ASC, id ASC`, the
167
+ joins query INNER JOINs `AclRecordPermissions` on `joinRecordId` (missing grant = dropped join plus
168
+ the "no AclRecordPermissions record permitting access to TableViewJoins.id" error), and context
169
+ `RecordFieldSettings` are optional for rendering (label-only). Also recorded the pre-existing unused
170
+ Units→Items join `7894941a-...` present in both clients, and the uuid-keyed idempotent single-file
171
+ migration style. (bala)
@@ -18,7 +18,7 @@
18
18
  | [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
19
19
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
20
20
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
21
- | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql |
21
+ | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
22
22
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
23
23
  | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
24
24
  | [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
10
- owners: [apeterson]
9
+ updated: 2026-08-18
10
+ owners: [apeterson, bala]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
13
13
  - api2/Component/Api/V2/V2.php
@@ -15,9 +15,12 @@ files:
15
15
  - dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql
16
16
  - dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql
17
17
  - dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql
18
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
19
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
18
20
  related:
19
21
  - tableview-apiwhereclause-row-filtering.md
20
22
  - ../../toga25-supply/features/meta-driven-table-data.md
23
+ - ../../_underscore/features/tableview-joins.md
21
24
  ---
22
25
 
23
26
  ## Summary
@@ -52,6 +55,8 @@ it. (Reference: `Core.RecordFields` id 369 = `ItemFulfillmentItems.quantity` set
52
55
  `type=NUMBER`, `precision=0`, which normalizes that column across all fulfillment views/clients.)
53
56
  - **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
54
57
  request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
58
+ How a join row becomes SQL — aliasing, chained multi-hop joins, and the ACL row the joined record
59
+ needs — is in [tableview-joins](../../_underscore/features/tableview-joins.md).
55
60
 
56
61
  ### `recordFieldId` is a cross-DB reference into shared `Core.RecordFields`
57
62
 
@@ -65,6 +70,32 @@ Reference ids observed for the `Items` record: **`Core.Records` id 21 = Items**,
65
70
  **`Core.RecordFields` id 106 = `Items.uuid`**. (Look these up per field before writing a migration —
66
71
  do not assume ids across environments.)
67
72
 
73
+ ## Column order (`index`) — the UNIQUE keys make reordering non-trivial
74
+
75
+ `TableViewFields` carries **`UNIQUE (tableViewId, index)`** and **`UNIQUE (tableViewId, slug)`**.
76
+ Columns are emitted `ORDER BY TableViewFields.index ASC`, so repositioning a column means rewriting
77
+ other rows' `index` — and a plain `UPDATE ... SET index = index + N` can hit the unique key
78
+ **mid-statement**, because MySQL checks per row, not at statement end.
79
+
80
+ The safe pattern (used to slot Part Number / Manufacturer / Item Description in right after Asset
81
+ Tag on `inventory_units`, 2026-08-18) is **park → shift → pull → drop**, so that every target slot is
82
+ empty at the moment it is written:
83
+
84
+ 1. Park the moving rows in a high band (`200+`).
85
+ 2. Shift the rows that must move down into a second band (`100+`).
86
+ 3. Pull those back down into the freed low slots.
87
+ 4. Drop the parked rows into their final positions.
88
+
89
+ Notes that matter when writing it:
90
+
91
+ - **`index` is `TINYINT UNSIGNED`** — the parking band must stay under 256.
92
+ - **Gaps in `index` are harmless** (it is display order only), but finishing with a contiguous
93
+ renumber makes the script **re-runnable to the same final state**.
94
+ - Do the whole thing **uuid-keyed** and unconditional at the end, so an already-applied migration
95
+ can simply be re-run instead of shipping a separate "reorder" file — see the authoring notes in
96
+ [tableview-joins](../../_underscore/features/tableview-joins.md). If the script uses `@` session
97
+ variables, all statements must run in **one connection**.
98
+
68
99
  ## Hidden (projected-but-not-rendered) columns
69
100
 
70
101
  A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
@@ -148,6 +179,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
148
179
  `meta.table.sort=[{slug,dir}]`; and the JOIN-`TableViewFields`-on-slug UPDATE recipe that
149
180
  avoids MySQL error 1093 (a self-subquery on `TableViews` is rejected). Applied to Quad's
150
181
  sales-orders listing (default `dateOrder DESC`). (apeterson)
182
+ - 2026-08-18 — Recorded that `TableViewFields` has **UNIQUE (tableViewId, `index`)** and
183
+ **UNIQUE (tableViewId, slug)**, so a column reorder cannot be a plain `index = index + N` (the
184
+ unique key is checked per row, mid-statement); documented the park → shift → pull → drop pattern,
185
+ the `TINYINT UNSIGNED` ceiling on the parking band, and that a closing contiguous renumber is what
186
+ makes such a migration re-runnable. Found while inserting Part Number / Manufacturer / Item
187
+ Description after Asset Tag on the `inventory_units` view for Elite + NYCHH. Cross-linked the new
188
+ [tableview-joins](../../_underscore/features/tableview-joins.md) doc for how a join row becomes
189
+ SQL. (bala)
151
190
  - 2026-08-17 — Documented that a column's `type`/`precision` are canonical from `Core.RecordFields`
152
191
  (no per-view override — a change hits every view/client) while `isCopyable`/`isSortable`/
153
192
  `isFilterable`/`isVisible` are per-view `TableViewFields` flags. Confirmed setting
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 18 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 24 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
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)
11
11
  - **togadesk** (TOGa Desk) — 12 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
18
18
 
19
19
  ## 2.0 framework
20
20
 
21
- - **_underscore** (_Underscore) _(framework core)_ — 58 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
+ - **_underscore** (_Underscore) _(framework core)_ — 60 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 51 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 24 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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php, _underscore/Model/Client/ApprovalTemplateStage.php |
5
+ | [Compass Approval-Decision Flow (Notifications & Manager Reassignment)](features/approval-decision-flow.md) | 2.0 | Compass's sales-order approval flow — approval/notification email lists, **manager reassignment**, VIP auto-approve, and EN/FR localization — lives **entirely i | _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Usa/ApprovalDecision.php, _underscore/Model/Compass/Canada/ApprovalDecision.php, _underscore/Model/Client/ApprovalTemplateStage.php, _underscore/Model/Quad/SalesOrder.php |
6
6
  | [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Model/Compass/PurchaseOrder.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql, dbchanges2/Client_Compass/2026-06-15b - BackfillSA132781ItemFulfillmentTracking.sql, dbchanges2/Client_Compass/2026-06-16 - CleanupSA132763CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16b - CleanupSA132743CrossLineTracking.sql, dbchanges2/Client_Compass/2026-06-16c - BackfillSA132763C40QYUCTracking.sql, dbchanges2/Client_Compass/2026-06-18a - CleanupSA132898DuplicateTracking.sql, dbchanges2/Client_Compass/2026-06-18b - CleanupSA132881DuplicateTracking.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
7
7
  | [Cost Centers — Unit Locations, numeric-only policy](features/cost-centers.md) | 2.0 | A Compass "cost center" — the value a user picks in commerce and that lands on an order — is **not** a `CostCenters` row. | toga2-commerce/src/pages/Cart/api/CartApi.ts, worker1.5/crons/toga2/compass/import_locations.php, _underscore/Model/Compass/SalesOrder.php, api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-06 - RemoveNonNumericCostCenters.sql |
8
8
  | [Compass isFulfillable — Data Quality & the Type-Derived Rule](features/isfulfillable-data-quality-and-type-rule.md) | 2.0 | A **prod-data investigation (2026-08-12, read-only)** into why so many Compass storefront lines still have a dead **Qty Fulfilled** cell. | _underscore/Model/Client/Item.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, toga2-supply/src/pages/Orders/view/OrderView/components/sections/OrderItemsTableSection.tsx |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-13
9
+ updated: 2026-08-18
10
10
  owners: ["apeterson", "dfranks", "bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/ApprovalDecision.php
@@ -14,6 +14,7 @@ files:
14
14
  - _underscore/Model/Compass/Usa/ApprovalDecision.php
15
15
  - _underscore/Model/Compass/Canada/ApprovalDecision.php
16
16
  - _underscore/Model/Client/ApprovalTemplateStage.php
17
+ - _underscore/Model/Quad/SalesOrder.php
17
18
  related:
18
19
  - mr-ma-order-approval-and-status.md
19
20
  - people-file-user-lifecycle.md
@@ -21,6 +22,8 @@ related:
21
22
  - ../../compass-canada/features/french-order-email-localization.md
22
23
  - ../../../2.0/apps/_underscore/features/email-template-sending.md
23
24
  - ../../../2.0/apps/_underscore/features/record-change-audit-log.md
25
+ - ../../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
26
+ - ../../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md
24
27
  ---
25
28
 
26
29
  ## Summary
@@ -85,14 +88,75 @@ holds the order:
85
88
  private static `isCompassCanadaClient(&$api)` returns
86
89
  `($api->client->clientIdentifier ?? '') === 'Compass_Canada'`. Anything not Canada falls back to
87
90
  the **US** host, so US behavior is unchanged.
88
- - All 8 link-building sites build the URL as `"https://" . resolveSupplyHost($api) . "/?..."` (only
89
- the host varies; the query string / uuid is identical). Sites wired: `ApprovalDecision.php` —
90
- manager-approval-request block and VIP auto-approve block (`orderUrl` + `userOrderUrl` in each);
91
- `SalesOrder.php` — admin-approved block (`postPut`), in-transit/delivered block, and
92
- `_sendVipManagerNotification`.
93
91
  - This reuses the **same `$api->client->clientIdentifier` signal** as the email-template language
94
92
  resolver, so the link host and the email template can never disagree.
95
93
 
94
+ ### The supply order route: one builder, and the hosts stay hardcoded (2026-08-18)
95
+
96
+ The supply half of that link is now built by **one function**, so a future route change is a
97
+ one-line edit:
98
+
99
+ ```php
100
+ const PATH__SUPPLY_SALES_ORDER = '/sales-orders?sales-orders=';
101
+
102
+ public static function buildSupplyOrderUrl(&$api, string $salesOrderUuid): string {
103
+ return ('https://' . self::resolveSupplyHost($api) . self::PATH__SUPPLY_SALES_ORDER . $salesOrderUuid);
104
+ }
105
+ ```
106
+
107
+ The old format was
108
+ `https://<host>/?sales-orders_recordsPerPage=10&sales-orders_page=1&Orders=orders-view&uuid=<uuid>`
109
+ (Quad's variant used `recordsPerPage=15` and `&sales-orders-modal=records-view`). It was replaced
110
+ because it was landing on an error page in togasupply. The new shape follows the documented routing
111
+ standard — page slug in the path, variables in the query string — which is what
112
+ `toga2-supply`'s `getPageSlug()` reads (first path segment). **See the verification gotcha below
113
+ before trusting it.**
114
+
115
+ **Decision: the supply/commerce hosts stay hardcoded production constants. Do not make them
116
+ environment-aware.** An environment-derived host was built and deliberately reverted **twice** this
117
+ session, in two variants: (a) resolve the host from `Core.Domains` by `clientId` + `appId` +
118
+ `environmentId`, and (b) derive it from the api host serving the request
119
+ (`$api->response->authority`) by swapping the `api` label for the client subdomain and
120
+ `togahub.com` for `togasupply.com`. Both were judged over-engineering for this ticket — **these
121
+ crons and interceptors are production-only in practice**, and per-client environment coverage in
122
+ `Core.Domains` is too patchy to rely on (Compass Canada and Quad have only 3-4 environments
123
+ registered). If it is ever revisited, read
124
+ [Core.Domains — the app host registry](../../../2.0/apps/_underscore/features/core-domains-app-host-registry.md)
125
+ first; the discarded `_Component_Library_AppUrl` component is recoverable at `_underscore` commit
126
+ `0640b2de` — **do not rebuild it from scratch.**
127
+
128
+ **Accepted consequence:** in any non-production environment these emails link to production, where
129
+ the order does not exist. This is **not a regression** — `origin/_production` already carried
130
+ prod-only hosts; the 2026-08-18 change altered only the *path*. A CodeRabbit review flagged it and
131
+ it was dismissed as pre-existing and out of scope.
132
+
133
+ ### Complete inventory of supply order-link generation (leverage points)
134
+
135
+ Only **three** clients ever build a supply order link — Compass USA, Compass Canada, and Quad.
136
+ None of the other 29 client model folders in `_underscore/Model` do. So this list is the whole
137
+ surface for any future supply route change:
138
+
139
+ **2.0 (`_underscore`) — 5 sites, all migrated 2026-08-18:**
140
+ - `Model/Compass/SalesOrder.php` ~L1059 (Manager Approval Needed) and ~L1185 (VIP Manager
141
+ Auto-Approved)
142
+ - `Model/Compass/ApprovalDecision.php` ~L422 (Manager Approval Request) and ~L729 (manager
143
+ reassignment)
144
+ - `Model/Quad/SalesOrder.php` ~L71 (Quad Approval Reminder) — Quad has its **own**
145
+ `const URL__SUPPLY_SALES_ORDER`, it does not share the Compass builder
146
+ - The first four all funnel through `_Model_Compass_SalesOrder::buildSupplyOrderUrl()`, so that
147
+ one function is the leverage point for Compass.
148
+
149
+ **1.0 (`worker`) — 2 sites, still UNCHANGED:** the manager-approval reminder crons still send a
150
+ bare supply homepage with no uuid. Details in
151
+ [Compass manager approval reminder emails](../../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md).
152
+
153
+ **`worker2` — zero** supply or commerce link generation.
154
+
155
+ **Out of scope but the same bug class, deliberately left alone:** the user-facing commerce
156
+ `userOrderUrl` links (`Model/Compass/SalesOrder.php` ~L484 and ~L1060,
157
+ `Model/Compass/ApprovalDecision.php` ~L423 and ~L730) plus roughly ten inline commerce URL literals
158
+ across the worker crons.
159
+
96
160
  ### VIP manager auto-approve — the rule and its three (all supervisor-derived) triggers
97
161
  A single rule governs VIP auto-approve: a **manager-stage (step 2)** approval auto-approves iff the
98
162
  assigned manager's `Users.c_isVip = 1` **AND** the order subtotal
@@ -170,6 +234,41 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
170
234
  `getFilteredCcEmails()`.
171
235
 
172
236
  ## Gotchas / known issues
237
+ - **⚠ UNVERIFIED, AND THERE IS CONTRARY EVIDENCE: `/sales-orders?sales-orders=<uuid>` may not open
238
+ the order.** The new route was never loaded in a running supply app. Checking `toga2-supply` on
239
+ **both `_beta` and `origin/_production`** (2026-08-18) found two problems:
240
+ 1. **`src/App.tsx` declares no `/sales-orders` route.** The Orders page is mounted at `path="/"`;
241
+ the only other order route is `/order-view/:uuid`. An unmatched path falls through to
242
+ `<Route path="/*" element={<PageNotFound />} />`.
243
+ 2. **Nothing reads a query param named after the page slug.** The details modal opens off
244
+ `?uuid=<uuid>` (`useManageModalUrl.ts` READ effect) and *writes* `?uuid=…&<slug>-modal=records-view`.
245
+ There is no `searchParams.get('sales-orders')` anywhere in the repo.
246
+
247
+ Note this also means the **old** URL's `uuid=` param was the one part that was right. Whatever was
248
+ breaking it ("something went wrong in togasupply") was more likely the
249
+ `sales-orders_recordsPerPage` / `sales-orders_page` params or the bogus `Orders=orders-view`, not
250
+ the `uuid`. **All 5 of the 2.0 link sites depend on the new route being correct — load it against
251
+ a real supply deploy before this ships**, and if it 404s the likely correct shape is
252
+ `/?uuid=<uuid>` (plus `&sales-orders-modal=records-view`) or the dedicated `/order-view/<uuid>`
253
+ route.
254
+ - **⚠ Fixing the code does not fix the emails: the stored template bodies hardcode the production
255
+ supply host.** This is **data, not code**. Verified in prod 2026-08-18: `Client_Compass` has **5**
256
+ `EmailTemplates` rows containing `togasupply` (ids `4, 22, 23, 27, 28`); `Client_CompassCanada`
257
+ has **9** (id `6` inactive, plus `19, 20, 27, 28, 32, 33, 34, 35`); `Client_Quad` has **0**. Two
258
+ distinct problems:
259
+ - Some are footer `href` values.
260
+ - Compass ids **27 and 28** (and the Canada equivalents) carry `compass.togasupply.com` as
261
+ **visible link text** while the `href` is `{orderUrl}` / `{userOrderUrl}` — so the email
262
+ *displays* a production hostname no matter what the code builds. Worse, **id 28** shows supply
263
+ link text over a `{userOrderUrl}` href that points at **commerce**.
264
+ - **When sweeping templates, match BOTH quote styles.** Compass id `4` uses single-quoted
265
+ `href='…'` and is missed entirely by a double-quote-only regex.
266
+ - **⚠ Pre-existing SQL escaping gap in this path (separate ticket, not fixed).**
267
+ `_Model_Compass_SalesOrder` ~L978 interpolates `$salesOrderUuid` straight into SQL —
268
+ `SalesOrders.uuid = '" . $salesOrderUuid . "'` — while ~L1037 **in the same file** correctly uses
269
+ `_Database::escape()`. The value is route-derived user input: `postPut` takes it as
270
+ `end(explode('/', $api->route))`. Fix it with `_Database::escape()` per the 2.0 back-end standard;
271
+ do not copy the unescaped line as a pattern.
173
272
  - **⚠ The approval gate is strict user-id equality, so a stale assignment is unrecoverable by the
174
273
  user.** `_Model_Client_ApprovalTemplateStage` decides "may this logged-in user act on this stage?"
175
274
  by comparing `ApprovalDecisions.assignedToUserId` against the id resolved from the logged-in user's
@@ -253,11 +352,32 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
253
352
  subclasses are empty, **Compass Canada orders inherited the US links** and sent Canada
254
353
  recipients to the US supply/commerce site, which does not contain their order — so the link never
255
354
  opened. Always build the host via `_Model_Compass_SalesOrder::resolveSupplyHost()` /
256
- `resolveCommerceHost()`, never a literal. Real incident: order SAC100650 manager Gary Berenz
355
+ `resolveCommerceHost()`, never a literal and for a supply **order** link use
356
+ `buildSupplyOrderUrl()`, which wraps the host resolver *and* the route. Real incident: order
357
+ SAC100650 — manager Gary Berenz
257
358
  could not open the order from the "Manager Approval Needed" email, so admin Kai Wong had to
258
359
  approve as manager on his behalf. (Fixed 2026-07-23.)
259
360
 
260
361
  ## Change history
362
+ - 2026-08-18 — **Migrated the supply order deep link in approval emails to the new supply route.**
363
+ All 5 of the 2.0 link sites (Compass `SalesOrder.php` ~L1059/~L1185, `ApprovalDecision.php`
364
+ ~L422/~L729, `Quad/SalesOrder.php` ~L71) now build
365
+ `https://<host>/sales-orders?sales-orders=<uuid>` instead of the old
366
+ `/?sales-orders_recordsPerPage=10&sales-orders_page=1&Orders=orders-view&uuid=<uuid>`, which was
367
+ producing an error page. Compass US/Canada share the new single builder
368
+ `_Model_Compass_SalesOrder::buildSupplyOrderUrl()` (host still via `resolveSupplyHost()`); Quad
369
+ keeps its own `URL__SUPPLY_SALES_ORDER` constant. Affected emails: Manager Approval Request,
370
+ Manager Approval Needed, VIP Manager Auto-Approved, Quad Approval Reminder. **Decided the hosts
371
+ stay hardcoded production constants** — an environment-aware builder (from `Core.Domains`, then
372
+ from `$api->response->authority`) was built and reverted twice as over-engineering; the discarded
373
+ `_Component_Library_AppUrl` is recoverable at commit `0640b2de`. Also recorded: the full
374
+ cross-tier inventory of supply-link sites (only Compass US/Canada/Quad build one; the two 1.0
375
+ `worker` reminder crons are **still unfixed**); that the prod `EmailTemplates` bodies hardcode
376
+ `togasupply` in 5 Compass and 9 Compass Canada rows, two of them as *visible link text* over an
377
+ `{orderUrl}` href; and a pre-existing **unescaped `$salesOrderUuid`** at ~L978. **⚠ The new route
378
+ is UNVERIFIED and `toga2-supply` (`_beta` and `_production`) has no `/sales-orders` route and no
379
+ reader for a `?sales-orders=` param — see the first gotcha before shipping.** `_underscore`
380
+ branch `TRUE-81048`, commits `0640b2de` then `a0378208`. (bala)
261
381
  - 2026-08-13 — **Fixed the delegate-manager lookup picking the wrong duplicate `Users` row.** Both
262
382
  delegate lookups in `_Model_Compass_SalesOrder` (`postPost` and `postPut`) ran
263
383
  `SELECT Users.uuid FROM Users WHERE Users.email = '<escaped>' LIMIT 1` with no `ORDER BY` and no
@@ -324,3 +444,7 @@ requester**. All of these comparisons are case-insensitive (`strcasecmp()`), mat
324
444
  `Compass_Canada` branch drives its EN/FR notification localization.
325
445
  - [Email Template Sending](../../../2.0/apps/_underscore/features/email-template-sending.md) — the
326
446
  shared engine the resolved approval-notification templates are sent through.
447
+ - [Core.Domains — the app host registry](../../../2.0/apps/_underscore/features/core-domains-app-host-registry.md)
448
+ — read this before any attempt to make the email link hosts environment-aware.
449
+ - [Compass manager approval reminder emails](../../../1.0/apps/worker/features/compass-manager-approval-reminder-emails.md)
450
+ — the two 1.0 `worker` crons that build the same link and are **still on the bare host, no uuid**.
@@ -5,7 +5,7 @@
5
5
  | [Elite — NetSuite → TOGa Supply inbound sync (TRUE-80499 onboarding)](features/netsuite-togasupply-sync.md) | 1.0 | Elite is the 18th client on the shared NetSuite → TOGa Supply importer ([engine](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md)). | worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/PurchaseOrder.php, _underscore/Model/Elite/ItemReceipt.php, dbchanges2/Client_Elite/_modules.txt, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
6
6
  | [Elite SalesOrder → NetSuite Push (postPost/postPut interceptors → worker2)](features/salesorder-netsuite-push.md) | 2.0 | Elite orders created in Toga are pushed into NetSuite **event-driven**, not on a cron. | _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
7
7
  | [Elite — Sales Order stage change posts a reply on the TOGa Desk (1.0) ticket](features/salesorder-status-togadesk-reply.md) | 2.0 | When an Elite sales order's **stage** changes, a reply is posted on the originating **TOGa Desk (1.0)** ticket so the requester sees progress where they raised | worker2/Worker/Sync/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrderStatus.php, _underscore/Model/Elite/SalesOrder.php |
8
- | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
8
+ | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Orders/view/OrderView/viewModel/FIELDS/ELITE/BASEFIELDS.json, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/hooks/useFetchData.tsx, toga2-supply/src/components/ui/Toaster.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/formatCustomerName.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql |
9
9
  | [Elite — stale TableView config (11 dead Core.RecordFields across 9 views; 2 now fixed)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Elite/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client_Elite/2026-08-17 - FulfillmentTableViews.sql, dbchanges2/Core/2026-08-07 - ServiceRequests TableView.sql, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
10
10
  | [Elite — raising a Service Request from a TOGa Desk ticket (App_Api_ServiceRequest)](features/togadesk-service-request-intake.md) | 1.0 | An Elite agent raises a **Service Request** from a TOGa Desk (1.0) ticket via a modal. | library/app/api/servicerequest.php, library/app/model/togadesk/ticket.php, togadesk/desk/includes/classes/class.ticket.php, togadesk/desk/template/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/modals/tickets/serviceRequest.php, togadesk/desk/includes/controllers/actions/tickets/serviceRequest.php, dbchanges2/Client_Elite/2026-08-11a - EliteServiceRequestTicketUnique.sql, test/@srija/Elite Testing/Service Requests/test_elite_desk_service_request.php |
11
11
  | [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, worker2/Worker/Sync/ServiceRequest.php, library/app/api/toga2.php, library/app/api/servicerequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, togadesk/desk/includes/classes/class.ticket.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php |
@@ -32,11 +32,13 @@ files:
32
32
  - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx
33
33
  - toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx
34
34
  - toga2-supply/package.json
35
+ - dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql
35
36
  related:
36
37
  - ../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md
37
38
  - ../../../2.0/apps/toga2-supply/features/order-detail-field-config-and-customer-name.md
38
39
  - ./supply2-tableview-config-drift.md
39
40
  - ../profile.md
41
+ - ../../../2.0/apps/_underscore/features/tableview-joins.md
40
42
  ---
41
43
 
42
44
  ## Summary
@@ -73,6 +75,15 @@ One host-scoped group-by view only: **"Units By Items"**, no group-by modal.
73
75
  - Local run: `npm run elite` (`vite --host elite.togasupply`) plus a
74
76
  `127.0.0.1 elite.togasupply` hosts-file entry.
75
77
 
78
+ **The nested `inventory_units` view now shows what the hardware actually is (2026-08-18).** Unit rows
79
+ previously carried only serial / asset tag, so a user could not identify the item without opening it.
80
+ Part Number, Manufacturer and Item Description were added directly after Asset Tag, mirroring
81
+ `inventory_items`, via `dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql` (an
82
+ identical file was applied for NYCHH). Manufacturer needs a **chained** Units → Items → Manufacturers
83
+ join — mechanism, ACL requirement and the column-reorder hazard are in
84
+ [tableview-joins](../../../2.0/apps/_underscore/features/tableview-joins.md). Purely backend metadata;
85
+ no frontend change.
86
+
76
87
  **Hardening from the CodeRabbit review (2026-08-10)** — guardrail-preserving variants, not the
77
88
  reviewer's literal diffs:
78
89
 
@@ -200,6 +211,12 @@ byte-identical in behavior** — that is the bar for editing shared UI here (see
200
211
  2026-08-07 prod→beta refresh that wiped the hand-built `service-requests` view.
201
212
 
202
213
  ## Change history
214
+ - 2026-08-18 — Added **Part Number / Manufacturer / Item Description** to the `inventory_units` table
215
+ view directly after Asset Tag (`dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql`,
216
+ same file content shipped for NYCHH; prod + client-sandbox), so unit rows identify the hardware
217
+ instead of showing serial/asset tag alone. Manufacturer comes through the first chained
218
+ Units → Items → Manufacturers join in `Client_Elite`; backend metadata only, no frontend change.
219
+ (bala)
203
220
  - 2026-08-18 - Added `ELITE: "Elite"` to `formatCustomerName.tsx`'s `CUSTOMER_MAP` so the
204
221
  order-details **Customer** row shows a name instead of a dash. Recorded that this row is
205
222
  **hardcoded in the frontend for every host** - the shared `valueKey`
@@ -2,4 +2,4 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | |
5
+ | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
@@ -15,11 +15,13 @@ project: _Underscore
15
15
  client: nychh
16
16
  type: profile
17
17
  status: active
18
- updated: 2026-08-17
18
+ updated: 2026-08-18
19
19
  owners: ["jcardinal", "apeterson", "bala"]
20
- files: []
20
+ files:
21
+ - dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql
21
22
  related:
22
23
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
24
+ - ../../2.0/apps/_underscore/features/tableview-joins.md
23
25
  ---
24
26
 
25
27
  ## Summary
@@ -34,6 +36,11 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
34
36
  run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
35
37
 
36
38
  ## Key features (this client)
39
+ - **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
40
+ and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
41
+ Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
42
+ to Elite's file; applied in prod and client-sandbox). Mechanism and the reorder hazard:
43
+ [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md).
37
44
  - Item-fulfillment table views (`item-fulfillments-for-sales-orders`,
38
45
  `item-fulfillments-for-sales-order-items`) rebuilt 2026-06-19 to mirror the Compass fix —
39
46
  re-rooted at `ItemFulfillmentItems` with item-level (record 318) tracking. See
@@ -22,6 +22,7 @@ related:
22
22
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
23
23
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
24
24
  - ../../2.0/apps/_underscore/features/surface-resolver.md
25
+ - ../compass-usa/features/approval-decision-flow.md
25
26
  ---
26
27
 
27
28
  ## Summary
@@ -91,4 +92,12 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
91
92
  - **Sales-orders listing default sort = `dateOrder DESC` (2026-08-18)** — most recent first
92
93
  (was `number DESC`), set on Quad's `TableViews` row. See
93
94
  [TableView field/column metadata](../../2.0/apps/api2/features/tableview-field-metadata.md).
95
+ - **Approval-reminder email supply link (2026-08-18).** Quad's "Quad Approval Reminder" email builds
96
+ its own supply order link from `_Model_Quad_SalesOrder::URL__SUPPLY_SALES_ORDER` (~L71) — it does
97
+ **not** share the Compass builder, so a Compass-side route change does not carry over to Quad and
98
+ vice versa. Migrated to `https://quad.togasupply.com/sales-orders?sales-orders=<uuid>` (was the
99
+ old `recordsPerPage=15` + `sales-orders-modal=records-view` query-string format). Quad has **0**
100
+ `EmailTemplates` rows containing a hardcoded `togasupply` host, so unlike Compass there is no
101
+ template-body sweep to do. **The new route is unverified** — see the gotcha in
102
+ [Compass approval-decision flow](../compass-usa/features/approval-decision-flow.md).
94
103
  - This profile is a starting point; expand as more Quad-specific behavior is captured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.608",
3
+ "version": "1.0.610",
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",