toga-ai 1.0.204 → 1.0.206
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/architecture.md +40 -1
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +104 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/surface-meta-option.md +47 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +90 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +71 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/tow-foundation/features/receipt-processing.md +31 -3
- package/package.json +1 -1
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
| [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
|
|
12
12
|
| [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
|
|
13
13
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
|
|
14
|
+
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
|
|
14
15
|
| [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
|
|
15
16
|
| [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-25
|
|
10
10
|
owners: ["jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/_underscore.php
|
|
@@ -32,6 +32,9 @@ Minimum PHP 8.1 (enforced in `_underscore.php`). The current architecture is
|
|
|
32
32
|
`_Worker_*`). Always use parameterized queries / the `_Db` layer — never interpolate input into SQL.
|
|
33
33
|
Beware the **lazy-transaction gotcha**: writes issued outside an explicitly committed transaction
|
|
34
34
|
can be silently dropped — confirm commit semantics before relying on a write.
|
|
35
|
+
The Surface presentation layer replaces `Page::meta()` but `meta()` must NOT be deleted until
|
|
36
|
+
every page is cut over (per-surface, with a parity diff); config describes, PHP decides —
|
|
37
|
+
business logic never moves into Surface config.
|
|
35
38
|
|
|
36
39
|
## Entry point & boot sequence
|
|
37
40
|
|
|
@@ -44,6 +47,41 @@ Every 2.0 project's `index.php` is just `<?php require '_underscore.php';`.
|
|
|
44
47
|
(parses INI) → `_Time::initialize()` → **`_underscore::initialize()`** (project hook,
|
|
45
48
|
required) → `_Session::initialize()` → `_Route::initialize()` (web only).
|
|
46
49
|
|
|
50
|
+
## Surface — platform UI presentation/configuration layer
|
|
51
|
+
|
|
52
|
+
A DB-driven UI presentation layer (2026-06-25, CTO-reviewed: AGREE-WITH-ADJUSTMENTS) that
|
|
53
|
+
serves UI config (labels, theme tokens, visibility/enable, icons, tooltips, button bars,
|
|
54
|
+
table quick-actions, status badges, modals) to React frontends so UI changes need no
|
|
55
|
+
front-end deploy and are editable by a future admin app over plain CRUD. Platform-wide
|
|
56
|
+
across all 2.0 apps — not client-specific. Replaces `_Model_Core_Page::meta()`.
|
|
57
|
+
|
|
58
|
+
**Model:** `Surfaces` (a configurable UI region) + `SurfaceElements` (ordered items: ~15
|
|
59
|
+
typed attribute columns + one JSON `config` long-tail) — typed columns, NOT EAV. Behavior
|
|
60
|
+
via an `Actions` catalog (string key + JSON payload; FE registry maps key→handler). i18n via
|
|
61
|
+
`Messages` + per-client `MessageTranslations` (ICU). Theming via semantic `ThemeTokens` (no
|
|
62
|
+
raw CSS in config). `Vocabularies`/`VocabularyTerms` for value→token/label sets. The entire
|
|
63
|
+
cascade lives in ONE sparse `SurfaceOverrides` table (precedence base < client < persona <
|
|
64
|
+
role, language overlay) replacing the ~40-table EAV settings cascade.
|
|
65
|
+
|
|
66
|
+
**Decisions:** typed-columns-not-EAV; `TableViews` kept as the table query/data engine and
|
|
67
|
+
only REFERENCED (Surface type=TABLE → `tableViewSlug`); ACL kept as-is and COMPOSED
|
|
68
|
+
(`aclActionId`), never absorbed; no versioning/draft/publish; server-side cached resolver
|
|
69
|
+
(`_Model_Core_Surface::resolve`, parameterized, zero-write-on-read). Two-tier gating —
|
|
70
|
+
Tier-1 trivial declarative rules (frozen `all/any/none + {field,op,value}` grammar) evaluated
|
|
71
|
+
client-side; Tier-2 business logic computed in PHP returning booleans (lives in
|
|
72
|
+
`_Model_<Client>_*` overrides + interceptors). Config/logic boundary: **config describes,
|
|
73
|
+
PHP decides.** M2M-safe delivery via opt-in `surface=<slug>` request option → response
|
|
74
|
+
`meta.surface`, never `data`. Generic FE machinery destined for `@agilant/toga-blox`.
|
|
75
|
+
|
|
76
|
+
**Rejected alternatives:** a generic EAV settings cascade (the old `*Settings` tables —
|
|
77
|
+
proven near-empty in prod); raw-CSS-in-config (the old "Dummy Fields" anti-pattern).
|
|
78
|
+
|
|
79
|
+
**CTO-mandated guardrails (CI-enforced, not intentions):** rule grammar is a frozen
|
|
80
|
+
allowlist that throws on unknown ops, with a CI test asserting the op-set; defensive
|
|
81
|
+
resolver with logged fallbacks; debug-bundle endpoint ships WITH the resolver; `c_longValue`
|
|
82
|
+
on overrides with strict per-attribute casting; per-surface meta() cutover with a parity
|
|
83
|
+
diff (do NOT delete meta() until every page is migrated); `BLANK_CLIENT_DATABASE` as a CI gate.
|
|
84
|
+
|
|
47
85
|
## Naming convention & autoloader
|
|
48
86
|
|
|
49
87
|
Class names map **directly** to file paths: replace each `_` with `/`; the last segment
|
|
@@ -285,3 +323,4 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
285
323
|
|
|
286
324
|
## Change history
|
|
287
325
|
- 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
|
|
326
|
+
- 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Core/Surface.php
|
|
13
|
+
- _underscore/Model/Core/SurfaceElement.php
|
|
14
|
+
- _underscore/Model/Core/Action.php
|
|
15
|
+
- _underscore/Model/Core/Vocabulary.php
|
|
16
|
+
- _underscore/Model/Core/VocabularyTerm.php
|
|
17
|
+
- _underscore/Model/Core/Message.php
|
|
18
|
+
- _underscore/Model/Client/SurfaceOverride.php
|
|
19
|
+
- _underscore/Model/Client/MessageTranslation.php
|
|
20
|
+
- _underscore/Model/Client/ThemeToken.php
|
|
21
|
+
- _underscore/Model/Core/Page.php
|
|
22
|
+
related:
|
|
23
|
+
- ../../dbchanges2/features/surface-layer-schema.md
|
|
24
|
+
- ../../api2/features/surface-meta-option.md
|
|
25
|
+
- acl-permission-chain.md
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What it is
|
|
29
|
+
|
|
30
|
+
The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a
|
|
31
|
+
cached resolver, `_Model_Core_Surface::resolve(&$api, string $slug)`, that merges base config +
|
|
32
|
+
cascade overrides + ACL + messages + theme tokens into one flat, serializable bundle. It is the
|
|
33
|
+
**replacement for `_Model_Core_Page::meta()`** and is shipped as a scripted-API method behind
|
|
34
|
+
`GET /v2/surfaces/{slug}/meta`. Platform/shared — not client-specific. Schema:
|
|
35
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
36
|
+
|
|
37
|
+
## The 9 models
|
|
38
|
+
|
|
39
|
+
Core: `_Model_Core_{Surface,SurfaceElement,Action,Vocabulary,VocabularyTerm,Message}`.
|
|
40
|
+
Client: `_Model_Client_{SurfaceOverride,MessageTranslation,ThemeToken}`. Each `extends _Model_True`
|
|
41
|
+
with `const DATABASE`/`TABLE`, typed public-prop fields, and array FKs declaring
|
|
42
|
+
`FIELDOPT_FOREIGNKEY_MODEL` (cross-DB FKs are soft — no SQL constraint). Modeled on
|
|
43
|
+
`RecordField.php`/`TableView.php`.
|
|
44
|
+
|
|
45
|
+
## How `resolve()` works
|
|
46
|
+
|
|
47
|
+
Inputs come from `$api` (the same accessor `_Model_Client_TableView::meta` uses):
|
|
48
|
+
`clientIdentifier`, `personaIds`, `roleIds`, `languageId`; `$slug` is the query arg.
|
|
49
|
+
|
|
50
|
+
1. Load `Surface` by slug via ORM `->load()` — **parameterized** (fixes the old `LIKE '$slug'`
|
|
51
|
+
injection).
|
|
52
|
+
2. One query: all `SurfaceElements` for `surfaceId`, ordered by `sortOrder`.
|
|
53
|
+
3. One query: matching `SurfaceOverrides`; apply **most-specific-last** in a single in-memory pass
|
|
54
|
+
(`ORDER BY (roleId IS NULL) DESC, (personaId IS NULL) DESC`; language filtered to active or NULL).
|
|
55
|
+
Precedence **base < client < persona < role**, language an orthogonal overlay.
|
|
56
|
+
4. **ACL composition, not absorption** — batch-resolve permissions via
|
|
57
|
+
`_Model_Client_AclActionPermission`; drop elements whose `aclActionId` isn't permitted (ACL stays
|
|
58
|
+
exactly as-is, see [acl-permission-chain](acl-permission-chain.md)).
|
|
59
|
+
5. Batch-load (one integer-`IN` query each): `Messages`+`MessageTranslations` (referenced keys only),
|
|
60
|
+
`Vocabularies`+`VocabularyTerms`, `Actions`, client `ThemeTokens`.
|
|
61
|
+
6. `type=TABLE` → emit `{tableViewSlug}` only; the FE keeps calling the existing `TableView::meta`
|
|
62
|
+
table pipeline (referenced, never duplicated).
|
|
63
|
+
7. Assemble the flat bundle. **Zero INSERTs.**
|
|
64
|
+
|
|
65
|
+
A `debug()` inspector returns the resolved bundle plus the raw cascade layers (shipped with the
|
|
66
|
+
resolver, not "later").
|
|
67
|
+
|
|
68
|
+
## Caching & invalidation
|
|
69
|
+
|
|
70
|
+
- Disk cache keyed `surface:meta:{app}:{slug}:{client}:{sortedPersonaIds}:{sortedRoleIds}:{lang}`.
|
|
71
|
+
- **Bust on write, never TTL-only.** `postPost/postPut/postPatch/postDelete` interceptors on all 9
|
|
72
|
+
models do a broad client-scoped bust; a write to a **Core** table must bust all clients' bundles
|
|
73
|
+
(or fold a Core-version stamp into the key).
|
|
74
|
+
- **Keep the bust OUTSIDE the DB transaction** (the lazy-transaction write-drop rule).
|
|
75
|
+
|
|
76
|
+
## Config vs logic boundary (the hard rule)
|
|
77
|
+
|
|
78
|
+
**Config describes; PHP decides.** Two-tier gating:
|
|
79
|
+
- **Tier 1** — trivial declarative gates (`{all|any|none:[{field,op,value}]}`, nestable) shipped as
|
|
80
|
+
JSON on the element and evaluated **client-side** against the loaded record. Grammar is a **frozen
|
|
81
|
+
allowlist** — a new op is an escalation to Tier 2, not a config change.
|
|
82
|
+
- **Tier 2** — business logic (multi-stage approvals, "reviewer assigned", anything touching
|
|
83
|
+
`Approvals`) is **computed in PHP** and returned as a resolved boolean. It lives in
|
|
84
|
+
`_Model_<Client>_*` overrides + `Client_ApiPayloadInterceptor` hooks — the Surface layer does
|
|
85
|
+
**not** absorb business logic.
|
|
86
|
+
|
|
87
|
+
## Gotchas
|
|
88
|
+
|
|
89
|
+
- **`_Model_Core_Page::meta()` (Model/Core/Page.php, ~1300 lines) is the engine being replaced** and
|
|
90
|
+
is dangerous to extend: it interpolates `LIKE '$slug'` (injection-prone), **runs INSERTs during a
|
|
91
|
+
read**, is N+1, hand-duplicates the 5-layer cascade 6× via UNION, and is uncached. The empirical
|
|
92
|
+
basis for the typed-over-EAV design: in `Client_Compass` the whole EAV settings cascade (`Settings`
|
|
93
|
+
16-key registry + ~40 `*Settings` tables) is almost all 0 rows, while typed `TableViews`/
|
|
94
|
+
`TableViewFields`/`SectionRecordFields` ARE populated. **Do not delete `meta()`** until every page
|
|
95
|
+
is migrated — cut over per-surface with a parity diff (old bundle vs new bundle).
|
|
96
|
+
- **The resolver must be defensive** — config writes bypass code review/CI, so missing/invalid config
|
|
97
|
+
falls back to sane defaults and never crashes a page. Strict per-`attribute` casting with logged
|
|
98
|
+
fallback on bad `SurfaceOverrides` data.
|
|
99
|
+
- **Cross-DB reads are batched** (DB_CORE vs DB_CLIENT) and FKs are soft — tolerate dangling refs.
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
- 2026-06-25 — Built the 9 models + `resolve()`/`debug()` as the cached, parameterized,
|
|
103
|
+
zero-write-on-read replacement for `_Model_Core_Page::meta()`; cache-bust interceptors on all 9
|
|
104
|
+
models (outside the transaction). meta() retained until per-page cutover with parity diff. (jcardinal)
|
|
@@ -5,6 +5,7 @@
|
|
|
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
6
|
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
|
|
7
7
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
8
|
+
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
8
9
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
9
10
|
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
|
|
10
11
|
| [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, _underscore/Route.php |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Surface action-state via the surface=<slug> request option (M2M-safe)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Model/Core/Surface.php
|
|
14
|
+
related:
|
|
15
|
+
- ../../_underscore/features/surface-resolver.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What it is
|
|
19
|
+
|
|
20
|
+
An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state
|
|
21
|
+
(`isVisible`/`isEnabled`) to a GET response **under `meta.surface`, never `data`**. It is the
|
|
22
|
+
M2M-safe delivery mechanism for the platform-wide Surface layer (see
|
|
23
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md)): the same core data API serves
|
|
24
|
+
both the UI and machine-to-machine integrations, so presentation state must never leak into the
|
|
25
|
+
business data that integrations consume.
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
When `surface=<slug>` is present on a GET, after loading the record(s) the engine calls
|
|
30
|
+
`_Model_Core_Surface::resolveRecordState($api, $slug, $record)` and attaches per-record
|
|
31
|
+
`{isVisible, isEnabled}` action state under `meta.surface` (single-record and list shapes; a list is
|
|
32
|
+
resolved in one batched pass, per row in `meta`). When the option is **absent** (all M2M/integration
|
|
33
|
+
traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work. The path is
|
|
34
|
+
**defensive**: it catches `Throwable` and omits `meta.surface` on error rather than failing the call.
|
|
35
|
+
|
|
36
|
+
## Key rules
|
|
37
|
+
|
|
38
|
+
- **Presentation state lives in `meta.surface`, never `data`.** Integrations get pure business data.
|
|
39
|
+
- **The business rule is enforced at the action endpoint** (the source of truth that also protects
|
|
40
|
+
M2M — e.g. an integration approving a `fulfilled` order is rejected by the same rule). The surface
|
|
41
|
+
option merely **previews** that rule for the UI; it is a read-only preview, not a second copy of
|
|
42
|
+
the logic.
|
|
43
|
+
- **One call, no extra round-trip** — the state pass rides the existing data fetch.
|
|
44
|
+
|
|
45
|
+
## Change history
|
|
46
|
+
- 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
|
|
47
|
+
state under `meta.surface` (M2M-safe, defensive, absent = unchanged). (jcardinal)
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql |
|
|
6
7
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Surface Layer Schema (UI presentation/config tables)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql
|
|
13
|
+
- dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql
|
|
14
|
+
- dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
|
|
15
|
+
- dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
|
|
16
|
+
- dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
|
|
17
|
+
- dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
|
|
18
|
+
- dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
|
|
19
|
+
related:
|
|
20
|
+
- ../../_underscore/features/surface-resolver.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
The persistent schema for the platform-wide **Surface** UI presentation/configuration layer
|
|
26
|
+
(see the `_underscore` [surface-resolver](../../_underscore/features/surface-resolver.md) feature
|
|
27
|
+
for the runtime). DB-driven UI config (labels, colors via tokens, visibility/enable, icons,
|
|
28
|
+
tooltips, button bars, table quick-actions, status badges, modals) so UI changes need no
|
|
29
|
+
front-end deploy and are editable by a future admin app over plain CRUD. **Platform/shared — not
|
|
30
|
+
client-specific** (Compass is only the first seed/test tenant). It replaces the legacy
|
|
31
|
+
`_Model_Core_Page::meta()` EAV settings cascade (the old ~40 `*Settings` tables — almost all empty
|
|
32
|
+
in prod).
|
|
33
|
+
|
|
34
|
+
## The 9 tables — Core (structural) vs Client (per-tenant deltas)
|
|
35
|
+
|
|
36
|
+
Structural definitions live in **Core** (platform-shared, mirrors `Records`/`RecordFields`);
|
|
37
|
+
per-tenant deltas live in **Client** (mirrors `TableViews`/ACL/`ItemTranslations`).
|
|
38
|
+
|
|
39
|
+
| Table | DB | Purpose |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `Surfaces` | Core | A configurable UI region (`type` ENUM TABLE/SECTION/BUTTON_BAR/ROW_ACTIONS/TAB_STRIP/FILTER_SET/MODAL/DRAWER) |
|
|
42
|
+
| `SurfaceElements` | Core | Ordered items: ~15 typed attr columns + one JSON `config` long-tail + `visibilityRule`/`enabledRule` JSON |
|
|
43
|
+
| `Actions` | Core | Behavior action catalog (FE registry keys + JSON payload) |
|
|
44
|
+
| `Vocabularies` | Core | A value-set (status/stage/priority/type/channel) per record |
|
|
45
|
+
| `VocabularyTerms` | Core | value → token/label/order |
|
|
46
|
+
| `Messages` | Core | i18n key catalog + ICU default value |
|
|
47
|
+
| `SurfaceOverrides` | Client | THE single sparse cascade table (+`c_longValue mediumtext`) replacing ~40 EAV tables |
|
|
48
|
+
| `MessageTranslations` | Client | per-language overlay (sidecar pattern) + `c_longValue` |
|
|
49
|
+
| `ThemeTokens` | Client | per-tenant semantic token → value (the only place a hex/icon name lives) |
|
|
50
|
+
|
|
51
|
+
`SurfaceOverrides` carries the entire cascade in one sparse table: scope columns
|
|
52
|
+
`personaId`/`roleId`/`languageId` (NULL = not scoped on that axis), an `attribute` ENUM, and
|
|
53
|
+
`value varchar(255)` + `c_longValue mediumtext`. Precedence **base < client < persona < role**
|
|
54
|
+
with language as an orthogonal overlay; the resolver applies it most-specific-last in one pass.
|
|
55
|
+
|
|
56
|
+
## Migration files (typed columns, not EAV)
|
|
57
|
+
|
|
58
|
+
CREATE order is FK-safe (`Messages → Actions → Vocabularies → VocabularyTerms → Surfaces →
|
|
59
|
+
SurfaceElements`). Core migrations run first; Client after. The session built/seeded the
|
|
60
|
+
**SalesOrders + login** vertical (not Tickets) as the review proof:
|
|
61
|
+
|
|
62
|
+
- `Core/2026-06-25a - SurfaceCoreTables.sql` — the 6 Core tables.
|
|
63
|
+
- `Core/2026-06-25b - SurfaceRecordsAndFields.sql` — `Core.Records`+`RecordFields` for all 9 tables (reserved id block **2300–2399**; `recordId` via `model=` subselect).
|
|
64
|
+
- `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
|
|
65
|
+
- `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
|
|
66
|
+
- `Client/2026-06-25b - SurfaceClientSeed.sql` — `ThemeTokens` + `MessageTranslations(en)`.
|
|
67
|
+
- `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records.
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
|
|
71
|
+
- **Cross-DB FKs (Core↔Client) are SOFT** — the ORM declares `FIELDOPT_FOREIGNKEY_MODEL` but **no
|
|
72
|
+
SQL constraint** exists (e.g. `Surfaces.tableViewId`, `SurfaceOverrides.surfaceId`,
|
|
73
|
+
`MessageTranslations.messageId`). This keeps Core/Client migration ordering safe and lets the
|
|
74
|
+
resolver tolerate dangling refs. Never add a real cross-DB constraint.
|
|
75
|
+
- **`BLANK_CLIENT_DATABASE.sql` MUST append** the 3 new Client tables + the `Languages` `en` seed,
|
|
76
|
+
or every newly provisioned client is born broken. This was appended and provisioning-tested.
|
|
77
|
+
Treat it as a CI gate (provision a throwaway client; assert the surface meta endpoint returns the
|
|
78
|
+
seed).
|
|
79
|
+
- **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
|
|
80
|
+
would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
|
|
81
|
+
resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
|
|
82
|
+
- **Reserved id block 2300–2399** for the Surface `Records`/`RecordFields` rows (Core max was 331);
|
|
83
|
+
use `model=` subselects for `recordId`, never hardcoded ids.
|
|
84
|
+
- **CLIENT-aclDatabase records** (`SurfaceOverrides`, `MessageTranslations`, `ThemeTokens`) need the
|
|
85
|
+
full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
|
|
86
|
+
|
|
87
|
+
## Change history
|
|
88
|
+
- 2026-06-25 — Initial schema for the Surface layer: 6 Core + 3 Client tables, `Records`/`RecordFields`
|
|
89
|
+
(ids 2300–2399), SalesOrders+login seed, ACL chain, and `BLANK_CLIENT_DATABASE` append. Typed
|
|
90
|
+
columns chosen over EAV (the old `*Settings` cascade was empirically near-empty in prod). (jcardinal)
|
|
@@ -8,3 +8,4 @@
|
|
|
8
8
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
|
|
9
9
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
10
10
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
|
|
11
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/{slug}/meta` instead of statically-imported JSON f | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/featureFlag.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Surface Frontend (DB-driven UI consumption, src/surface/)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga25-supply
|
|
5
|
+
project: TOGa 2.5 Supply
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-25
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- toga25-supply/src/surface/useFetchSurfaceMeta.ts
|
|
13
|
+
- toga25-supply/src/surface/evaluateSurfaceRule.ts
|
|
14
|
+
- toga25-supply/src/surface/actionRegistry.ts
|
|
15
|
+
- toga25-supply/src/surface/SurfaceActionBar.tsx
|
|
16
|
+
- toga25-supply/src/surface/SurfaceSection.tsx
|
|
17
|
+
- toga25-supply/src/surface/resolve.ts
|
|
18
|
+
- toga25-supply/src/surface/types.ts
|
|
19
|
+
- toga25-supply/src/surface/featureFlag.ts
|
|
20
|
+
- toga25-supply/src/surface/index.ts
|
|
21
|
+
- toga25-supply/src/pages/Login/LoginPage.tsx
|
|
22
|
+
- toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
|
|
23
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
|
|
24
|
+
related:
|
|
25
|
+
- ../../_underscore/features/surface-resolver.md
|
|
26
|
+
- meta-driven-table-data.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What it is
|
|
30
|
+
|
|
31
|
+
The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from
|
|
32
|
+
`GET /v2/surfaces/{slug}/meta` instead of statically-imported JSON field bundles. Lives in
|
|
33
|
+
`src/surface/` for now with a `TODO(blox)` to extract the generic machinery into `@agilant/toga-blox`
|
|
34
|
+
so all 2.0 apps inherit it. Backend: [surface-resolver](../../_underscore/features/surface-resolver.md).
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
- **`useFetchSurfaceMeta`** — the single fetch-once/React-Query-cached choke point for a surface's
|
|
39
|
+
meta. Loading a different *record* into the same surface reuses cached meta.
|
|
40
|
+
- **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
|
|
41
|
+
grammar** that **throws on an unknown op** (generalized from the SalesOrders
|
|
42
|
+
`buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
|
|
43
|
+
the loaded record. A new op is an escalation to a backend Tier-2 capability method, never a grammar
|
|
44
|
+
change.
|
|
45
|
+
- **`actionRegistry`** — string action key → existing app handler (the proven `MODAL_RENDERERS`
|
|
46
|
+
pattern). The DB ships keys + payloads; the app supplies the actual handlers.
|
|
47
|
+
- **theme/message resolvers** (`resolve.ts`) — token → CSS and ICU message rendering.
|
|
48
|
+
- **`SurfaceActionBar`/`SurfaceActions` + `SurfaceSection`** — generic renderers.
|
|
49
|
+
|
|
50
|
+
## What's wired (review proof)
|
|
51
|
+
|
|
52
|
+
Login + sales-orders list + the sales-order modal, all behind the **`SURFACE_ENABLED`** flag (env
|
|
53
|
+
`VITE_SURFACE_ENABLED`, **default OFF**). The SalesOrder action bar reproduces
|
|
54
|
+
`orderViewFields.json` approve/deny/approvalWorkflow/viewLog/editOrder by delegating to the existing
|
|
55
|
+
`onActionClick`/`onOpenLog` handlers. The table stays on the existing TableView pipeline via
|
|
56
|
+
`tableViewSlug` (see [meta-driven-table-data](meta-driven-table-data.md)) — the Surface layer
|
|
57
|
+
references the TableView, never replaces it.
|
|
58
|
+
|
|
59
|
+
## Gotchas
|
|
60
|
+
|
|
61
|
+
- **Default OFF.** Nothing renders through the Surface path unless `VITE_SURFACE_ENABLED` is set —
|
|
62
|
+
the existing JSON-bundle path remains live until per-surface cutover.
|
|
63
|
+
- **Tier-1 only on the client.** Business-logic gates (Tier-2) arrive as resolved booleans from the
|
|
64
|
+
backend (`meta.surface`); never re-encode business rules in the FE evaluator.
|
|
65
|
+
- **`tsc` not yet run** this session (the private `@agilant/toga-blox` registry needs npm creds);
|
|
66
|
+
treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
|
|
67
|
+
|
|
68
|
+
## Change history
|
|
69
|
+
- 2026-06-25 — Built `src/surface/` (fetch hook, frozen Tier-1 rule evaluator, action registry, theme/
|
|
70
|
+
message resolvers, action-bar + section renderers); wired login + SalesOrders list + SO modal behind
|
|
71
|
+
the OFF-by-default `SURFACE_ENABLED` flag. Generic machinery destined for toga-blox. (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,10 +15,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
|
|
16
16
|
## 2.0 framework
|
|
17
17
|
|
|
18
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
18
|
+
- **_underscore** (_Underscore) _(framework core)_ — 16 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
19
|
- **worker2** (Worker) — 15 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
|
-
- **api2** (API) —
|
|
21
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
20
|
+
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
23
23
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
24
24
|
- **toga2-view** (TOGa View Frontend) — 4 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -5,7 +5,7 @@ project: Worker
|
|
|
5
5
|
client: tow-foundation
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-06-
|
|
8
|
+
updated: 2026-06-25
|
|
9
9
|
owners: ["rgirish"]
|
|
10
10
|
files:
|
|
11
11
|
- worker2/Worker/Client/TowFoundation.php
|
|
@@ -57,8 +57,11 @@ Credit Card Receipts/
|
|
|
57
57
|
5. **Pass 1 — Extract** — for each receipt:
|
|
58
58
|
- Enforce type + size limit: unsupported mime types (`application/octet-stream`) throw immediately; 4 MB cap for images, 10 MB for documents
|
|
59
59
|
- Download file bytes from SharePoint
|
|
60
|
-
-
|
|
60
|
+
- **`.docx` files bypass Talos** and route to `extractDocxData()` (Talos returns HTTP 500 on a `.docx` MIME type — it only accepts PDF/image). That method opens the docx as a ZIP, extracts `word/document.xml`, strips tags, and regex-parses `MM/DD/YY: description` lines into `line_items[]`
|
|
61
|
+
- All other types POST to Talos AI `/api/ai/generate` → structured `{vendor_name, invoice_date, total, payment_memo, category, ...}`
|
|
62
|
+
- **Year guard on AI dates** — if Talos returns an `invoice_date` whose year is more than 1 year from the current year (AI hallucination on two-digit year inputs, e.g. `5/13/76` → 1976, `5/28/28` → 2028), the year is clamped to the current year while month/day are preserved
|
|
61
63
|
- Cross-verify amount ± $0.01 against parsed statement; if matched, override `payment_memo` with statement description
|
|
64
|
+
- **Zero-total fallback** — if the extracted/docx total is 0, `lookupStatementTotalByVendor()` derives the amount from the billing statement (see *Statement structure* below)
|
|
62
65
|
- Build base filename (no suffix yet) and store in `$pendingRenames`
|
|
63
66
|
- On extract failure → move to `Archive/exception/` immediately
|
|
64
67
|
6. **Deduplication** — after Pass 1, receipts sharing identical `(personName, invoice_date, vendor_name, total)` are deduplicated; only the first is kept, the rest are logged as `DUPLICATE skipped` and dropped. This catches the same receipt uploaded twice to SharePoint.
|
|
@@ -66,6 +69,7 @@ Credit Card Receipts/
|
|
|
66
69
|
8. **Pass 2 — Move** — for each pending rename:
|
|
67
70
|
- If base name appears more than once, assign `_a`, `_b`, `_c`... suffix to **all** colliding files (including the first)
|
|
68
71
|
- PATCH SharePoint to rename + move to `Archive/` (flat — no person subfolder)
|
|
72
|
+
- **Multi-entry docx expansion** — a `.docx` whose `extractDocxData()` produced multiple `line_items[]` (e.g. a subway-rides document with 14 dated trip entries) is expanded into one Excel row per entry
|
|
69
73
|
8. **Excel generation** — one `.xlsx` per person (PhpSpreadsheet); columns: Row #, Account Name, QB Vendor, Payment Amount, Date, Payment Method, Payment Memo, QB Description, Class, Category, Payment Account, Ref No.
|
|
70
74
|
9. **SharePoint upload** — each Excel uploaded to root-level `Credit Card Receipts/3. QB Excel/` via Graph API PUT (shared folder for all persons)
|
|
71
75
|
10. **Summary email** — sent with Excel files attached; To: Jheanelle, CC: Magdalena, BCC: devteam@togatech.com
|
|
@@ -83,6 +87,28 @@ Amex billing cycle runs 3rd-to-3rd. `computeRefNo()` returns the cycle-end date
|
|
|
83
87
|
- Charge on or before the 3rd → cycle ends on the 3rd of the same month
|
|
84
88
|
- Charge after the 3rd → cycle ends on the 3rd of the next month
|
|
85
89
|
|
|
90
|
+
### Billing statement structure (Amex xlsx)
|
|
91
|
+
|
|
92
|
+
The Amex statement Emily submits is a **multi-sheet** xlsx — one sheet per weekly billing
|
|
93
|
+
period (e.g. `"05.04.2026 - 05.11.2026"`, `"05.11.2026 - 05.18.2026"`, …). `loadStatementExcel()`
|
|
94
|
+
must iterate **`getAllSheets()`** and accumulate rows from every sheet — using `getActiveSheet()`
|
|
95
|
+
reads only sheet 0 and silently drops charges that live on later sheets.
|
|
96
|
+
|
|
97
|
+
Each sheet's columns: `Date | Receipt | Notes | Description | Amount | Extended Details |
|
|
98
|
+
Appears On Your Statement As | Address | City/State | Zip Code | Country | Reference | Category`.
|
|
99
|
+
|
|
100
|
+
- The **`Receipt` column (col B)** is a human-readable label Emily fills in to group charges
|
|
101
|
+
(e.g. `"1"`, `"2"`, `"Subway Rides"`). All transit charges (`NYCT PAYGO`) get the label
|
|
102
|
+
`"Subway Rides"` across all sheets. `loadStatementExcel()` must detect and capture this column.
|
|
103
|
+
- `lookupStatementTotalByVendor()` resolves a statement total for a vendor in two passes:
|
|
104
|
+
1. **Receipt-label sum (first)** — sum the `Amount` of **all** rows whose `Receipt` label
|
|
105
|
+
contains the vendor keyword. Transit is one `$3.00` row per ride (e.g. 14 rows tagged
|
|
106
|
+
`"Subway Rides"`), so the total is the **sum** ($42.00), not a single row.
|
|
107
|
+
2. **Description-keyword match (fallback)** — for non-transit vendors, match the keyword
|
|
108
|
+
against the `Description` column. (The description for subway is `"AplPay NYCT PAYGO
|
|
109
|
+
NEW YORK"`, which contains no `"subway"` — so the receipt-label pass is what makes
|
|
110
|
+
transit resolve at all.)
|
|
111
|
+
|
|
86
112
|
### QB vendor matching
|
|
87
113
|
`TowFoundationVendors.php` holds 2,994 vendor names exported verbatim from Tow Foundation's
|
|
88
114
|
QuickBooks vendor list (June 2026). `matchQbVendor()` runs three passes against this list:
|
|
@@ -180,8 +206,9 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
|
|
|
180
206
|
exists at the root of `Credit Card Receipts/` alongside the person folders.
|
|
181
207
|
|
|
182
208
|
- **Person folder name normalization** — SharePoint folders are named `"Brent Peterkin CC receipts"` but `CLASS_MAP` / `PAYMENT_ACCOUNT_MAP` keys are just `"Brent Peterkin"`. The ` CC receipts` suffix is stripped via regex in `walkReceiptsFolder()`. Without this, Class and Payment Account columns are blank for those persons.
|
|
209
|
+
- **Archive paths must use the actual SharePoint folder name, not the normalized name** — the normalized person name (` CC receipts` stripped, spaces replaced) is for **map lookups only**. When building the SharePoint archive path, use the *actual* folder name (e.g. `"Emily Tow CC receipts"`), not the normalized `"Emily Tow"` — otherwise the PATCH move 404s because the path segment does not exist. Keep the normalized name and the real folder name as separate values.
|
|
183
210
|
- **`billingCycle` filter is suffix-match, not exact** — always pass just the date portion (`"06-03-2026"`), not the full folder name. Passing the full name (`"Amex ending in 06-03-2026"`) also works but would miss Mastercard folders.
|
|
184
|
-
- **
|
|
211
|
+
- **Vendors not in QB vendor list** — as of June 2026 these vendors are not in Tow Foundation's QuickBooks, so `qb_vendor` is blank for their charges until the client adds them to QB and the vendor list is refreshed: **Ole Mole**, **AMORE PIZZA CAFE**, **Green & Tonic (New Canaan)**, **NCFP (National Center for Philanthropy)**, **Rippling**, **Langan's**. This is a client action, not a code fix.
|
|
185
212
|
- **Duplicate detection is within-run only** — if the same receipt was processed in a prior run and archived, it won't be caught. The dedup only covers receipts present in the current run's `$pendingRenames`.
|
|
186
213
|
- **Unmapped AI categories pass through** — if the AI returns a category string not in `TowFoundationCategories.php`, `mapCategory()` logs a warning and writes the raw AI string to the Excel. Check error logs after a run if the Category column looks odd; add the new value to the map file to fix it.
|
|
187
214
|
- **Short vendor acronyms (< 4 chars) require whole-word match** — the substring pass uses a word-boundary regex for needles under 4 chars to avoid false positives (e.g. `"UPS"` inside `"USPS"`). If a short vendor name is not matching, check that the QB vendor list entry starts or ends with the acronym as a whole word.
|
|
@@ -189,6 +216,7 @@ Fatal errors send only to `NOTIFY_EMAIL_DEV` (no CC/BCC).
|
|
|
189
216
|
|
|
190
217
|
## Change history
|
|
191
218
|
|
|
219
|
+
- 2026-06-25 — Statement parsing hardened for Emily Tow's May 2026 Amex xlsx: `loadStatementExcel()` now iterates `getAllSheets()` (4 weekly-period sheets) instead of `getActiveSheet()` and captures the `Receipt` column; `lookupStatementTotalByVendor()` now **sums** all receipt-label-matched rows (14 × $3.00 "Subway Rides" = $42.00) before falling back to description-keyword match — fixes subway totals reading $0/$9.00. Added `extractDocxData()` so `.docx` receipts bypass Talos (Talos 500s on docx), parsing `MM/DD/YY: description` line items into per-entry Excel rows. Added a year guard clamping AI-hallucinated `invoice_date` years (e.g. 1976, 2028) to the current year. Fixed archive-path bug: SharePoint move now uses the actual folder name (`"Emily Tow CC receipts"`) not the normalized name, fixing 404 on archive moves. 5 more vendors found missing from the QB vendor list (client action). (rgirish)
|
|
192
220
|
- 2026-06-23 — `TowFoundationCategories.php` added: AI category values now mapped to exact QB account strings via `mapCategory()`; `"Transportation"` → `"6610 Travel expense"` etc. (19 mappings). QB vendor substring match minimum lowered from 4 to 3 chars with whole-word boundary guard for short needles — fixes `"CVS"` → `"CVS Pharmacy"`, `"MTA"` → `"MTA Metro card"`, prevents `"UPS"` → `"USPS"` false positive. (rgirish)
|
|
193
221
|
- 2026-06-18 — QB vendor matching via local fuzzy match against `TowFoundationVendors.php` (2,994 vendors); AI category prompt updated to return specific types (Dining, Travel, etc.); duplicate receipt detection added (person+date+vendor+amount dedup); `billingCycle` filter parameter added (suffix-match — card-type agnostic); person folder ` CC receipts` suffix stripped so CLASS_MAP/PAYMENT_ACCOUNT_MAP resolve correctly (rgirish)
|
|
194
222
|
- 2026-06-12 — QB Excel upload moved to root-level `3. QB Excel/`; Archive/exception folders flattened (no person subfolder); unsupported mime types (`application/octet-stream`) now throw ReceiptProcessingException instead of being silently skipped (rgirish)
|
package/package.json
CHANGED