toga-ai 1.0.288 → 1.0.290
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/component-model-namespace-registration.md +102 -0
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +25 -2
- package/knowledge/2.0/standards/backend-testing.md +49 -3
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/rate/INDEX.md +1 -1
- package/knowledge/clients/rate/features/whole-home-warranty-purchase-guard.md +16 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
| [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 |
|
|
9
9
|
| [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 |
|
|
10
10
|
| [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 | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
11
|
+
| [_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, api2/Component/Api/Netsuite/Netsuite.php |
|
|
11
12
|
| [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 |
|
|
12
13
|
| [Error Reporting — Issue/Event Aggregation (exceptionHandler)](features/error-reporting-issue-event.md) | `_underscore`'s global exception handler persists every uncaught exception into a two-table **Issue / Event** model in the **shared Core Logs DB** (`_underscore | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
13
14
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-08
|
|
10
|
+
owners: [dfranks]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Loader.php
|
|
13
|
+
- worker2/_.php
|
|
14
|
+
- api2/_.php
|
|
15
|
+
- worker2/Component/Forecast/Db/Db.php
|
|
16
|
+
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
17
|
+
- api2/Component/Api/Netsuite/Netsuite.php
|
|
18
|
+
related:
|
|
19
|
+
- ../architecture.md
|
|
20
|
+
- ./forecast-sale-import.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the
|
|
25
|
+
project namespace** at the top of the file:
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
namespace <NAMESPACE>; // worker2 → 'worker', api2 → 'api'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
where `<NAMESPACE>` is the value of that project's `_.php` `const NAMESPACE` (worker2 =
|
|
32
|
+
`'worker'`, api2 = `'api'`). The `namespace` declaration **IS the "registration"** — there
|
|
33
|
+
is no config toggle. If the class is declared in the **global** namespace instead, the
|
|
34
|
+
`_underscore` autoloader throws at **class-load (runtime)**:
|
|
35
|
+
|
|
36
|
+
> The namespace for '`<Class>`' located in '`<path>`' has not been defined which is
|
|
37
|
+
> prohibited for all _Model and _Component classes. Define the namespace for this class and
|
|
38
|
+
> try again.
|
|
39
|
+
|
|
40
|
+
**This is NOT caught by `php -l`** (parse-only) — it only surfaces at autoload/runtime, so
|
|
41
|
+
lint-clean code can still fatally throw on the first request that touches the class.
|
|
42
|
+
|
|
43
|
+
## How it works — `_underscore/Loader.php` (lines ~71-83)
|
|
44
|
+
For a project-local `_Component_*` / `_Model_*` class the autoloader:
|
|
45
|
+
1. `require`s the local file.
|
|
46
|
+
2. Checks whether the class **also** exists under the project namespace
|
|
47
|
+
(`<NAMESPACE>\<Class>`).
|
|
48
|
+
3. **If yes** → `eval`s a bridge class in the global namespace
|
|
49
|
+
(`class <Class> extends <NAMESPACE>\<Class> {}`, Loader.php:79-80) so callers can use the
|
|
50
|
+
bare `_Component_Foo` name.
|
|
51
|
+
4. **If no** (class was declared globally, so `<NAMESPACE>\<Class>` doesn't exist) → **throws**
|
|
52
|
+
the "namespace … has not been defined" error above.
|
|
53
|
+
|
|
54
|
+
**Correct precedent to copy:** `api2/Component/Api/Netsuite/Netsuite.php` —
|
|
55
|
+
`namespace api;` + `class _Component_Api_Netsuite extends \_Component` + `\_Config::netsuite(...)`,
|
|
56
|
+
`new \_ApiRequest(...)`.
|
|
57
|
+
|
|
58
|
+
### Framework references inside a namespaced file — what resolves vs. what must be backslash-qualified
|
|
59
|
+
Once the file is in `namespace <NAMESPACE>;`, **static calls and constants** to framework
|
|
60
|
+
classes resolve transparently via the autoloader's namespace fallback (Loader.php ~94-120),
|
|
61
|
+
bridged as `<NAMESPACE>\_X` → the framework class — e.g. `_Time::convert(...)`,
|
|
62
|
+
`_underscore::DB_FORECAST`, `_Component_Api_Netsuite::send(...)`, `new _Query(...)` all work
|
|
63
|
+
unqualified.
|
|
64
|
+
|
|
65
|
+
**Three constructs do NOT get that treatment and MUST be backslash-qualified:**
|
|
66
|
+
- `catch (\Exception $e)` / `catch (\RuntimeException $e)` — unqualified `catch (Exception $e)`
|
|
67
|
+
resolves to the non-existent `<NAMESPACE>\Exception` and silently no-matches (or fatals).
|
|
68
|
+
**This silent-swallow `catch` is the most dangerous / easiest-to-miss line** — the block
|
|
69
|
+
never catches, so error handling quietly does nothing.
|
|
70
|
+
- `new \Exception(...)` / `throw new \RuntimeException(...)` — same reason.
|
|
71
|
+
- `extends \_Component` (and `extends \_Model` etc.) — the base class in the class declaration
|
|
72
|
+
must be backslash-qualified.
|
|
73
|
+
|
|
74
|
+
## Why this matters — the Forecast SALES drift root cause
|
|
75
|
+
This mechanism was the root cause of the Forecast **SALES** drift observed after a worker2
|
|
76
|
+
push: `_Component_Forecast_SaleImport` and `_Component_Forecast_Db` had been declared in the
|
|
77
|
+
**global** namespace, so every NetSuite sales/invoice/opportunity webhook **fatally threw at
|
|
78
|
+
autoload before writing to `Forecast.Sales`**. Because NetSuite replays edits across old
|
|
79
|
+
transactions, the symptom **presented as ~7 months of historical drift** rather than as a
|
|
80
|
+
"new code broken" failure — a misleading symptom that this mechanism explains.
|
|
81
|
+
|
|
82
|
+
(The engine ultimately lives under `_underscore` — see the
|
|
83
|
+
[Forecast.Sales import engine doc](./forecast-sale-import.md) — precisely because a
|
|
84
|
+
`_Component_*` class loaded from a project path is rejected; but even a legitimately
|
|
85
|
+
project-local `_Component_*`/`_Model_*` must still declare the project namespace.)
|
|
86
|
+
|
|
87
|
+
## Gotchas / known issues
|
|
88
|
+
- `php -l` will **not** catch a missing/global namespace or an unqualified `catch`/`throw`/
|
|
89
|
+
`extends` — boot the class (autoload it) to verify. A wrong autoload path is likewise a
|
|
90
|
+
runtime-only failure.
|
|
91
|
+
- A global-namespace `_Component_*`/`_Model_*` fails **loudly** (the throw above). An
|
|
92
|
+
unqualified `catch (Exception $e)` fails **silently** — the more insidious of the two.
|
|
93
|
+
|
|
94
|
+
## Change history
|
|
95
|
+
- 2026-07-08 — Documented the `_Component_*`/`_Model_*` project-namespace registration
|
|
96
|
+
requirement (Loader.php eval-bridge mechanism, ~71-83), the runtime-only failure mode (not
|
|
97
|
+
caught by `php -l`), and the three constructs that must be backslash-qualified inside a
|
|
98
|
+
namespaced file (`catch`/`throw`/`new \Exception`, `extends \_Component`). Root-caused the
|
|
99
|
+
Forecast SALES drift to `_Component_Forecast_SaleImport`/`_Component_Forecast_Db` declared
|
|
100
|
+
in the global namespace (webhooks threw at autoload before writing; presented as 7-month
|
|
101
|
+
historical drift because NetSuite replays edits). Correct precedent:
|
|
102
|
+
`api2/Component/Api/Netsuite/Netsuite.php`. (dfranks)
|
|
@@ -38,6 +38,7 @@ related:
|
|
|
38
38
|
- ../../worker2/features/netsuite-salesorder-open-orders-sync.md
|
|
39
39
|
- ../../worker2/features/netsuite-supporting-record-webhook-importer.md
|
|
40
40
|
- ./netsuite-rest-client.md
|
|
41
|
+
- ./component-model-namespace-registration.md
|
|
41
42
|
---
|
|
42
43
|
|
|
43
44
|
## Summary
|
|
@@ -135,7 +136,14 @@ This is the **opposite of the legacy cron**, which reads the pre-signed listSale
|
|
|
135
136
|
(already `-foreignamount`) and so applies the factor **only** to the synthetic shipping
|
|
136
137
|
line. **Do not copy the cron's sign handling into the webhook** — it reintroduces the
|
|
137
138
|
~$470K credit-memo sign bug (credits stored positive). The cron is not authoritative for
|
|
138
|
-
the raw-REST path.
|
|
139
|
+
the raw-REST path.
|
|
140
|
+
|
|
141
|
+
> **Do not confuse this line-amount/revenue sign with the `amountDue` "store raw positive"
|
|
142
|
+
> decision (TRUE-78923).** The "store raw positive / don't sign-flip credit memos & refunds"
|
|
143
|
+
> guidance applies **only to `amountDue`** (which this importer does not write) — it does
|
|
144
|
+
> **NOT** apply to the line `amount`/revenue sign. For revenue, credit memos + cash refunds
|
|
145
|
+
> **must** land NEGATIVE and invoices + cash sales POSITIVE in `Forecast.Sales`; that is the
|
|
146
|
+
> stored contract. Mis-carrying the amountDue rule onto revenue would store credits positive. Confirmed end-to-end against the local Forecast mirror (creditMemo
|
|
139
147
|
revenue stored -350..-600; cashRefund revenue -885 / profit -78; invoice positive).
|
|
140
148
|
|
|
141
149
|
> A reviewer may flag "profit double-signs the factor" — false positive:
|
|
@@ -481,10 +489,25 @@ record is deleted in NetSuite.)
|
|
|
481
489
|
(e.g. `./` under worker2) with "namespace … has not been defined" — at **class-load
|
|
482
490
|
(runtime), not `php -l`**. So this engine lives in `_underscore` next to
|
|
483
491
|
`_Component_Forecast_Db` even though only worker2 uses it; it references the worker2 class
|
|
484
|
-
`_Worker_Netsuite_Item`, resolved lazily at runtime in the worker2 context.
|
|
492
|
+
`_Worker_Netsuite_Item`, resolved lazily at runtime in the worker2 context. **A
|
|
493
|
+
legitimately project-local `_Component_*`/`_Model_*` (e.g. the worker2 copies of these
|
|
494
|
+
classes) must still declare `namespace worker;` or it throws the same error at autoload —
|
|
495
|
+
and this exact global-namespace mistake caused the SALES drift (webhooks fatally threw
|
|
496
|
+
before writing, presenting as 7-month historical drift). See the
|
|
497
|
+
[namespace-registration doc](./component-model-namespace-registration.md) for the
|
|
498
|
+
eval-bridge mechanism and the `catch`/`throw`/`extends` backslash-qualify traps.**
|
|
485
499
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
486
500
|
|
|
487
501
|
## Change history
|
|
502
|
+
- 2026-07-08 — **Clarified the revenue sign convention is NOT the `amountDue` "store raw
|
|
503
|
+
positive" rule.** The TRUE-78923 "don't sign-flip credit memos/refunds" guidance applies
|
|
504
|
+
only to `amountDue` (which this importer does not write); the line `amount`/revenue sign
|
|
505
|
+
contract is unchanged — credit memos + cash refunds NEGATIVE, invoices + cash sales
|
|
506
|
+
POSITIVE. Also cross-linked the new
|
|
507
|
+
[`_Component_*`/`_Model_*` namespace-registration doc](./component-model-namespace-registration.md):
|
|
508
|
+
a global-namespace declaration of `_Component_Forecast_SaleImport`/`_Component_Forecast_Db`
|
|
509
|
+
was the root cause of the SALES drift (webhooks threw at autoload before writing; presented
|
|
510
|
+
as 7-month historical drift because NetSuite replays edits). (dfranks)
|
|
488
511
|
- 2026-07-07 — **Documented the AMQ enqueuer's line-field inline-edit blind spot** (Forecast2 SALES
|
|
489
512
|
profit-drift forensics; invoice 6715127 / 2026-02-20 / $3,392.70). Editing a **sublist LINE
|
|
490
513
|
field** (e.g. cost `TRANLINE.MCOSTESTIMATE`) inline in the NetSuite UI raises **no** record UE
|
|
@@ -5,9 +5,12 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-07-
|
|
8
|
+
updated: 2026-07-08
|
|
9
9
|
owners: [mhammontree]
|
|
10
|
-
files:
|
|
10
|
+
files:
|
|
11
|
+
- _underscore/Query.php
|
|
12
|
+
- _underscore/Database.php
|
|
13
|
+
- _underscore/Environment.php
|
|
11
14
|
related:
|
|
12
15
|
- ../apps/_underscore/architecture.md
|
|
13
16
|
- ../apps/_underscore/features/per-client-database-connections.md
|
|
@@ -27,12 +30,22 @@ From a 2.0 app root (e.g. `worker2`): `chdir` into the app, require Composer aut
|
|
|
27
30
|
`_underscore.php`, and set `ENVIRONMENT` before bootstrap:
|
|
28
31
|
|
|
29
32
|
```php
|
|
33
|
+
putenv('ENVIRONMENT=dev-markhammontree-laptop'); // REQUIRED, before requiring _underscore.php
|
|
34
|
+
// e.g. dev-<yourname>-laptop — matches your Config/<ENVIRONMENT>.ini
|
|
30
35
|
chdir('/path/to/worker2');
|
|
31
36
|
require 'vendor/autoload.php';
|
|
32
37
|
require '_underscore.php'; // boots Loader/Config/etc.
|
|
33
|
-
// ENVIRONMENT must be set (env var / server) so _Config picks the right INI
|
|
34
38
|
```
|
|
35
39
|
|
|
40
|
+
- **`ENVIRONMENT` is mandatory.** The bootstrap uses it to select `worker2/Config/<ENVIRONMENT>.ini`.
|
|
41
|
+
If it is unset, `_underscore.php` throws *"The environment variable 'ENVIRONMENT' is required and
|
|
42
|
+
has not been set"* (`_underscore/Environment.php:12`). Set it via `putenv(...)` in the script
|
|
43
|
+
before the `require`, or export it in the shell.
|
|
44
|
+
- **On Windows the shell is PowerShell** — set env vars as `$env:VAR='x'; php script.php`, **not**
|
|
45
|
+
the bash `VAR=x php script.php` prefix. The bash-style prefix silently does nothing in
|
|
46
|
+
PowerShell, so the script falls back to its defaults (a confusing "it ran but used the wrong
|
|
47
|
+
config" failure).
|
|
48
|
+
|
|
36
49
|
## You MUST pin the client DB explicitly
|
|
37
50
|
|
|
38
51
|
`_underscore::DB_CLIENT` is a **logical alias**, not a schema name — there is no database literally
|
|
@@ -51,8 +64,41 @@ Without this, any `Model/Client/*` query resolves the unmapped alias and fails.
|
|
|
51
64
|
schema you have locally/on the target env (internal testing uses `Client_True` = TOGA Technology).
|
|
52
65
|
Remember the per-client **logs** connection trap too (see the per-client-database-connections doc).
|
|
53
66
|
|
|
67
|
+
## Commit before you read your own writes (lazy-transaction trap)
|
|
68
|
+
|
|
69
|
+
Standalone scripts run under `_underscore`'s **lazy-transaction model with a separate read
|
|
70
|
+
connection**, so a write is invisible to a later read — and to the code under test — until it is
|
|
71
|
+
committed. Concretely:
|
|
72
|
+
|
|
73
|
+
- `_Database::register(..., alias: _underscore::DB_CLIENT)` sets up a pending transaction for that
|
|
74
|
+
DB. The **first** write (via `_Query`) flips autocommit **OFF** and opens the transaction
|
|
75
|
+
(`_underscore/Query.php` ~303–304, keyed by the database value passed to `_Query`).
|
|
76
|
+
- INSERTs therefore sit **uncommitted**. A subsequent `SELECT` on the read connection returns
|
|
77
|
+
nothing, so a read-back-by-marker returns `null` and dependent FK inserts fail (classic symptom:
|
|
78
|
+
`saleItemId = 0` FK violation).
|
|
79
|
+
|
|
80
|
+
**Pattern:** after seeding base rows, call
|
|
81
|
+
`_Database::transactionCommit(_underscore::DB_CLIENT)` (`_underscore/Database.php` ~165–212). This
|
|
82
|
+
commits **and** restores `autocommit(true)`, so all later writes — and reads of them — behave
|
|
83
|
+
normally. Order your script:
|
|
84
|
+
|
|
85
|
+
1. seed base rows →
|
|
86
|
+
2. **commit** →
|
|
87
|
+
3. read ids back →
|
|
88
|
+
4. insert dependent rows →
|
|
89
|
+
5. assert →
|
|
90
|
+
6. cleanup — and **commit the cleanup too**, so the `DELETE`s persist rather than being rolled
|
|
91
|
+
back when the script exits.
|
|
92
|
+
|
|
93
|
+
This is the same lazy-transaction write-drop family noted in the `_underscore` architecture doc;
|
|
94
|
+
it bites the standalone-script/verification case specifically. Demonstrated working in
|
|
95
|
+
`test/@Mark/Rate/verify_wholehome_per_address_guard.php`.
|
|
96
|
+
|
|
54
97
|
## Rules
|
|
55
98
|
|
|
56
99
|
- Do not add or assume a PHPUnit suite for 2.0 backend work; write a `test`-repo script instead.
|
|
100
|
+
- Always set `ENVIRONMENT` before bootstrap (PowerShell: `$env:ENVIRONMENT='...'`, or `putenv`).
|
|
57
101
|
- Always pin the concrete `Client_<Id>` schema to `DB_CLIENT` (and logs/archive if the code logs).
|
|
102
|
+
- **Commit seeded rows before you read them back** — the read connection cannot see uncommitted
|
|
103
|
+
writes; commit cleanup too so it persists.
|
|
58
104
|
- Keep scripts in your per-dev `test` folder; never point them at production databases.
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 26 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
| [Rate SAML SSO](features/saml-sso.md) | 2.0 | Rate uses Azure AD as its IdP (`login.rate.com`). | _underscore/Model/Rate/ClientAuthentication.php, saml/Controller/Index.php, toga2-view/src/hooks/useAuthenticationFlow.ts |
|
|
9
9
|
| [Service Card Entitlement Display](features/service-card-entitlements.md) | 2.0 | Rate's home and services pages display one service card per purchased entitlement. | src/components/ServiceCard/ServiceCard.tsx, src/components/ServiceCard/index.ts, src/hooks/useBundleServices.ts, src/pages/Home/api/homeApi.ts, src/pages/Home/view/HomePage.tsx, src/pages/Home/viewModels/useHomePageViewModel.ts, src/pages/Services/view/ServicesPage.tsx, src/pages/Services/viewModels/useServicePageViewModel.ts |
|
|
10
10
|
| [Rate Service-Purchase Confirmation Emails (Tech / Warranty)](features/service-purchase-emails.md) | 2.0 | When a Rate customer purchases a service, a confirmation email is sent. | _underscore/Model/Rate/Entitlement.php, worker2/Worker/Notification/EmailTemplate.php, dbchanges2/Client_Rate/2026-06-30a - Rate purchase email templates.sql |
|
|
11
|
-
| [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | A customer may hold **one active Whole Home Warranty (WH) per validated address, globally** (across all borrowers). | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql |
|
|
11
|
+
| [Rate Whole Home Warranty Per-Address Purchase Guard](features/whole-home-warranty-purchase-guard.md) | 2.0 | A customer may hold **one active Whole Home Warranty (WH) per validated address, globally** (across all borrowers). | _underscore/Model/Rate/Entitlement.php, _underscore/Model/Client/Address.php, dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql, test/@Mark/Rate/verify_wholehome_per_address_guard.php |
|
|
12
12
|
| [Rate](profile.md) | 2.0 | Rate is a mortgage/lending client. | |
|
|
@@ -6,12 +6,13 @@ project: _Underscore
|
|
|
6
6
|
client: rate
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-08
|
|
10
10
|
owners: [mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Rate/Entitlement.php
|
|
13
13
|
- _underscore/Model/Client/Address.php
|
|
14
14
|
- dbchanges2/Client_Rate/2026-07-07a - WholeHomeWarrantyPerAddressGuard.sql
|
|
15
|
+
- test/@Mark/Rate/verify_wholehome_per_address_guard.php
|
|
15
16
|
related:
|
|
16
17
|
- clients/rate/profile.md
|
|
17
18
|
- clients/rate/features/aig-contract-creation.md
|
|
@@ -113,6 +114,17 @@ suggested-vs-entered when `success && address1`, and a "couldn't verify" toaster
|
|
|
113
114
|
[address-validation feature](../../../2.0/apps/_underscore/features/address-validation.md) for the
|
|
114
115
|
endpoint mechanics.
|
|
115
116
|
|
|
117
|
+
## Verification
|
|
118
|
+
|
|
119
|
+
A self-contained backend verification script lives at
|
|
120
|
+
`test/@Mark/Rate/verify_wholehome_per_address_guard.php` (run via PHP CLI with `ENVIRONMENT`
|
|
121
|
+
set, pinned to the real migrated **`Client_Rate`** schema — see the
|
|
122
|
+
[2.0 backend-testing standard](../../../2.0/standards/backend-testing.md)). It applies
|
|
123
|
+
`c_serviceAddressId` idempotently, seeds a token-tagged scratch WH graph (2 addresses / 2 items /
|
|
124
|
+
2 active subscriptions / 2 entitlements), then exercises the pure helpers, the dedup matrix (via
|
|
125
|
+
reflection on the private methods), and the `prePost` branches, cleaning up in a `finally`. The
|
|
126
|
+
live carrier waterfall is opt-in via `RUN_LIVE=1` (defaults off). Verified **18/18 pass**.
|
|
127
|
+
|
|
116
128
|
## Gotchas / known issues
|
|
117
129
|
|
|
118
130
|
- **TOCTOU on the uniqueness check (accepted).** The check-then-persist dedup is **not**
|
|
@@ -131,6 +143,9 @@ endpoint mechanics.
|
|
|
131
143
|
|
|
132
144
|
## Change history
|
|
133
145
|
|
|
146
|
+
- 2026-07-08 — Added a backend verification script
|
|
147
|
+
(`test/@Mark/Rate/verify_wholehome_per_address_guard.php`, 18/18 pass against the migrated
|
|
148
|
+
`Client_Rate` schema) and a Verification pointer. (mhammontree)
|
|
134
149
|
- 2026-07-07 — Built the WH per-address purchase guard (TRUE-79533): `prePost` on
|
|
135
150
|
`_Model_Rate_Entitlement` hard-blocks invalid addresses and second active WH at the same
|
|
136
151
|
normalized address (global), adopts the carrier-normalized address onto the payload, and
|
package/package.json
CHANGED