toga-ai 1.0.744 → 1.0.745
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/autoincrement-record-numbering.md +96 -0
- package/knowledge/2.0/apps/_underscore/features/config-group-access.md +35 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +41 -1
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +18 -1
- package/knowledge/2.0/apps/dbchanges2/workflows/nonprod-metadata-drift-repair.md +47 -1
- package/knowledge/2.0/apps/toga2-commerce/features/inactive-item-purchase-gating.md +23 -1
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +18 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md +46 -2
- package/knowledge/2.0/apps/worker2/features/netsuite-transferorder-outbound-push.md +302 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/item-catalogs-and-duplicate-items.md +26 -4
- package/knowledge/clients/compass-usa/profile.md +13 -0
- package/knowledge/clients/compass-usa/workflows/changing-a-kit-line-item.md +165 -0
- package/knowledge/clients/compass-usa/workflows/granting-persona-bundle-access.md +21 -1
- package/knowledge/clients/nychh/INDEX.md +2 -1
- package/knowledge/clients/nychh/features/netsuite-transfer-order-import.md +23 -2
- package/knowledge/clients/nychh/features/transfer-order-netsuite-push.md +110 -0
- package/knowledge/clients/nychh/profile.md +9 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
|
|
11
11
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
12
12
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
13
|
+
| [FIELD_AUTOINCREMENT record numbering (SA / TA prefixes, NULL-only trigger, no padding)](features/autoincrement-record-numbering.md) | A human-facing record number (`SA100001`, `TA100000`) is generated by the ORM, not by MySQL. | _underscore/Model.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/TransferOrder.php |
|
|
13
14
|
| [FIELD_SQL calculated fields — the underscore-prefix + same-name-method contract](features/calculated-sql-fields.md) | A `FIELD_SQL` (calculated) field on a `_Model` is bound by a **two-part contract that `_Model` enforces by throwing at model-construction time**, not by convent | _underscore/Model.php, _underscore/Model/Client/PurchaseOrder.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/ServiceRequest.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, api2/Component/Api/V2/V2.php, _underscore/Model/Quad/Item.php, _underscore/Model/Quad/VendorItem.php, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
|
|
14
15
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | test/@Mark/true-80824-fedex-inflate-test.php, dbchanges2/Client_Growrk/2026-08-10e - GrowrkUpsServiceMethodCodes.sql, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Component/Library/Carriers/Usps/Usps.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
15
16
|
| [Running 2.0 code from a bare CLI script (bootstrap + transactions)](features/cli-script-bootstrap.md) | A throwaway CLI script (a data check, a backfill dry-run, a render harness) that wants the real 2.0 framework — `_Model`, `_Query`, `_Database` — is **not** the | _underscore/Database.php, _underscore/Environment.php, api2/Initialize.php |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "FIELD_AUTOINCREMENT record numbering (SA / TA prefixes, NULL-only trigger, no padding)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-02
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model.php
|
|
13
|
+
- _underscore/Model/Client/SalesOrder.php
|
|
14
|
+
- _underscore/Model/Client/TransferOrder.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../../worker2/features/netsuite-transferorder-outbound-push.md
|
|
18
|
+
- ../../worker2/features/netsuite-salesorder-outbound-push.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
A human-facing record number (`SA100001`, `TA100000`) is generated by the ORM, not by MySQL. A model
|
|
24
|
+
declares it as `FIELD_AUTOINCREMENT` with two options:
|
|
25
|
+
|
|
26
|
+
```php
|
|
27
|
+
public $number = [
|
|
28
|
+
self::FIELD_AUTOINCREMENT,
|
|
29
|
+
self::FIELDOPT_AUTOINCREMENT_PREFIX => 'TA',
|
|
30
|
+
self::FIELDOPT_AUTOINCREMENT_FIRSTNUMBER => 100000,
|
|
31
|
+
];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`_Model_Client_SalesOrder` uses `SA` / 100000; `_Model_Client_TransferOrder` was given `TA` / 100000
|
|
35
|
+
on 2026-09-02, on the **shared** client model, so every client's transfer orders get it.
|
|
36
|
+
|
|
37
|
+
## How it works (verified in `_underscore/Model.php` ~L546)
|
|
38
|
+
|
|
39
|
+
1. **The generator only fires when the field arrives NULL** (`if (is_null($value))`). **A supplied
|
|
40
|
+
number always wins.** That is what makes an importer that sends its own number (the NetSuite
|
|
41
|
+
importer sends `number = $nsOrder->tranId`) completely unaffected by adding the option — and it is
|
|
42
|
+
also why a front-end that still posts `"number": "286906"` means the generator **never runs**.
|
|
43
|
+
2. **The next value is `MAX(numeric part) + 1` scoped by `WHERE number LIKE '<prefix>%'`.** So
|
|
44
|
+
pre-existing numbers in another format are ignored: NYCHH's 1,930 legacy transfer-order numbers
|
|
45
|
+
(e.g. `286906`) do not participate, and the sequence starts clean at `TA100000`.
|
|
46
|
+
3. **There is NO zero padding.** The output is prefix + integer, so `TA00000` is not achievable
|
|
47
|
+
without changing `Model.php`.
|
|
48
|
+
|
|
49
|
+
## ⚠ A prefix on a SHARED model is not unique across clients
|
|
50
|
+
|
|
51
|
+
Because the option lives on `_Model_Client_TransferOrder`, **every tenant starts at `TA100000`** —
|
|
52
|
+
the scan is per client database. Any cross-client identifier must therefore **not** be keyed on
|
|
53
|
+
`number`:
|
|
54
|
+
|
|
55
|
+
- NetSuite account **1095849 is shared by every client** and enforces `externalId` uniqueness
|
|
56
|
+
account-wide, which is why both pushes key their external id on the record **uuid**
|
|
57
|
+
(`toga-so-<uuid>` / `toga-to-<uuid>`) — see
|
|
58
|
+
[the transfer-order push](../../worker2/features/netsuite-transferorder-outbound-push.md).
|
|
59
|
+
|
|
60
|
+
## A generated number can be a PLACEHOLDER — decide its lifetime explicitly
|
|
61
|
+
|
|
62
|
+
`SA`/`TA` numbers are **overwritten by NetSuite's `tranId`** on write-back, so the generated value
|
|
63
|
+
lives about 12 seconds. That is the intended behaviour for both order types, but it makes `number` a
|
|
64
|
+
**moving value** — never build a key, a match, or an external id on it.
|
|
65
|
+
|
|
66
|
+
**Keeping a Toga-side number permanently takes TWO changes, not one** (written 2026-09-02, then
|
|
67
|
+
**reverted** at the developer's request — recorded so nobody rediscovers it):
|
|
68
|
+
|
|
69
|
+
1. a guard in `writeBackNetSuiteId()` so the push does not overwrite `number`, **and**
|
|
70
|
+
2. dropping `number` from the PUT in **both** `library/app/api/toga2.php` (~L4295) and
|
|
71
|
+
`_underscore/Trait/Netsuite/TransferOrder.php`.
|
|
72
|
+
|
|
73
|
+
Miss (2) and the 1.0 importer compares `number` and PUTs NetSuite's over the top on its next run, so
|
|
74
|
+
the guard alone achieves nothing. Both files are back to untouched (verified with an empty
|
|
75
|
+
`git diff`).
|
|
76
|
+
|
|
77
|
+
## Gotchas / known issues
|
|
78
|
+
|
|
79
|
+
- **`FIELD_AUTOINCREMENT` is one of the field types special-cased by `buildSqlFieldValue()`**, so a
|
|
80
|
+
null value on it does **not** collapse a `load()` WHERE clause the way a null `FIELD_CHAR` does —
|
|
81
|
+
see the collapsed-WHERE OOM note in [_underscore architecture](../architecture.md).
|
|
82
|
+
- **Adding the option to a model does not renumber anything** and does not touch existing rows; it
|
|
83
|
+
only affects inserts that arrive with `number` NULL.
|
|
84
|
+
- **A front-end that sends `number` silently disables the feature.** There is no error — the posted
|
|
85
|
+
value is simply accepted. Check the actual POST body before concluding the generator is broken.
|
|
86
|
+
|
|
87
|
+
## Change history
|
|
88
|
+
|
|
89
|
+
- 2026-09-02 — Documented `FIELD_AUTOINCREMENT` numbering while giving
|
|
90
|
+
`_Model_Client_TransferOrder.number` a `TA` / 100000 sequence (mirroring `SalesOrder`'s `SA`) on the
|
|
91
|
+
**shared** client model: the generator fires **only** on a NULL value, the next number is
|
|
92
|
+
`MAX(numeric) + 1` scoped by `number LIKE '<prefix>%'` (so 1,930 legacy NYCHH numbers are ignored),
|
|
93
|
+
and there is **no zero padding**. Recorded that a shared-model prefix is not unique across tenants
|
|
94
|
+
(every client starts at `TA100000`), so cross-client keys must use the uuid; and that keeping a
|
|
95
|
+
Toga-side number permanently needs **both** a `writeBackNetSuiteId` guard and dropping `number`
|
|
96
|
+
from the 1.0 importer's PUT — written and then reverted. (bala)
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: ["mhammontree", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Config.php
|
|
@@ -92,6 +92,32 @@ the two keys to `beta.ini` and `sandbox-dev.ini`.
|
|
|
92
92
|
S3 helper) from a new tier, **diff that tier's ini against `production.ini` for the keys the helper
|
|
93
93
|
reads** — the group existing proves nothing.
|
|
94
94
|
|
|
95
|
+
## Do NOT move a correctness-critical constant into the ini (rejected suggestion, 2026-09-02)
|
|
96
|
+
|
|
97
|
+
A code-review bot (CodeRabbit) proposed moving `const NETSUITE_READ_DEPTH = 4` out of
|
|
98
|
+
`_Model_Client_TransferOrder` and into `Config/<environment>.ini` behind `_Config`. **Rejected**, and
|
|
99
|
+
the reasoning generalises:
|
|
100
|
+
|
|
101
|
+
1. **It is not a setting, it is a fact about the data shape.** The value the code needs sits three
|
|
102
|
+
FK hops out (`transferOrder` → `purchaseOrdersTransferOrders` → `purchaseOrder` → `number`), so 4
|
|
103
|
+
is the *minimum* depth that returns it — and at 3 the value comes back **missing with no error**.
|
|
104
|
+
A knob whose only correct value is a property of the query is not configuration.
|
|
105
|
+
2. **The ini is per environment, so it invites exactly the drift class we already fight** — a depth
|
|
106
|
+
that differs between prod, beta and sandbox (see
|
|
107
|
+
[non-prod metadata drift](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md)).
|
|
108
|
+
3. **Precedent is in code:** `_Worker_Netsuite_SalesOrder::TOGA_FETCH_DEPTH = 4` is a class constant
|
|
109
|
+
and `_Model_Elite_SalesOrder` hardcodes `'depth' => 4` inline.
|
|
110
|
+
4. **The suggested call did not even exist.** `_Config` has **no `group()` method** — it dispatches
|
|
111
|
+
through `__callStatic`, so it would have been `_Config::netsuite('readDepth')`, which **throws on a
|
|
112
|
+
missing key** (`Config.php` ~L79). An uncaught throw from a `postPost` interceptor returns
|
|
113
|
+
**HTTP 500 `EO-1` and rolls the write back**, so the "cleanup" would have let a one-line ini edit
|
|
114
|
+
take the transfer-order write endpoint down.
|
|
115
|
+
|
|
116
|
+
> **General rule: a depth/shape constant that a query's correctness depends on stays in code.** Move
|
|
117
|
+
> to the ini only what genuinely varies by environment (hosts, credentials' location, feature tiers).
|
|
118
|
+
> And when a review suggests a `_Config` call, check the method exists — a plausible-looking
|
|
119
|
+
> `_Config::group('x')` is not part of this API.
|
|
120
|
+
|
|
95
121
|
## Gotchas / known issues
|
|
96
122
|
|
|
97
123
|
- **`_Config::<group>('prop', false)` is NOT a null-safe read.** It is property-optional only.
|
|
@@ -106,6 +132,14 @@ reads** — the group existing proves nothing.
|
|
|
106
132
|
|
|
107
133
|
## Change history
|
|
108
134
|
|
|
135
|
+
- 2026-09-02 — Recorded a **rejected** code-review suggestion to move
|
|
136
|
+
`_Model_Client_TransferOrder::NETSUITE_READ_DEPTH = 4` into `Config/<environment>.ini`: the value is
|
|
137
|
+
a fact about the read shape (the PO number sits 3 FK hops out; at depth 3 it returns **missing with
|
|
138
|
+
no error**), the ini is per environment so it would invite drift, precedent (`TOGA_FETCH_DEPTH`,
|
|
139
|
+
Elite's inline `'depth' => 4`) keeps it in code, and the suggested API does not exist —
|
|
140
|
+
`_Config` has **no `group()`**, it uses `__callStatic`, and a missing key **throws**, which from a
|
|
141
|
+
`postPost` interceptor is a 500 `EO-1` with the write rolled back. **General rule: a depth/shape
|
|
142
|
+
constant a query's correctness depends on stays in code.** (bala)
|
|
109
143
|
- 2026-08-10 — Recorded that a **missing key inside an existing group throws as well**
|
|
110
144
|
(`Config.php:79`), and that the risky keys are the ones **framework helpers** read on your behalf:
|
|
111
145
|
`_Worker::runTask`'s non-debug path needs `[cloud] aws_worker_queue_url` /
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
|
-
| [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Rate/Entitlement.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, _underscore/Model/Compass/ApprovalDecision.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql, dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql |
|
|
6
|
+
| [API Payload Interceptors (metadata-registered prePost/postPut model hooks)](features/api-payload-interceptors.md) | A `_Model`'s `prePost()` / `postPost()` / `prePut()` / `postPut()` hooks are **not** called by the model. | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Rate/Entitlement.php, _underscore/Model/Compass/PurchaseOrder.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Elite/SalesOrder.php, _underscore/Model/Elite/ServiceRequest.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/SalesOrderItem.php, _underscore/Model/Compass/ApprovalDecision.php, dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql, dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql, dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql |
|
|
7
7
|
| [Multi-Client (Cross-Client) Data Retrieval](features/cross-client-data-retrieval.md) | A single authenticated V2 GET listing can return records across **many** clients (designed for 1000+) that the caller is entitled to, honoring **each target cli | api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Cache/Table.php, _underscore/Model/Cache/Tables/Client.php, _underscore/Model/Core/Record.php, worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Cache/2026-06-30a - MultiClientCacheTables.sql, dbchanges2/Core/2026-06-30b - CacheClusterRegistrationAndRecordTtl.sql, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
8
8
|
| [cXML ShipNotice Gateway (ASN ingestion, carrier resolution, per-client provisioning)](features/cxml-shipnotice-gateway.md) | `_Component_Api_Cxml` accepts a supplier `ShipNoticeRequest` and translates it into a `POST /v2/advance-shipping-notices` on the V2 JSON engine. | api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/V2.php, api2/Controller/Index.php |
|
|
9
9
|
| [Encrypted-User-UUID Auth Handoff (/auth/encrypted-user-uuid)](features/encrypted-user-uuid-auth-handoff.md) | `POST /auth/encrypted-user-uuid` is the intended **cross-client / SSO-handoff identity mechanism**: given an encrypted `{client, user}` UUID pair, it mints a fr | api2/Component/Api/CrossClient/CrossClient.php |
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -15,6 +15,7 @@ files:
|
|
|
15
15
|
- _underscore/Model/Rate/Entitlement.php
|
|
16
16
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
17
17
|
- _underscore/Model/Client/SalesOrder.php
|
|
18
|
+
- _underscore/Model/Client/TransferOrder.php
|
|
18
19
|
- _underscore/Model/Elite/SalesOrder.php
|
|
19
20
|
- _underscore/Model/Elite/ServiceRequest.php
|
|
20
21
|
- _underscore/Model/Compass/SalesOrder.php
|
|
@@ -25,7 +26,9 @@ files:
|
|
|
25
26
|
- dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql
|
|
26
27
|
- dbchanges2/Client_Compass/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql
|
|
27
28
|
- dbchanges2/Client_CompassCanada/2026-08-14a - AddSalesOrderInactiveStandaloneItemPreInterceptors.sql
|
|
29
|
+
- dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
|
|
28
30
|
related:
|
|
31
|
+
- ../../worker2/features/netsuite-transferorder-outbound-push.md
|
|
29
32
|
- ../../../clients/rate/features/aig-contract-creation.md
|
|
30
33
|
- ./record-scripts.md
|
|
31
34
|
- ../../toga2-commerce/features/inactive-item-purchase-gating.md
|
|
@@ -329,6 +332,34 @@ The corollary is that **reading the client subclass is not enough to know who a
|
|
|
329
332
|
you must also read the parent chain *and* the interceptor rows of every tenant that overrides the
|
|
330
333
|
same record.
|
|
331
334
|
|
|
335
|
+
#### The deliberate exception — shared model + "the row is the switch" (2026-09-02)
|
|
336
|
+
|
|
337
|
+
The rule above is about **accidental** reach. Putting a hook on `_Model_Client_X` **on purpose** is
|
|
338
|
+
legitimate when the behaviour is generic and gated, and the interceptor row is the gate: *no row, no
|
|
339
|
+
call.* Worked example — the transfer-order → NetSuite push lives on
|
|
340
|
+
`_Model_Client_TransferOrder::postPost`, not on `_Model_Nychh_TransferOrder`, so *whoever has the
|
|
341
|
+
`transfer-orders` row gets it* (see
|
|
342
|
+
[Toga TransferOrder → NetSuite Push](../../worker2/features/netsuite-transferorder-outbound-push.md)).
|
|
343
|
+
|
|
344
|
+
Three conditions make that safe, and all three must hold before you copy the pattern:
|
|
345
|
+
|
|
346
|
+
1. **No client currently has a row for that record** — check every tenant, not just yours.
|
|
347
|
+
2. **The behaviour is client-agnostic**, resolving everything from the record's own FKs.
|
|
348
|
+
3. **You state the real prerequisite in the doc and the code.** Here the hook only works for a client
|
|
349
|
+
whose model uses `_Trait_Netsuite_TransferOrder` (that trait declares
|
|
350
|
+
`c_netsuiteInternalSalesOrderId`); NYCHH is the only one today, so **any other client given a
|
|
351
|
+
`transfer-orders` row would start pushing to NetSuite** and break on the missing field.
|
|
352
|
+
|
|
353
|
+
**Adding an interceptor row for a shared-model hook is a code change, not configuration.**
|
|
354
|
+
|
|
355
|
+
#### ⚠ Call `parent::postPost()` only if the parent actually declares it
|
|
356
|
+
|
|
357
|
+
Elite's hook **must** call its parent; the transfer-order hook **must not**.
|
|
358
|
+
`_Model_Client_TransferOrder` extends `_Model_True`, which declares **no `postPost`**, so
|
|
359
|
+
`parent::postPost()` there is a fatal — the same *"Call to undefined method"* class of failure as a
|
|
360
|
+
bad interceptor row, just from the other direction. **Read the parent chain up to `_Model_True`
|
|
361
|
+
before adding the call by habit.**
|
|
362
|
+
|
|
332
363
|
### `Core.Records` facts for interceptor debugging
|
|
333
364
|
|
|
334
365
|
- **`Core.Records.model` is UNIQUE** — exactly one record row per model class, so an interceptor's
|
|
@@ -595,6 +626,15 @@ and the failing environment**. It is a small table, and the drift is usually exa
|
|
|
595
626
|
|
|
596
627
|
## Change history
|
|
597
628
|
|
|
629
|
+
- 2026-09-02 — Recorded the **deliberate shared-model exception**: a hook may live on
|
|
630
|
+
`_Model_Client_X` when the behaviour is client-agnostic and the interceptor row is the gate (no row,
|
|
631
|
+
no call), with the transfer-order → NetSuite push on `_Model_Client_TransferOrder::postPost` as the
|
|
632
|
+
worked example — plus the three conditions that must hold (no tenant already has a row for that
|
|
633
|
+
record, the logic resolves everything from the record's own FKs, and the real prerequisite is
|
|
634
|
+
stated: here the client's model must use `_Trait_Netsuite_TransferOrder`). Adding such a row is a
|
|
635
|
+
**code change, not configuration**. Also recorded the mirror-image fatal: **call
|
|
636
|
+
`parent::postPost()` only if the parent declares it** — `_Model_Client_TransferOrder` extends
|
|
637
|
+
`_Model_True`, which has none, so the parent call Elite's hook requires is a fatal there. (bala)
|
|
598
638
|
- 2026-08-20 (later pass) — Recorded that **activating a client's FIRST interceptor row switches on
|
|
599
639
|
the entire INHERITED chain**, with the Elite production outage as the worked example: two new
|
|
600
640
|
`sales-orders` rows made every `POST /v2/sales-orders` return `EO-1` 500 with the order rolled
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: [tcox, bala, apeterson, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -154,6 +154,17 @@ GET /v2/transfer-orders/{uuid}/transfer-order-items
|
|
|
154
154
|
alternative to the 1.0 direct-query fix (the developer applied both). **Ordering caveat:** the
|
|
155
155
|
migration must run **after** the duplicate-`TransferOrderItems` cleanup, or the inline expansion
|
|
156
156
|
returns all the dup rows.
|
|
157
|
+
- **A BRIDGE table needs its own row too — and the header bridge is a different record from the item
|
|
158
|
+
bridge.** Reading a transfer order's **PO number** depends on
|
|
159
|
+
`transfer-orders (312) → purchase-orders-transfer-orders (325)`, the **header** bridge
|
|
160
|
+
(`_Model_Client_PurchaseOrders_TransferOrder`). Record **329**
|
|
161
|
+
(`purchase-order-items-transfer-order-items`) is the **item-level** bridge and is **not** what the
|
|
162
|
+
PO number reads through — production had 312→**329** and it had to be repointed to 325
|
|
163
|
+
(2026-09-02). Both rows are needed for the outbound transfer-order push, which reads lines *and*
|
|
164
|
+
PO number in one depth-4 call: see
|
|
165
|
+
[Toga TransferOrder → NetSuite Push](../../worker2/features/netsuite-transferorder-outbound-push.md).
|
|
166
|
+
Per-environment drift and repair order are in
|
|
167
|
+
[non-prod metadata drift](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
|
|
157
168
|
|
|
158
169
|
### ⚠ `join` / `ojoin` drive the WHERE/filter — NOT the response shape
|
|
159
170
|
|
|
@@ -353,6 +364,12 @@ ACL gate for calculated fields lives at **:7216** in `getFullModelData()` — wh
|
|
|
353
364
|
[V2 request logging](request-logging.md).
|
|
354
365
|
|
|
355
366
|
## Change history
|
|
367
|
+
- 2026-09-02 — Recorded that a **bridge** collection needs its own `InherentRecordChildren` row and
|
|
368
|
+
that the **header** bridge is a different record from the item bridge: a transfer order's PO number
|
|
369
|
+
reads through `312 → 325 purchase-orders-transfer-orders`, **not** `329
|
|
370
|
+
purchase-order-items-transfer-order-items`; production carried 329 and was repointed to 325. Both
|
|
371
|
+
312→313 and 312→325 are prerequisites for the outbound transfer-order push's single depth-4 read.
|
|
372
|
+
(bala)
|
|
356
373
|
- 2026-09-01 — **Recorded the biggest exception to this doc's own "always send an explicit `fields=`
|
|
357
374
|
list" advice: an `_`-prefixed field named in `fields=` is NEVER ACL-checked.** `V2.php:3338`
|
|
358
375
|
classifies a `FIELD_SQL` field as calculated *before* the ACL branch at `:3353`, and `:3353` then
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: ["mhammontree", "bala", "kyalamarthi"]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql
|
|
@@ -23,6 +23,8 @@ related:
|
|
|
23
23
|
- ../architecture.md
|
|
24
24
|
- ./client-schema-drift-audit.md
|
|
25
25
|
- ../../api2/features/api-payload-interceptors.md
|
|
26
|
+
- ../../api2/features/v2-rest-query-contract.md
|
|
27
|
+
- ../../worker2/features/netsuite-transferorder-outbound-push.md
|
|
26
28
|
- ../../api2/workflows/environment-configuration-and-provisioning.md
|
|
27
29
|
- ../../_underscore/features/acl-permission-chain.md
|
|
28
30
|
---
|
|
@@ -87,6 +89,42 @@ exercise TOGa Supply Fulfill & Ship (TRUE-80494).
|
|
|
87
89
|
6. **Re-test the feature end-to-end in that environment**, then record what is still missing so the
|
|
88
90
|
next person doesn't rediscover it.
|
|
89
91
|
|
|
92
|
+
## `Core.InherentRecordChildren` drifts too — and MySQL **1452** means the `Records` row is missing
|
|
93
|
+
|
|
94
|
+
`InherentRecordChildren` decides what a `/v2` read inlines (see
|
|
95
|
+
[V2 REST query contract](../../api2/features/v2-rest-query-contract.md)), so it is exactly the kind
|
|
96
|
+
of small metadata table that drifts per environment and produces "works in prod, empty in beta".
|
|
97
|
+
|
|
98
|
+
Worked example (2026-09-02, `transfer-orders` = record **312**, needed by the outbound
|
|
99
|
+
transfer-order push):
|
|
100
|
+
|
|
101
|
+
| Environment | State found | Action |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| prod | 312→313 and 312→**329** (the *item*-level bridge) | repointed to 312→313 + 312→**325** |
|
|
104
|
+
| client-sandbox | only 312→357 (`transfer-order-notes`) | added 313 and 325 |
|
|
105
|
+
| dev-sandbox | **no rows for 312 at all** | still pending |
|
|
106
|
+
|
|
107
|
+
> **⚠ `1452 Cannot add or update a child row: a foreign key constraint fails
|
|
108
|
+
> (Core.InherentRecordChildren, CONSTRAINT fk_inherentrecordchildren_child FOREIGN KEY (recordId)
|
|
109
|
+
> REFERENCES Records (id))` does NOT mean your bridge row is wrong — it means the `Core.Records` row
|
|
110
|
+
> does not exist in THAT environment.** dev-sandbox has records 312 and 313 but **no record 325 at
|
|
111
|
+
> all**. **Repair order is always `Records` → `RecordFields` → `InherentRecordChildren`.**
|
|
112
|
+
|
|
113
|
+
**Copy the production literals** — `Core.Records` / `Core.RecordFields` ids are team-maintained
|
|
114
|
+
platform constants, so the same numbers go in every environment (that is what lets a `Client/`
|
|
115
|
+
migration hardcode a `recordFieldId`; see the architecture rule and the open question below):
|
|
116
|
+
|
|
117
|
+
- **Record 325** — uuid `9b1c650c-f961-4718-bf29-c4ce4d54ae44`, `aclDatabase CLIENT`,
|
|
118
|
+
`childPolicy MATCH`, model `\_Model_Client_PurchaseOrders_TransferOrder`, route
|
|
119
|
+
`purchase-orders-transfer-orders`.
|
|
120
|
+
- **Its four `RecordFields`**, all `isIdentifier = 1`: **2209** `id`
|
|
121
|
+
(`b2b498b2-c4a2-4ba9-8d18-aa967322dedd`), **2210** `uuid`
|
|
122
|
+
(`a9724441-9d3b-4a73-acd2-409f22ed2ce7`), **2211** `purchaseOrderId`
|
|
123
|
+
(`81aecb84-c4d6-4281-867d-ef915495b8aa`), **2212** `transferOrderId`
|
|
124
|
+
(`a4d5967d-dd7b-41d8-b42c-54f5cebe6647`). Ids 2209–2212 were verified **free** in dev-sandbox.
|
|
125
|
+
- **Then** the two child rows: 312→313 and 312→325. Record **ACL** rows for 325 may also be needed —
|
|
126
|
+
untested; an `EZ-1` is the symptom.
|
|
127
|
+
|
|
90
128
|
## The `Client/` fan-out is skipped independently of `Core/` — and the gap is NON-CONTIGUOUS
|
|
91
129
|
|
|
92
130
|
Measured on dev-sandbox 2026-08-17 (TRUE-80562): **31 of 33 tenant DBs lack
|
|
@@ -257,6 +295,14 @@ is caught up, test against a **local Forecast fixture** instead — see
|
|
|
257
295
|
|
|
258
296
|
## Change history
|
|
259
297
|
|
|
298
|
+
- 2026-09-02 — Added `Core.InherentRecordChildren` as a drifting table, with record **312**
|
|
299
|
+
(`transfer-orders`) worked through all three environments: prod carried the **wrong** child (329,
|
|
300
|
+
the item-level bridge) and was repointed to **325**, client-sandbox had only 357 and got 313 + 325,
|
|
301
|
+
dev-sandbox still has none. Recorded the diagnostic that **MySQL 1452 on
|
|
302
|
+
`fk_inherentrecordchildren_child` means the `Core.Records` row is missing in that environment**
|
|
303
|
+
(dev-sandbox has no record 325 at all), that the repair order is **Records → RecordFields →
|
|
304
|
+
InherentRecordChildren**, and the prod literals to copy (record 325 + RecordFields 2209–2212, all
|
|
305
|
+
`isIdentifier = 1`, ids verified free in dev-sandbox). (bala)
|
|
260
306
|
- 2026-08-19 — Recorded that the drift is **not only metadata**: dev-sandbox's `Forecast/` **DDL** fan-out
|
|
261
307
|
appears unapplied since ~Feb 2026 — it has `Employees.departmentId` (2026-02) but is missing
|
|
262
308
|
`Sales.amountDue` (2026-06-09), `Customers.accountType` (2026-07-14) and `Opportunities.clickupTaskId`
|
|
@@ -6,7 +6,7 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/src/utils/checkIsItemPurchasable.ts
|
|
@@ -32,6 +32,7 @@ related:
|
|
|
32
32
|
- ../../api2/features/v2-rest-query-contract.md
|
|
33
33
|
- ../../../clients/compass-usa/profile.md
|
|
34
34
|
- ../../../clients/compass-canada/profile.md
|
|
35
|
+
- ../../../clients/compass-usa/workflows/changing-a-kit-line-item.md
|
|
35
36
|
---
|
|
36
37
|
|
|
37
38
|
## Summary
|
|
@@ -53,6 +54,14 @@ dev-sandbox `Client_Compass`, across bundles that are themselves active:
|
|
|
53
54
|
(32 inactive vs 3 active primaries). So a gate on the flag alone would break almost every kit on
|
|
54
55
|
the storefront.
|
|
55
56
|
|
|
57
|
+
**The tier is not a column on the item — it is the kit line's group.** A component's tier comes from
|
|
58
|
+
`BundleItems.bundleItemGroupId` joined to `BundleItemGroups`, whose slugs are **1 `primary` /
|
|
59
|
+
2 `secondary` / 3 `tertiary`** (same ids in `Client_Compass` and `Client_CompassCanada`, verified
|
|
60
|
+
prod 2026-09-02). The "hero" of a kit is simply its `primary` line. **A kit can carry more than one
|
|
61
|
+
*active* primary** — Compass bundles 27 and 140 each keep an old iPad hero (`MQ6J3LL/A-S`) alongside
|
|
62
|
+
a new one (`MD7F4LL/A-S`) — so never assume one primary per bundle. See
|
|
63
|
+
[Changing what is IN a Compass kit](../../../clients/compass-usa/workflows/changing-a-kit-line-item.md).
|
|
64
|
+
|
|
56
65
|
**Therefore the gate is on the purchase CONTEXT, never on the flag alone.** The discriminator is
|
|
57
66
|
whether the line belongs to a kit: `isset($payload->bundleItem)` server-side, the bundle wrapper
|
|
58
67
|
client-side. Kit lines are never evaluated.
|
|
@@ -172,11 +181,24 @@ Implementation:
|
|
|
172
181
|
had already opened an item kept seeing a working product page after it was switched off. That is
|
|
173
182
|
why the item-detail queries are `staleTime: 0` + `gcTime: 0` **and** excluded from persistence.
|
|
174
183
|
See [catalog cache freshness](./catalog-cache-freshness.md).
|
|
184
|
+
- **⚠ Do not resolve a hero (or any kit line) by `partNumber` alone.** `Client_Compass.Items` can
|
|
185
|
+
hold the same part number twice with opposite flags — `MD7F4LL/A-S` is id **2760** (`title` set,
|
|
186
|
+
`isActive = 0`, the row the kits actually reference) and id **2784** (`title` NULL,
|
|
187
|
+
`isActive = 1`, unused). A part-number lookup can land on the twin that no kit uses, and the two
|
|
188
|
+
disagree about `isActive`. Match on `BundleItems.itemId`.
|
|
175
189
|
- **The server check is one query per order, not per line** — uuids are deduped into a single
|
|
176
190
|
`IN (…)` lookup, and the empty list returns early because `IN ()` is a MySQL syntax error.
|
|
177
191
|
|
|
178
192
|
## Change history
|
|
179
193
|
|
|
194
|
+
- 2026-09-02 — Added how a component's **tier** is actually determined (`BundleItems.bundleItemGroupId`
|
|
195
|
+
to `BundleItemGroups`, slugs 1 `primary` / 2 `secondary` / 3 `tertiary` in both Compass schemas),
|
|
196
|
+
since the measured PRIMARY/secondary/tertiary table above never said where the tier comes from.
|
|
197
|
+
Recorded that a kit can hold **more than one active primary** (Compass bundles 27 and 140 keep
|
|
198
|
+
`MQ6J3LL/A-S` beside `MD7F4LL/A-S`), and added a gotcha that a hero must be resolved by
|
|
199
|
+
`BundleItems.itemId` rather than `partNumber` because `MD7F4LL/A-S` exists twice with **opposite**
|
|
200
|
+
`isActive` values (id 2760 used, `isActive = 0`; id 2784 unused, `isActive = 1`). Data findings from
|
|
201
|
+
a Compass kit data change; no code changed. (bala)
|
|
180
202
|
- 2026-08-14 — TRUE-80489: built the layered gate stopping an `Items.isActive = 0` item being
|
|
181
203
|
viewed, carted or ordered as a **standalone** line, with kit lines exempt. Recorded the measured
|
|
182
204
|
reason the flag alone is unusable (84 of 85 active Compass bundles contain an inactive component;
|
|
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-09-02
|
|
10
|
+
owners: [apeterson, bala]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/TransferOrders/TransferOrders.tsx
|
|
13
13
|
- toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts
|
|
@@ -49,6 +49,7 @@ related:
|
|
|
49
49
|
- ../../toga-blox/features/api-client.md
|
|
50
50
|
- ../../../../clients/nychh/profile.md
|
|
51
51
|
- ../../../../clients/nychh/features/transfer-order-inventory-quantities.md
|
|
52
|
+
- ../../worker2/features/netsuite-transferorder-outbound-push.md
|
|
52
53
|
---
|
|
53
54
|
|
|
54
55
|
## Summary
|
|
@@ -412,6 +413,15 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
|
|
|
412
413
|
- **Open dependency:** `Client/2026-08-28a` writes to `ThemeTokens`, a table created by
|
|
413
414
|
`Client/2026-06-25a - SurfaceClientTables.sql`. Any prod tenant that never received `2026-06-25a`
|
|
414
415
|
will error, and the full 36-tenant list was **not** audited.
|
|
416
|
+
- **🚨 Two REQUIRED changes to the Create Transfer Order POST (2026-09-02), both for the new
|
|
417
|
+
NetSuite push.** The push is triggered from `_Model_Client_TransferOrder::postPost` — see
|
|
418
|
+
[Toga TransferOrder → NetSuite Push](../../worker2/features/netsuite-transferorder-outbound-push.md):
|
|
419
|
+
1. **Drop `"number"` from the POST body.** The modal currently sends e.g.
|
|
420
|
+
`"number": "286906"`, and `FIELD_AUTOINCREMENT` only generates the new `TA100000` sequence when
|
|
421
|
+
`number` arrives **NULL** — a supplied value silently wins, with no error.
|
|
422
|
+
2. **Send `transferOrderItems` NESTED in the same POST.** `postPost` reads the order back
|
|
423
|
+
in-request; if the lines are posted in separate calls afterwards it sees **zero lines** and the
|
|
424
|
+
push aborts (and the trigger would have to move).
|
|
415
425
|
- ~~No `transfer-orders-list-actions` BUTTON_BAR surface~~ — **closed** by `Core/2026-08-28b`
|
|
416
426
|
(Surface 52, elements 187–189); the header is now `SurfaceActionBar`-driven like Sales Orders.
|
|
417
427
|
- **Test data lives in the session scratchpad, never in dbchanges2** (24 seed orders + an unseed
|
|
@@ -479,6 +489,12 @@ deliberate, separate exception — see [surface-frontend](./surface-frontend.md)
|
|
|
479
489
|
earlier migration without names, so a name-based lookup on them finds nothing.
|
|
480
490
|
|
|
481
491
|
## Change history
|
|
492
|
+
- 2026-09-02 — Recorded two **required** changes to the Create Transfer Order POST, both driven by
|
|
493
|
+
the new [TransferOrder → NetSuite push](../../worker2/features/netsuite-transferorder-outbound-push.md):
|
|
494
|
+
**drop `"number"`** (the new `TA100000` `FIELD_AUTOINCREMENT` sequence only generates when `number`
|
|
495
|
+
arrives NULL — a supplied value silently wins) and **send `transferOrderItems` nested in the same
|
|
496
|
+
POST** (the `postPost` hook reads the order back in-request; lines posted separately mean it sees
|
|
497
|
+
zero lines and the push aborts). (bala)
|
|
482
498
|
- 2026-09-01 — **Rebuilt the Create Transfer Order modal against the live Claude Design export and
|
|
483
499
|
closed two of its three data-layer blockers.** The **single-source lock is superseded**: Source
|
|
484
500
|
Location is now the one active `allowSending = 1` location, read-only from open and never derived
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
| [Toga → NetSuite Sales-Order Push (multi-client, mapping-driven worker)](features/netsuite-salesorder-outbound-push.md) | The **outbound** half of `worker2/Worker/Netsuite/SalesOrder.php` (everything from the `OUTBOUND PUSH (REST)` banner down) pushes a Toga sales order **into** Ne | worker2/Worker/Netsuite/SalesOrder.php, test/@Bala/tests/netsuite_salesorder_payload_tests.php, worker/crons/toga2/prudential/transmissions_to_netsuite.php |
|
|
34
34
|
| [NetSuite Supporting-Record Backfill Worker (bulk reconciliation recipe)](features/netsuite-supporting-record-backfill-worker.md) | The **bulk counterpart** to the [supporting-record webhook importer](./netsuite-supporting-record-webhook-importer.md). | worker2/Worker/Netsuite/CustomerAccountTypeBackfill.php, worker2/Worker/Netsuite/ItemFulfillableBackfill.php, worker2/Worker/Netsuite/OpportunityEndCustomerBackfill.php, worker2/Worker/Netsuite/Customer.php, library/app/model/forecast2/customer.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
35
35
|
| [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, worker/crons/toga2/forecast2/import_supporting_records.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker2/Worker/Netsuite/CustomerAccountTypeBackfill.php, library/app/api/netsuite/rest.php, library/app/model/forecast2/customer.php |
|
|
36
|
+
| [Toga TransferOrder → NetSuite Push (a $0 sales order IS the transfer order)](features/netsuite-transferorder-outbound-push.md) | A transfer order created in TOGa Supply is pushed to NetSuite as a **zero-value sales order**. | worker2/Worker/Netsuite/SalesOrder.php, _underscore/Model/Client/TransferOrder.php, dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql, test/@Bala/tests/netsuite_transferorder_payload_tests.php, test/@Bala/tests/transferorder_interceptor_tests.php |
|
|
36
37
|
| [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
|
|
37
38
|
| [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
|
|
38
39
|
| [NYCHH Asset-Tag Backfill (worker2)](features/nychh-asset-tag-backfill.md) | Keeps NYC Health & Hospitals (`Client_Nychh`) unit asset tags and MAC addresses synced from NetSuite. | worker2/Worker/Client/Nychh.php, worker2/Worker/Client/Nychh/AssetTagBackfill.php, worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php, worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-02
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/SalesOrder.php
|
|
@@ -14,6 +14,7 @@ files:
|
|
|
14
14
|
- worker/crons/toga2/prudential/transmissions_to_netsuite.php
|
|
15
15
|
related:
|
|
16
16
|
- ./netsuite-salesorder-open-orders-sync.md
|
|
17
|
+
- ./netsuite-transferorder-outbound-push.md
|
|
17
18
|
- ./creating-worker-actions.md
|
|
18
19
|
- ../../_underscore/features/netsuite-rest-client.md
|
|
19
20
|
- ../../api2/features/api-payload-interceptors.md
|
|
@@ -37,6 +38,24 @@ share no code, constants or helpers (verified 2026-08-10: the inbound half's 2,2
|
|
|
37
38
|
tokens are identical to `origin/_production`). **Edit one without assuming anything about the
|
|
38
39
|
other.**
|
|
39
40
|
|
|
41
|
+
**Since 2026-09-02 this same outbound half also pushes TRANSFER orders** (as $0 NetSuite sales
|
|
42
|
+
orders) through a separate entry point that reuses `buildNetSuiteOrder`, `doCreate`, the
|
|
43
|
+
`externalId` machinery and `writeBackNetSuiteId`. That route is its own subject — see
|
|
44
|
+
[Toga TransferOrder → NetSuite Push](./netsuite-transferorder-outbound-push.md) — and everything
|
|
45
|
+
below still describes the **sales-order** path only.
|
|
46
|
+
|
|
47
|
+
### The shared builder now serves two order types — the changes were additive and defaulted
|
|
48
|
+
|
|
49
|
+
Four helpers gained a transfer-order branch, each **defaulting to the old behaviour**:
|
|
50
|
+
`buildExternalId($uuid, $isTransferOrder = false)`,
|
|
51
|
+
`fetchSalesOrder($ctx, $uuid, $isTransferOrder = false)`, `writeBackNetSuiteId()` (route), and
|
|
52
|
+
`buildNetSuiteOrder()`, which now prefers an explicit `$order->otherRefNum` **before** the
|
|
53
|
+
ticket-number → service-request-number chain. **`CreateNetSuite` / `UpdateNetSuite` / `Sync` keep
|
|
54
|
+
their original signatures**, and the sales-order path is provably unchanged because
|
|
55
|
+
`_Model_Client_SalesOrder` declares **no `otherRefNum` field** — the new branch is unreachable from
|
|
56
|
+
a sales order. When you add a precedence like that, verify the *other* model does not have the field
|
|
57
|
+
rather than assuming it will not be set.
|
|
58
|
+
|
|
40
59
|
## Key files / entry points
|
|
41
60
|
|
|
42
61
|
| Action | Method | Purpose |
|
|
@@ -201,7 +220,7 @@ cleanup caveat is in the Elite doc; the general rule is in
|
|
|
201
220
|
`CreateNetSuite`/`UpdateNetSuite` as thin wrappers — which also closed a TOCTOU, since the decision
|
|
202
221
|
and the write now use the same fetched record.
|
|
203
222
|
|
|
204
|
-
## Test harness (85 tests,
|
|
223
|
+
## Test harness (85 tests — 84 pass, test 9 fails BY DESIGN)
|
|
205
224
|
|
|
206
225
|
`test/@Bala/tests/netsuite_salesorder_payload_tests.php` loads the **real** worker file with
|
|
207
226
|
stubbed dependencies and drives the private methods by **reflection**, so it tests shipped code
|
|
@@ -222,6 +241,20 @@ run, create, update, and the job-carried-snapshot path, plus:
|
|
|
222
241
|
Stub additions this needed: `$throwOnCreate` and `$createPayloadsSeen` on the NetSuite stub, and a
|
|
223
242
|
`$nextRows` queue on the `_Query` stub so `getClientContext()` resolves.
|
|
224
243
|
|
|
244
|
+
### ⚠ Test 9 is a PRE-EXISTING failure — do not attribute it to whatever you are working on
|
|
245
|
+
|
|
246
|
+
The suite reports **84 passed / 1 failed**. Test 9 asserts `otherRefNum` is the order number
|
|
247
|
+
(`SA100001`), from when that field was sourced from `SalesOrders.number`; the shipped code sources it
|
|
248
|
+
from the ticket number and then the service-request number. It is **left failing on purpose** — a
|
|
249
|
+
failing test is information, and "fixing" it would mean changing the Elite path, which the developer
|
|
250
|
+
ruled out.
|
|
251
|
+
|
|
252
|
+
**Technique worth keeping: to prove a test failure pre-dates your change, run the harness against a
|
|
253
|
+
`git show HEAD:<path>` copy** in a scratch directory (`git show HEAD:Worker/Netsuite/SalesOrder.php`
|
|
254
|
+
→ scratch file) — the harness hardcodes an absolute `require` path, so pointing it at the committed
|
|
255
|
+
copy is the cheap way to get a baseline. Done 2026-09-02: identical **84/1**, so the failure predates
|
|
256
|
+
the transfer-order work.
|
|
257
|
+
|
|
225
258
|
## Gotchas / known issues
|
|
226
259
|
|
|
227
260
|
- **⚠ A crash *after* the NetSuite create leaves an orphan.** `createRecord()` reads the `Location`
|
|
@@ -265,6 +298,17 @@ Stub additions this needed: `$throwOnCreate` and `$createPayloadsSeen` on the Ne
|
|
|
265
298
|
|
|
266
299
|
## Change history
|
|
267
300
|
|
|
301
|
+
- 2026-09-02 — This outbound half now also serves **transfer orders** (a $0 NetSuite sales order)
|
|
302
|
+
through a separate entry point — see
|
|
303
|
+
[Toga TransferOrder → NetSuite Push](./netsuite-transferorder-outbound-push.md). Four shared
|
|
304
|
+
helpers gained additive, defaulted transfer-order branches (`buildExternalId`, `fetchSalesOrder`,
|
|
305
|
+
`writeBackNetSuiteId` route, and an `$order->otherRefNum` precedence in `buildNetSuiteOrder`);
|
|
306
|
+
`CreateNetSuite` / `UpdateNetSuite` / `Sync` signatures are untouched and the sales-order path is
|
|
307
|
+
provably unchanged because `_Model_Client_SalesOrder` has no `otherRefNum` field. Also recorded
|
|
308
|
+
that harness **test 9 is a pre-existing failure** (asserts the old `otherRefNum = SalesOrders.number`
|
|
309
|
+
sourcing) — left failing deliberately — and the technique that proved it: run the harness against a
|
|
310
|
+
`git show HEAD:` copy of the worker for a baseline. (bala)
|
|
311
|
+
|
|
268
312
|
- 2026-08-20 — **Made the create idempotent with a NetSuite `externalId`**
|
|
269
313
|
(`NETSUITE_EXTERNAL_ID_PREFIX = 'toga-so-'`, `buildExternalId()`,
|
|
270
314
|
`findNetSuiteOrderByExternalId()`, and `writeBackNetSuiteId()` extracted because three paths now
|