toga-ai 1.0.569 → 1.0.571

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Library (1.0 Framework) Architecture](architecture.md) | `library` is the shared library repository for **all 1.0 (legacy) applications** — the `App_` framework. | library/_.php, library/app/, library/browser/ |
6
+ | [Address Validation Gateway (App_Api_OfficeDepot::validateAddress + USPS fallback)](features/address-validation-gateway.md) | `App_Api_OfficeDepot::validateAddress()` is the shared 1.0 (`App_`) address-validation gateway. | library/app/api/officedepot.php, library/app/api/usps.php |
6
7
  | [Where a new App_ class goes — the app/ folder IS a behavioral contract](features/app-class-placement-base-contracts.md) | In `library/app/`, choosing a folder is **not** a filing decision — the autoloader maps `App_<Folder>_<File>` to `app/<folder>/<file>.php`, and each folder's ba | library/app/model.php, library/app/client.php, library/app/api.php, library/app/api/servicerequest.php, library/app/api/volt.php, library/app/api/carrier/fedex.php |
7
8
  | [App_Sso — Reusable 1.0 SSO Initiation (SP-initiated SAML via saml.togahub.com)](features/app-sso-initiation.md) | `App_Sso` (`library/app/sso.php`) is the **1.0 port of the 2.0 SAML gateway's SP-initiated SSO initiation**, packaged as a reusable, framework-level capability | library/app/sso.php, library/sso/togahub_private_key.key |
8
9
  | [Cron Execution Monitoring (App_Framework check-in/out → CronJobExecutions)](features/cron-execution-monitoring.md) | `App_Framework::cronInitialization()` / `App_Framework::cronFinished()` (in `library/app/framework.php`) give every 1.0 (`App_`) cron job a check-in/check-out l | library/app/framework.php |
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Address Validation Gateway (App_Api_OfficeDepot::validateAddress + USPS fallback)
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-13
10
+ owners: [jcardinal]
11
+ files:
12
+ - library/app/api/officedepot.php
13
+ - library/app/api/usps.php
14
+ related:
15
+ - ../../toga/features/odp-customer-search-loyalty-persistence.md
16
+ ---
17
+
18
+ ## Summary
19
+ `App_Api_OfficeDepot::validateAddress()` is the shared 1.0 (`App_`) address-validation gateway.
20
+ It is the entry point every TOGa 1.0 flow calls (via classic AJAX
21
+ `ajaxAction('App_Api_OfficeDepot','validateAddress')`) to standardize/verify a typed address
22
+ before it is saved. It wraps ODP's address-validation service and falls back to
23
+ `App_Api_USPS::validateAddress()` (USPS REST API v3, OAuth `client_credentials`, in
24
+ `library/app/api/usps.php`). It returns a standardized address (corrected city/casing/zip) that
25
+ callers are expected to write back before saving.
26
+
27
+ ## Key files / entry points
28
+ - `library/app/api/officedepot.php` — `App_Api_OfficeDepot::validateAddress()`, the gateway.
29
+ - `library/app/api/usps.php` — `App_Api_USPS::validateAddress()`, the fallback carrier.
30
+
31
+ ## How it works
32
+ 1. **Endpoint selection.** The gateway calls the ODP **sandbox** endpoint when
33
+ `App_Registry::inTestMode()` is true (`inTestMode` = `config['internal']['test_mode'] > 0`),
34
+ and the **live** ODP endpoint otherwise. Production is effectively hard-pinned to live
35
+ (comment "2024-01-22 JC - Always using production...").
36
+ 2. **On a real ODP answer** (HTTP 200, whether it accepts or rejects the address) the gateway
37
+ returns ODP's standardized result.
38
+ 3. **On any non-200 ODP response** (sandbox down/expired, etc.) it falls back to
39
+ `App_Api_USPS::validateAddress()` and returns the USPS result — a genuine standardization
40
+ **or** a genuine bad-address rejection both count as a real answer and are returned.
41
+ 4. **Only when BOTH ODP and USPS are unavailable** does it return `array('success' => true)`
42
+ with no standardized fields (fail-open, so associates are never hard-blocked by an outage).
43
+
44
+ ## Gotchas / known issues
45
+ - **Beta-bypass class of bug (fixed 2026-08-13).** Previously the ODP-failure `else` branch
46
+ returned `array('success' => true)` with **no** standardized address fields and the USPS
47
+ fallback was commented out. Beta/test uses the ODP **sandbox**; when the sandbox was
48
+ down/expired, callers saw `success:true` and saved the **raw typed address** (wrong city,
49
+ wrong casing) with no validation. Production was unaffected because it hits the working live
50
+ ODP endpoint. The fix restores the USPS fallback so a validation only truly fails open when
51
+ **both** carriers are down.
52
+ - **"Both unavailable" must check BOTH USPS service-down strings.** USPS signals unavailability
53
+ two ways: `'Address validation service is temporarily unavailable'` **and**
54
+ `'USPS OAuth authentication failed'`. The fail-open condition must match either. Checking only
55
+ the first string wrongly blocks associates when ODP is down *and* USPS OAuth also fails
56
+ (a php-reviewer pass caught this).
57
+ - **Front-end write-back anti-pattern (widespread, mostly UNFIXED).** Many 1.0 front-end
58
+ callbacks use the gateway response only as a pass/fail **gate**, then rebuild the save payload
59
+ from the raw DOM inputs — discarding the standardized `address1`/`city`/`state`/`zipCode` the
60
+ gateway returned. So even a successful validation never persists the corrected address. Known
61
+ flows with this anti-pattern still present:
62
+ `toga/app/togarefresh2026/servicerequests/finalizequote.php`,
63
+ `toga/app/togarefresh2026/appointment/select.php`,
64
+ `toga/app/servicerequests/builder.php`, `toga/app/loaners/checkout.php`,
65
+ `toga/app/jobjackets/view.php`, and the `togarefresh2023` variants. The 2026-08-13 gateway fix
66
+ protects all of them from the beta bypass, but the write-back was only corrected in the
67
+ `togarefresh2026` customersearch flow (see related doc). When touching any of these, write the
68
+ standardized address back into the inputs before building the save payload.
69
+
70
+ ## Related docs
71
+ - `../../toga/features/odp-customer-search-loyalty-persistence.md` — the customersearch save
72
+ flow, where the write-back fix was applied.
73
+
74
+ ## Change history
75
+ - 2026-08-13 — Fix: restored the USPS fallback in `App_Api_OfficeDepot::validateAddress` so the
76
+ beta/sandbox path no longer fakes `success:true` and saves unvalidated addresses; fail-open now
77
+ requires BOTH ODP and USPS down, checking both USPS service-down strings. (jcardinal)
@@ -6,13 +6,14 @@ project: TOGa
6
6
  client: office-depot
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-10
9
+ updated: 2026-08-13
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - toga/app/togarefresh2026/customersearch/view.php
13
13
  - library/app/model/customer.php
