toga-ai 1.0.497 → 1.0.498
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/features/error-reporting-issue-event.md +29 -1
- package/knowledge/2.0/apps/api2/architecture.md +8 -2
- package/knowledge/2.0/apps/api2/features/request-logging.md +21 -1
- package/knowledge/2.0/standards/backend-php.md +38 -11
- package/knowledge/clients/compass-usa/features/mits-po-to-so-item-linking.md +82 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: ["dfranks", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Error.php
|
|
@@ -87,6 +87,28 @@ produced a different hash on every request — dedup never actually worked.
|
|
|
87
87
|
cannot orphan a curated Issue: an admin repoints the new fingerprint at the existing Issue
|
|
88
88
|
from the Tools console to merge them.
|
|
89
89
|
|
|
90
|
+
### Where each exception class routes (the class you throw IS the routing decision)
|
|
91
|
+
|
|
92
|
+
| Thrown class | HTTP | Sentry | `Logs.Issue` | Fingerprint / routing |
|
|
93
|
+
|---|---|---|---|---|
|
|
94
|
+
| `_Exception_Validation` | **400** | no | **no** | n/a — bad client input. Still leaves a full `Logs.Api` row (see [api2 request logging](../../api2/features/request-logging.md)) |
|
|
95
|
+
| `_Exception_Business` | **500** | yes | **yes** | stable `issueKey` (refactor-immune) → business recipients configured in the Tools console |
|
|
96
|
+
| plain global `Exception` | **500** | yes | **yes** | **trace**-based fingerprint (moves when code moves) → developer ClickUp task |
|
|
97
|
+
| `_Exception` | — | — | — | **never throw it deliberately** (below) |
|
|
98
|
+
|
|
99
|
+
Verified against `api2/Controller/Index.php` (≈L205-245, L369-377) and `_Error.php`
|
|
100
|
+
(`captureException` ≈L222-265, `buildFingerprint` ≈L455-490) on **2026-08-03**, while TRUE-78188
|
|
101
|
+
was still in flight — **re-verify against `Controller/Index.php`** rather than trusting the table
|
|
102
|
+
blindly.
|
|
103
|
+
|
|
104
|
+
**`_Exception` must never appear at a deliberate throw site.** Its constructor takes **five**
|
|
105
|
+
required args (`errorNumber`, `errorString`, `errorFile`, `errorLine`, `trace`) because it is an
|
|
106
|
+
*error-handler shape* populated by `_Error` from `set_error_handler` context — not a general
|
|
107
|
+
exception. `throw new _Exception('msg')` raises an `ArgumentCountError` **before** the intended
|
|
108
|
+
exception is ever constructed, so the real signal is destroyed. Jeff states this rationale in the
|
|
109
|
+
`Exception/Business.php` docblock. Census in `_underscore`: 173 plain `throw new Exception` vs 14
|
|
110
|
+
`_Exception`.
|
|
111
|
+
|
|
90
112
|
### `_Exception_Business` — identity immune to refactoring
|
|
91
113
|
|
|
92
114
|
`_underscore/Exception/Business.php` carries a stable `issueKey` plus a `minimumUrgency` floor.
|
|
@@ -267,6 +289,12 @@ clientUserId). **Neither was built.** As built instead:
|
|
|
267
289
|
|
|
268
290
|
## Change history
|
|
269
291
|
|
|
292
|
+
- 2026-08-03 — Added the **exception-class routing table** (`_Exception_Validation` → 400/no
|
|
293
|
+
Issue; `_Exception_Business` → 500 + `issueKey`-fingerprinted Issue to business recipients;
|
|
294
|
+
plain `Exception` → 500 + trace-fingerprinted Issue to a developer task) and recorded that
|
|
295
|
+
**`_Exception` must never be thrown deliberately** — its 5-arg error-handler constructor raises
|
|
296
|
+
`ArgumentCountError` first. Verified against `Controller/Index.php` / `_Error.php` while
|
|
297
|
+
TRUE-78188 was still in flight. (dfranks)
|
|
270
298
|
- 2026-08-01 — **Decided: `Logs` cluster tables use SINGULAR names.** Renamed all six
|
|
271
299
|
post-deployment (`Issues`→`Issue`, `Events`→`Event`, `IssueFingerprints`→`IssueFingerprint`,
|
|
272
300
|
`IssueClickupTasks`→`IssueClickupTask`, `IssueEmailAddresses`→`IssueEmailAddress`,
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, bala, mhammontree]
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: [jcardinal, bala, mhammontree, dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Controller/Index.php
|
|
13
13
|
- api2/Component/Api/V2/V2.php
|
|
@@ -102,6 +102,12 @@ One ~2,000-line `execute()` then `processRoutePairs()`:
|
|
|
102
102
|
`public`, `delegator`/`encrypted`. JWTs HS256, `sub` access/refresh (access 3600s,
|
|
103
103
|
refresh 30d); signing secret **rotated** with current+previous accepted across the
|
|
104
104
|
boundary. The JWT `id` claim carries the full identity used for ACL.
|
|
105
|
+
**`/v2/auth/oauth` credential transport:** OAuth 2.0 `client_credentials`. `client_id` +
|
|
106
|
+
`client_secret` may arrive as **HTTP Basic** *or* as POST body fields — **Basic takes
|
|
107
|
+
priority** when both are present. `scope` is **required**; `grant_type` is **optional** and
|
|
108
|
+
defaults to `client_credentials` (accepted: `client_credentials`, `password`,
|
|
109
|
+
`refresh_token`). The call mints a JWT; subsequent requests carry `Authorization: Bearer`.
|
|
110
|
+
(`api2/Component/Api/V2/V2.php` L414-460.)
|
|
105
111
|
4. **Response envelope** — `{transactionId, timestamp, authority, audience, isSuccess,
|
|
106
112
|
status, error, messages[], meta{}, data{}}`; `isSuccess` = 2xx status.
|
|
107
113
|
5. **Transaction logging** — every request logged (to client/core Logs DB, or as a JSONL
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-03
|
|
10
10
|
owners: ["mhammontree", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -46,6 +46,21 @@ In `api2/Component/Api/V2/V2.php` (~L2168) every request is logged and routed:
|
|
|
46
46
|
- There is also a **config-gated branch** (~L2166): when `_Config::api('log_filepath')` is set,
|
|
47
47
|
the log entry is written to a **file** on the host instead of the DB.
|
|
48
48
|
|
|
49
|
+
### A rejected request (HTTP 400) is NOT unlogged
|
|
50
|
+
|
|
51
|
+
`api2/Controller/Index.php` (L369-377) handles a non-success response by **committing** the
|
|
52
|
+
`DB_LOGS` and `DB_CLIENT_LOGS` transactions and rolling back **only** the business-data
|
|
53
|
+
transaction. A rejection therefore still writes a **full `Client_Logs.Api` / `Core_Logs.Api`
|
|
54
|
+
row** — request payload, response payload, `apiId`, `sourceIp`, `transactionId` — while the
|
|
55
|
+
business row is discarded.
|
|
56
|
+
|
|
57
|
+
So a 400 **is** SQL-queryable and attributable to the calling credential. What a 400 lacks is a
|
|
58
|
+
`Logs.Issue` row: **no alerting, no aggregation, no owner** (see
|
|
59
|
+
[Error Reporting — Issue/Event Capture](../../_underscore/features/error-reporting-issue-event.md)).
|
|
60
|
+
That is the deciding factor when choosing between `_Exception_Validation` (400, logged as a
|
|
61
|
+
request, no Issue) and `_Exception_Business` (500, Sentry + a routed Issue). Do **not** reach for
|
|
62
|
+
`_Exception_Business` merely to "make sure the rejection is recorded" — it already is.
|
|
63
|
+
|
|
49
64
|
## Blind spots when auditing a client's API traffic
|
|
50
65
|
|
|
51
66
|
Looking only at `Logs_<Client>.Api` misses four sources:
|
|
@@ -95,6 +110,11 @@ re-send.
|
|
|
95
110
|
|
|
96
111
|
## Change history
|
|
97
112
|
|
|
113
|
+
- 2026-08-03 — Corrected a common wrong assumption: an **HTTP 400 leaves a full log row**.
|
|
114
|
+
`Controller/Index.php` L369-377 commits `DB_LOGS`/`DB_CLIENT_LOGS` and rolls back only the
|
|
115
|
+
business transaction, so rejections are queryable and attributable to the credential; what they
|
|
116
|
+
lack is a `Logs.Issue` (no alerting/owner) — the deciding factor between `_Exception_Validation`
|
|
117
|
+
and `_Exception_Business`. (dfranks)
|
|
98
118
|
- 2026-07-29 — Documented (TRUE-80179) that the core/writer `Logs.Api` retains the **full original
|
|
99
119
|
`requestPayload`** on failed writes, making it a **replayable backlog source** and a scope
|
|
100
120
|
diagnostic: dedupe to one-per-contract with a window function on
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
9
|
-
owners: [jcardinal, mhammontree]
|
|
8
|
+
updated: 2026-08-03
|
|
9
|
+
owners: [jcardinal, mhammontree, dfranks]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/_underscore/architecture.md
|
|
@@ -573,15 +573,42 @@ $c = 'Hello ' . $world;
|
|
|
573
573
|
|
|
574
574
|
* Always use exceptions to handle unexpected conditions.
|
|
575
575
|
|
|
576
|
-
###
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
576
|
+
### Choosing an exception class is a ROUTING decision, not style
|
|
577
|
+
|
|
578
|
+
At a deliberate throw site the class you pick determines the HTTP status, whether Sentry sees
|
|
579
|
+
it, whether a `Logs.Issue` is created, how that Issue is fingerprinted, and **who gets
|
|
580
|
+
notified**. Pick deliberately:
|
|
581
|
+
|
|
582
|
+
| Throw this | When | HTTP | Sentry | `Logs.Issue` | Goes to |
|
|
583
|
+
|---|---|---|---|---|---|
|
|
584
|
+
| `_Exception_Validation` | bad **client input** — a hard-block in a pre/post interceptor or model op | **400** | no | no | the caller, via the response envelope (front end can toast it) |
|
|
585
|
+
| `_Exception_Business` | a **business condition a business user can act on and a developer cannot fix** | 500 | yes | yes, keyed by a stable `issueKey` | business recipients configured in the Tools `/errors` console |
|
|
586
|
+
| plain global `Exception` | a genuine **code defect / unexpected state** | 500 | yes | yes, fingerprinted by **trace** | a developer ClickUp task |
|
|
587
|
+
|
|
588
|
+
`_Exception_Business`'s constructor is `(issueKey, message, minimumUrgency)`. The `issueKey` is
|
|
589
|
+
the fingerprint, so the Issue's identity survives refactoring; a plain `Exception`'s
|
|
590
|
+
trace-based fingerprint changes when the code moves.
|
|
591
|
+
|
|
592
|
+
**A 400 is not "unlogged."** `api2/Controller/Index.php` commits the log transactions and rolls
|
|
593
|
+
back only the business transaction, so a rejection still writes a full `Logs.Api` row (request
|
|
594
|
+
+ response payload, `apiId`, `sourceIp`, `transactionId`). What a 400 lacks is a `Logs.Issue` —
|
|
595
|
+
no alerting, no aggregation, no owner. Do not upgrade a client-input rejection to
|
|
596
|
+
`_Exception_Business` just to "make sure it's recorded."
|
|
597
|
+
|
|
598
|
+
### NEVER `throw new _Exception(...)`
|
|
599
|
+
|
|
600
|
+
`_Exception` is an **error-handler shape**, not a general exception: its constructor requires
|
|
601
|
+
five arguments (`errorNumber`, `errorString`, `errorFile`, `errorLine`, `trace`) because
|
|
602
|
+
`_Error` populates it from `set_error_handler` context. `throw new _Exception('msg')` raises an
|
|
603
|
+
`ArgumentCountError` **before** your exception is ever constructed, destroying the real signal.
|
|
604
|
+
Throwing a plain `_Exception` also yields an HTTP 500 + stack trace — wrong for a client-input
|
|
605
|
+
rejection and an information leak. Census in `_underscore`: 173 plain `throw new Exception` vs
|
|
606
|
+
14 `_Exception`.
|
|
607
|
+
|
|
608
|
+
Sources: `_underscore/Exception.php`, `Exception/Validation.php`, `Exception/Business.php`,
|
|
609
|
+
`_underscore/Error.php`, `api2/Controller/Index.php`. First applied: the three Rate WH
|
|
610
|
+
hard-blocks (TRUE-79533). Taxonomy verified 2026-08-03 **while TRUE-78188 was still in flight** —
|
|
611
|
+
re-verify against `Controller/Index.php` before relying on it.
|
|
585
612
|
|
|
586
613
|
### Comprehensive Error Handling
|
|
587
614
|
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal", "rgirish"]
|
|
9
|
+
updated: 2026-08-03
|
|
10
|
+
owners: ["jcardinal", "rgirish", "dfranks"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
13
|
- worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
|
|
@@ -29,6 +29,56 @@ production data-integrity bug.
|
|
|
29
29
|
- USA/Canada subclasses (`Model/Compass/Usa|Canada/PurchaseOrder.php`) are empty and inherit
|
|
30
30
|
this behavior — edit the parent.
|
|
31
31
|
|
|
32
|
+
## Who actually calls `POST /v2/purchase-orders` (MITS is NOT the only caller)
|
|
33
|
+
|
|
34
|
+
`/v2/purchase-orders` for Compass is a **shared endpoint with two unrelated inbound channels**,
|
|
35
|
+
distinguished in `Core.ClientApiIdentities` (clientId 2). **The channels are told apart by the
|
|
36
|
+
FIELD NAME, not by whether a MITS SO is present at all:**
|
|
37
|
+
|
|
38
|
+
| Identity | Channel | SO field sent | Values | `purchaseOrderItems` |
|
|
39
|
+
|---|---|---|---|---|
|
|
40
|
+
| `153531108` | MITS | **`mitsSalesOrder`** (unprefixed) | only `SA…` or `MR…` | always present |
|
|
41
|
+
| `COMPASS-CXML` | cXML / EDI | **`c_mitsSalesOrder`** (`c_`-prefixed) | numeric, e.g. `472981379001` | present |
|
|
42
|
+
| `COMPASS-CXML` | cXML / EDI | **neither field** | — | often **absent entirely** |
|
|
43
|
+
|
|
44
|
+
Measured over 60 days of inbound `POST /v2/purchase-orders` (`Logs_Compass.Api`):
|
|
45
|
+
- **MITS — 4,547 posts, 100% carry the unprefixed `mitsSalesOrder`**; `SA` 2,928, `MR` 1,619.
|
|
46
|
+
Never `MA`, never any other prefix. **No `SA` value has ever been sent by a non-MITS caller.**
|
|
47
|
+
- **COMPASS-CXML — 2,697 posts**: 2,455 carry `c_mitsSalesOrder`, and **242 carry neither field**.
|
|
48
|
+
In a 30-day slice: 1,508 `c_mitsSalesOrder` posts all had items; **136 posts had neither field
|
|
49
|
+
and no `purchaseOrderItems` key** — the legitimate header-first EDI POs.
|
|
50
|
+
- Compass **Canada** posts under a **separate identity (`409531`)**, so never hardcode an
|
|
51
|
+
identity id.
|
|
52
|
+
|
|
53
|
+
**Consequences for any validation added to this endpoint:**
|
|
54
|
+
- **The unprefixed `mitsSalesOrder` is the MITS marker** — not the identity id, and not the
|
|
55
|
+
`SA`/`MR` prefix. Supporting evidence: `prePost` reads `$payload->mitsSalesOrder`; the only
|
|
56
|
+
`c_mitsSalesOrder` occurrence in the codebase is the model field declaration at
|
|
57
|
+
`_underscore/Model/Compass/PurchaseOrder.php:18`; and `Core.ApiPayloadInterceptors` has **no
|
|
58
|
+
rows for `recordId = 17`** (the `purchase-orders` record), so nothing rewrites field names in
|
|
59
|
+
flight.
|
|
60
|
+
- A "reject purchase orders with zero line items" rule keyed on the **unprefixed** field cannot
|
|
61
|
+
touch the 136 item-less EDI posts — they carry neither field. Gating on **item count alone**
|
|
62
|
+
would reject all of them.
|
|
63
|
+
- **`MA` orders never arrive from MITS.** Compass creates them manually; they go to ODP, reach us
|
|
64
|
+
over EDI, and land on an exception report for manual NetSuite entry. (An earlier belief that MA
|
|
65
|
+
is a third MITS prefix to filter on is wrong.)
|
|
66
|
+
|
|
67
|
+
> **⚠ Querying trap — a substring match on `mitsSalesOrder` conflates the two callers**, because
|
|
68
|
+
> `c_mitsSalesOrder` *contains* it. Anyone characterising this traffic from `Logs_Compass.Api`
|
|
69
|
+
> must match the **exact key**; a pattern like `"mitsSalesOrder":"` also silently misses the
|
|
70
|
+
> prefixed form, which is how this session first reached the wrong conclusion that cXML never
|
|
71
|
+
> sends an SO reference. Second trap: payloads appear **both minified**
|
|
72
|
+
> (`"mitsSalesOrder":"MR…"`) **and pretty-printed** (`"mitsSalesOrder": "MR…"`, space after the
|
|
73
|
+
> colon), so a naive pattern under-counts a second way.
|
|
74
|
+
|
|
75
|
+
> **OPEN / UNVERIFIED:** it is **not** confirmed whether the 2.0 framework strips the `c_` prefix
|
|
76
|
+
> when populating `$payload` for an interceptor. If it does, cXML requests carrying
|
|
77
|
+
> `c_mitsSalesOrder` would also enter a guard keyed on `$payload->mitsSalesOrder`. Harmless while
|
|
78
|
+
> those posts all carry items, but a future item-less cXML post carrying `c_mitsSalesOrder` would
|
|
79
|
+
> then be wrongly rejected. This **cannot be settled from logs** — it needs a dev-environment
|
|
80
|
+
> check.
|
|
81
|
+
|
|
32
82
|
## How it works
|
|
33
83
|
- **SA orders (normal):** each inbound PO item carries `createdFromSalesOrderItem.lineNumber`
|
|
34
84
|
(the MITS line, which includes configuration-line offsets). `prePost` remaps it to the real
|
|
@@ -50,6 +100,17 @@ production data-integrity bug.
|
|
|
50
100
|
because SA/MR originate in Compass (their Compass SO+items pre-exist) whereas MA is
|
|
51
101
|
ODP-originated, so nothing else would populate it.
|
|
52
102
|
|
|
103
|
+
### MR and SA consume PO line items differently (asymmetry inside one method)
|
|
104
|
+
- **MR** — `handleMrOrder` (≈L54-57) **silently skips** any item whose
|
|
105
|
+
`vendorItem->item->partNumber` is empty while building `salesOrderItems`. So an MR PO whose
|
|
106
|
+
lines *all* lack a `partNumber` passes a naive `count($payload->purchaseOrderItems) > 0` check
|
|
107
|
+
and still creates an **empty Sales Order**. A "usable item" filter for MR must therefore test
|
|
108
|
+
`partNumber`, not array length.
|
|
109
|
+
- **SA** — items are linked via `createdFromSalesOrderItem` →
|
|
110
|
+
`salesOrderItemPurchaseOrderItems`, and the quantity cross-check (≈L318-324) explicitly
|
|
111
|
+
`continue`s past `partNumber`-less lines rather than discarding them. Applying the MR
|
|
112
|
+
`partNumber` filter to SA would **reject valid orders**.
|
|
113
|
+
|
|
53
114
|
## Data model
|
|
54
115
|
- `SalesOrderItems_PurchaseOrderItems` (`Client_Compass`): `uuid`, `salesOrderItemId`,
|
|
55
116
|
`purchaseOrderItemId`. One row per (SO item, PO item) the PO fulfills.
|
|
@@ -59,6 +120,15 @@ production data-integrity bug.
|
|
|
59
120
|
USA and Canada share the parent handler unchanged.
|
|
60
121
|
|
|
61
122
|
## Gotchas / known issues
|
|
123
|
+
- **SQL injection in `prePost` (OPEN as of 2026-08-03).** `_underscore/Model/Compass/PurchaseOrder.php`
|
|
124
|
+
**L232** interpolates the raw client payload straight into SQL:
|
|
125
|
+
`WHERE SalesOrders.number LIKE '{$payload->mitsSalesOrder}'` — no escape, no cast. The **same
|
|
126
|
+
value is escaped at L301**, so the file is internally inconsistent and the correct pattern is
|
|
127
|
+
already present. Fix with `_Database::escape()` per the 2.0 back-end standard. Reachable only by
|
|
128
|
+
a caller holding valid Compass API credentials (OAuth2 `client_credentials`), so it is **not**
|
|
129
|
+
anonymous or internet-facing — normal priority, not an emergency. Separately, five sites in this
|
|
130
|
+
file (L148, L301, L461, L462, L471) use **`addslashes()`** where the standard requires
|
|
131
|
+
`_Database::escape()`.
|
|
62
132
|
- **Spurious cross-part links from the `postPost` lineNumber join (fixed 2026-06-11).** The
|
|
63
133
|
`postPost` link pass matched `SalesOrderItems.lineNumber = PurchaseOrderItems.lineNumber`
|
|
64
134
|
for *all* orders. For SA orders that is wrong: PO line numbers are PO-local, so whenever an
|
|
@@ -137,6 +207,16 @@ USA and Canada share the parent handler unchanged.
|
|
|
137
207
|
need item backfill from the sibling ODP SO first).
|
|
138
208
|
|
|
139
209
|
## Change history
|
|
210
|
+
- 2026-08-03 — Documented that `/v2/purchase-orders` has **two** Compass inbound channels (MITS
|
|
211
|
+
`153531108` vs `COMPASS-CXML`; Canada is a third identity `409531`), told apart by **field
|
|
212
|
+
name**: unprefixed `mitsSalesOrder` = MITS, `c_mitsSalesOrder` = cXML, neither = the item-less
|
|
213
|
+
header-first EDI POs. So a zero-item rejection keyed on the unprefixed field is safe, while
|
|
214
|
+
gating on item count alone is not. Recorded the substring/pretty-print querying traps that
|
|
215
|
+
conflate the two callers, and left open whether the framework strips the `c_` prefix for
|
|
216
|
+
interceptor payloads. Also recorded the MR-vs-SA `partNumber` item-consumption
|
|
217
|
+
asymmetry (MR silently skips `partNumber`-less lines → empty SO despite `count() > 0`) and an
|
|
218
|
+
open SQL-injection at L232 plus five `addslashes()` sites. Corrects the earlier belief that
|
|
219
|
+
`MA` arrives from MITS. (dfranks)
|
|
140
220
|
- 2026-07-13 — Fixed empty MA orders: the 3a ODP-PO-import cron's new-SO branch now builds
|
|
141
221
|
`salesOrderItems` from the 850 PO items (the MA back-linking block excludes MA by design).
|
|
142
222
|
Forward-only; 192 historical empties still need item backfill. (rgirish)
|
package/package.json
CHANGED