toga-ai 1.0.224 → 1.0.225
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.
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-29
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- test/@dave/checker.php
|
|
@@ -174,6 +174,22 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
174
174
|
stays bounded under the existing 1G `memory_limit`. (This corrected a prior docblock claim that `$sqls`
|
|
175
175
|
was never accumulated globally.)
|
|
176
176
|
|
|
177
|
+
- **PROFIT is now compared for all THREE categories, not just Sales.** `checker.php` and `fixer.php` were
|
|
178
|
+
previously profit-aware only for Sales; they now compute and compare profit for **Open Orders** and
|
|
179
|
+
**Opportunities** too.
|
|
180
|
+
- **Open-order profit** mirrors fixer's per-line economics: `qtyOpen * (rate - unitCost)`,
|
|
181
|
+
`unitCost = costestimate/quantity` (divide-by-zero-safe), **round-then-sum per line** to match the stored
|
|
182
|
+
`decimal(14,2)` `OpenOrderItems.profit` column.
|
|
183
|
+
- **Opportunity profit** = the legacy importer's `Σ(amount − costEstimate)` over qualifying lines = SuiteQL
|
|
184
|
+
`Σ(-foreignamount + costestimate)`; **revenue** stays the header `projectedtotal` (pulled line-level via
|
|
185
|
+
`LEFT JOIN` + `MAX(projectedtotal)`). It **must EXCLUDE item-group parent lines** — SuiteQL
|
|
186
|
+
`itemtype = 'Group'` ↔ `App_Model_Forecast2_Item::NETSUITE_TRANSACTION_TYPE__ITEM_GROUP`, which the
|
|
187
|
+
importer skips. A dedicated `OPP_EXCLUDED_ITEMTYPES` constant (= the ORD exclusion list + `'Group'`)
|
|
188
|
+
carries this.
|
|
189
|
+
- **`fixer.fixOpportunities()` previously did NOT write `projectedProfit` at all.** It now computes
|
|
190
|
+
`projectedProfit` from the `listOpportunities()` itemList (`Σ(amount − costEstimate)`, skipping ITEM_GROUP
|
|
191
|
+
and items missing from the `Items` lookup, rounded to cents) so the FIX can actually resolve any
|
|
192
|
+
opportunity-profit drift the FIND flags (see the compute-identically invariant below).
|
|
177
193
|
- **checker → fixer handoff is WINDOW-based, not id-based (decision).** checker hands fixer a tight day
|
|
178
194
|
window and lets fixer **re-find** the discrepant ids inside it, rather than passing exact ids. Rationale:
|
|
179
195
|
maker≠checker independence (fixer re-derives the drift), and checker has already shrunk the window, so a
|
|
@@ -269,6 +285,40 @@ None — Forecast2 is a single shared dataset.
|
|
|
269
285
|
(NS 852.06 vs FC 852.07). **Correction to any prior note:** the open-orders **2025-04 "unlocalized month" was
|
|
270
286
|
this round-then-sum noise, NOT NS-live-vs-Forecast-replica timing jitter** — the rounding fix eliminated it,
|
|
271
287
|
disproving the earlier replica-jitter hypothesis.
|
|
288
|
+
- **NULL `rate` on cost-only open-order lines silently drops them from a NS profit SUM → permanent phantom
|
|
289
|
+
profit drift FIX can never resolve.** NetSuite open-order **cost-only** lines (zero revenue — e.g.
|
|
290
|
+
freight/handling sub-items) can carry a **NULL `rate`**. The FIND profit SQL computed `tl.rate - unitCost`;
|
|
291
|
+
in SQL `NULL - x = NULL`, so `ROUND(...)` returned NULL and the whole line **silently dropped out of
|
|
292
|
+
`SUM(...)`**, inflating the NS-side profit. But fixer's FIX (PHP) and the legacy importer treat a NULL rate
|
|
293
|
+
as **0** (`(float)$rate ?? 0`), so the stored Forecast2 profit was already correct. The two conventions
|
|
294
|
+
disagreed, so FIND kept flagging profit drift (e.g. Δprofit **$0.54** on many identical **$12.20** SOs;
|
|
295
|
+
large **$1,365–$9,500** deltas on **$0-revenue** SOs) that FIX could never close — every FIX reported
|
|
296
|
+
`ins=0/upd=0/del=0` because the line was already right. **Revenue never exposed it** (those lines are $0
|
|
297
|
+
revenue either way: NULL-dropped on NS vs 0-stored on FC both sum the same), so it surfaced **only in the
|
|
298
|
+
profit dimension**. **Fix:** wrap rate as `NVL(tl.rate, 0)` in the open-order **revenue AND profit**
|
|
299
|
+
expressions so FIND uses the same "NULL rate = 0" convention as FIX/importer. Applied in **both**
|
|
300
|
+
`checker.php` (openorders `nsSub`) and `fixer.php` (`findOpenOrderDiscrepancies`). After the fix all three
|
|
301
|
+
categories report ✓ in sync — there was never real drift, so **no production write was needed**.
|
|
302
|
+
**Diagnostic that nailed it:** a per-line probe dumping NS `transactionline`
|
|
303
|
+
(`lineid`/`item`/`itemtype`/`qty`/`billed`/`rate`/`costestimate` + recomputed profit) alongside the stored
|
|
304
|
+
`OpenOrderItems` rows, which revealed the NULL `rate` on the cost-only lines. (This is a *distinct*
|
|
305
|
+
convention-mismatch from the round-then-sum gotcha above — there the divergence was rounding; here it is
|
|
306
|
+
NULL handling. Both are "FIND and FIX must compute identically.")
|
|
307
|
+
- **INVARIANT: checker FIND, fixer FIND, and fixer FIX must compute a given dimension IDENTICALLY, or `--fix`
|
|
308
|
+
chases phantoms forever.** Any divergence in inclusion filter, rounding, or NULL handling between the three
|
|
309
|
+
passes manifests as a discrepancy FIND flags and FIX refuses to resolve (`ins=0/upd=0/del=0`), looping
|
|
310
|
+
forever. Both the round-then-sum and the NULL-rate gotchas above are instances. When adding/changing a
|
|
311
|
+
compared dimension, change all three sites in lockstep. `listOpportunities()`
|
|
312
|
+
(`library/app/api/netsuite/rest.php`) returns line items with `amount = -foreignamount` and
|
|
313
|
+
`costEstimate = -costestimate`, so the REST-itemList sum and the SuiteQL line-sum are **algebraically
|
|
314
|
+
identical** — that equivalence is what lets fixer's FIX (REST itemList) and FIND (SuiteQL) agree on
|
|
315
|
+
opportunity profit.
|
|
316
|
+
- **PHASE 3 post-fix re-check ignores `--category` and re-runs ALL three FIND passes (known inefficiency,
|
|
317
|
+
NOT yet fixed).** `fixer.php` PHASE 3 (post-fix re-check) re-runs all three category FIND passes
|
|
318
|
+
unconditionally, ignoring `--category` / `$runCats`. The **Sales** per-transaction aggregate SuiteQL
|
|
319
|
+
(~694K-row Sales table, paginated 1000/page) is the heaviest op in the tool, so even a category-scoped run
|
|
320
|
+
(e.g. openorders-only) triggers a **minute-plus full Sales re-scan that looks like a hang**. Suggested fix
|
|
321
|
+
(not applied): gate each PHASE 3 FIND on `in_array($cat, $runCats)`.
|
|
272
322
|
- **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
|
|
273
323
|
dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
|
|
274
324
|
identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
|
|
@@ -360,6 +410,23 @@ None — Forecast2 is a single shared dataset.
|
|
|
360
410
|
|
|
361
411
|
## Change history
|
|
362
412
|
|
|
413
|
+
- 2026-06-29 — **Found/fixed the NULL-rate phantom open-order profit drift; extended PROFIT comparison to all
|
|
414
|
+
three categories.** Cost-only NS open-order lines (zero revenue) can have a **NULL `rate`**; the FIND
|
|
415
|
+
profit SQL did `tl.rate - unitCost` and `NULL - x = NULL` silently dropped the line from `SUM`, inflating
|
|
416
|
+
NS profit, while FIX/importer treat NULL rate as 0 — a permanent phantom (Δprofit $0.54 on identical $12.20
|
|
417
|
+
SOs; $1,365–$9,500 on $0-revenue SOs) FIX reported `ins/upd/del=0` on forever. Revenue never showed it ($0
|
|
418
|
+
either way). **Fix:** `NVL(tl.rate,0)` in the open-order revenue AND profit expressions in **both**
|
|
419
|
+
`checker.php` (openorders `nsSub`) and `fixer.php` (`findOpenOrderDiscrepancies`); all three categories then
|
|
420
|
+
✓ in sync, no prod write needed. Localized via a per-line NS `transactionline` probe. Also: checker/fixer
|
|
421
|
+
now compare **profit for Open Orders and Opportunities**, not just Sales — open-order profit
|
|
422
|
+
`qtyOpen*(rate-unitCost)` round-then-sum per line; opportunity profit `Σ(amount−costEstimate)` excluding
|
|
423
|
+
`itemtype='Group'` (= `ITEM_GROUP`, new `OPP_EXCLUDED_ITEMTYPES` constant), revenue = header
|
|
424
|
+
`projectedtotal`; `fixer.fixOpportunities()` now writes `projectedProfit` (previously never did) from the
|
|
425
|
+
`listOpportunities()` itemList. Recorded the cross-toolchain **invariant**: checker FIND, fixer FIND, and
|
|
426
|
+
fixer FIX must compute each dimension identically or `--fix` chases phantoms. **Known inefficiency (not
|
|
427
|
+
fixed):** `fixer.php` PHASE 3 post-fix re-check re-runs all three FIND passes ignoring `--category`, so a
|
|
428
|
+
scoped run still triggers the heavy ~694K-row Sales re-scan (looks like a hang); suggested gate on
|
|
429
|
+
`in_array($cat,$runCats)`. (dfranks)
|
|
363
430
|
- 2026-06-26 — **Added `checker.php` (moment-fingerprint drift detector) + `looper.php` (loop runner), and
|
|
364
431
|
found/fixed the open-orders round-then-sum rounding gotcha.** `checker.php` confirms sync in seconds and
|
|
365
432
|
localizes real drift via a 5-number per-bucket fingerprint (`n,rev,profit,idSum,idSq`; power-sums mod the
|
package/package.json
CHANGED