14
14
  related:
15
15
  - ./bundleconfirmation-cart-preservation.md
16
+ - ../../library/features/address-validation-gateway.md
16
17
  ---
17
18
 
18
19
  ## Summary
@@ -47,6 +48,18 @@ field.
47
48
  ## Data model
48
49
  - `Customers.merchantId` (TOGA_ODP legacy DB) — stores the ODP loyalty/rewards `memberId`.
49
50
 
51
+ ## Standardized-address write-back (Next button flow)
52
+ The "Existing PC Services" flow — `content=togarefresh2026_customersearch`, Next button
53
+ (`#nextSubmit`) → `validateAddress()` → JS callback `validateAddressResponse(resp)` — used the
54
+ gateway response only as a pass/fail gate, then rebuilt the save `formData` from the raw DOM
55
+ inputs and called `App_Model_Customer::saveNewCustomer` — discarding `resp['address1']`/`city`/
56
+ `state`/`zipCode`, so the validated casing/city/zip never persisted. Fixed 2026-08-13: before
57
+ building `formData`, the callback now writes the standardized address from `resp` back into the
58
+ `streetAddress`/`line2`/`city`/`state`/`zip` inputs (guarded on `resp['address1']` present), so
59
+ the corrected address is what gets saved. The gateway itself lives in `library` — see
60
+ `../../library/features/address-validation-gateway.md`, which lists the many other 1.0 flows
61
+ that still have this write-back anti-pattern.
62
+
50
63
  ## Gotchas / known issues
51
64
  - **`saveNewCustomer` has two callers.** `togarefresh2026` now sends `merchantId`;
52
65
  `togarefresh2023` does **not**. The `!empty()` guard is what keeps the 2023 flow safe — it
@@ -64,6 +77,9 @@ field.
64
77
  - `./bundleconfirmation-cart-preservation.md`
65
78
 
66
79
  ## Change history
