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.
- package/knowledge/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/compass-manager-approval-reminder-emails.md +94 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +2 -0
- package/knowledge/2.0/apps/_underscore/features/core-domains-app-host-registry.md +131 -0
- package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +171 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +41 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/approval-decision-flow.md +131 -7
- package/knowledge/clients/elite/INDEX.md +1 -1
- package/knowledge/clients/elite/features/supply2-scope.md +17 -0
- package/knowledge/clients/nychh/INDEX.md +1 -1
- package/knowledge/clients/nychh/profile.md +9 -2
- package/knowledge/clients/quad/profile.md +9 -0
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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) —
|
|
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)_ —
|
|
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-
|
|
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
|
|
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-
|
|
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