80
+ - 2026-08-13 — Fix: `validateAddressResponse` now writes the standardized address from the
81
+ validation gateway back into the form inputs before saving, so validated city/casing/zip
82
+ persist (previously the response was used only as a pass/fail gate). (jcardinal)
67
83
  - 2026-08-10 — Fix: ODP loyalty number now persists to Customers.merchantId across the
68
84
  togarefresh2026 customer-search save flow (front end + saveNewCustomer), guarded by !empty().
69
85
  Flagged a pre-existing SQL-injection risk in the same method for follow-up. (jcardinal)
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 17 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 18 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 23 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)
@@ -94,6 +94,31 @@ The 850 carries the identifiers the 855 echoes back:
94
94
  full 850 → TOGa identifier map, incl. `BEG` = ODP PO number and `REF*QC` = the value to
95
95
  search on for the ODP SalesOrder.)
96
96
 
97
+ ## Rejections raise a business alert (Logs.Issue)
98
+ A reject used to be **silent** — the PO was skipped and no human was notified. Cron `3a` now
99
+ surfaces every over-quantity/duplicate rejection as a curated **Logs.Issue** alert by throwing
100
+ `App_Exception_Business` (issue key **`COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY`**,
101
+ `URGENCY_HIGH`, message listing the rejected PO numbers). The reject 855 is still built and
102
+ acknowledged back to Office Depot first; the alert is purely additional notification. The issue
103
+ key follows the sibling cron `1_transmit_compass_sales_orders_to_mits.php` naming convention.
104
+
105
+ ### ⚠ Gotcha — the throw is DEFERRED to end-of-run, never inline
106
+ The exception is **not** thrown in the reject branch. It is deferred to the very end of the run:
107
+ after the full `$ediFiles['Contents']` loop, after each S3 850 file is logged + deleted, and
108
+ after `App_Framework::cronFinished()`. A single collector array
109
+ (`$rejectedPurchaseOrderNumbers`) accumulates rejected PO numbers across all files/POs, and one
110
+ exception is thrown at end-of-run.
111
+
112
+ **Why it must be deferred:** an inline throw unwinds the loop *before* the per-file S3 delete, so
113
+ the same 850 is re-fetched next run and a **duplicate reject 855 is re-sent to Office Depot every
114
+ run — an "855 storm."** Deferring guarantees each 850 is deleted first, and the stable issue key
115
+ collapses multiple rejects into one Logs.Issue.
116
+
117
+ Throwing **after** `cronFinished()` is safe: the overlap guard
118
+ (`App_Framework::exitIfProcessRunning`, a `ps -ef` process-table scan) has no dependency on
119
+ cronFinished's `CronJobExecutions` checkout row, and on CLI an uncaught `App_Exception_Business`
120
+ just echoes `ERROR [ref]:` and exits.
121
+
97
122
  ## ⚠ Open item — reject codes vs ODP's 855 companion guide
98
123
  The reject codes used (`BAK02 = RD`, `ACK01 = IR`) are the **standards-correct** X12 values.
99
124
  They have **not** yet been confirmed against Office Depot's 855 companion guide. If ODP
@@ -102,6 +127,13 @@ A legacy, abandoned sketch (`compass/edi/3_send_edi_855(reject).php`) used **non
102
127
  codes `BAK01 = A1` / `BAK02 = RE` — that path was **deliberately NOT followed**.
103
128
 
104
129
  ## Change history
130
+ - 2026-08-13 — Rejections now raise a business alert: cron `3a` throws
131
+ `App_Exception_Business` (`COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY`,
132
+ `URGENCY_HIGH`) so every over-quantity/duplicate reject surfaces as a Logs.Issue instead of
133
+ being silent. The throw is **deferred to end-of-run** (after the file loop, per-file S3
134
+ delete, and `cronFinished()`) via a `$rejectedPurchaseOrderNumbers` collector — an inline
135
+ throw would skip the S3 delete and cause an "855 storm" (same 850 re-fetched, duplicate reject
136
+ 855 re-sent every run). `php -l` clean; php-reviewer 0/0. (jcardinal)
105
137
  - 2026-08-13 — Built the over-quantity guard on the ODP 850 importer (cron 3a): ODP
106
138
  re-sending an already-processed Compass SO line under a new PO number now rejects the whole
107
139
  PO (new helper `officeDepotPurchaseOrderExceedsCompassDemand()`, "first occurrence passes",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.569",
3
+ "version": "1.0.571",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",