toga-ai 1.0.144 → 1.0.146
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/dbchanges2/architecture.md +32 -2
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +31 -2
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +16 -0
- package/knowledge/INDEX.md +1 -0
- package/knowledge/clients/aig/INDEX.md +6 -0
- package/knowledge/clients/aig/features/entitlement-intake.md +131 -0
- package/knowledge/clients/aig/profile.md +40 -0
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-06-19
|
|
10
|
+
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
13
13
|
- Client/
|
|
@@ -141,6 +141,36 @@ file inside `HISTORIC` running; **never** add new changes there.
|
|
|
141
141
|
5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
|
|
142
142
|
work in a `HISTORIC` folder.
|
|
143
143
|
|
|
144
|
+
## Bulk data loads — one statement, not many
|
|
145
|
+
|
|
146
|
+
When a change inserts many rows (reference-data / code-table loads), write it as a **single
|
|
147
|
+
bulk `INSERT`**, not one `INSERT` statement per row. The database is billed per query and the
|
|
148
|
+
external executor runs each statement as its own round-trip, so N separate inserts cost N times
|
|
149
|
+
the round-trips (and query cost) of one batched statement. This is a standing team preference.
|
|
150
|
+
|
|
151
|
+
- Use one `INSERT ... VALUES (...),(...),...` or `INSERT ... SELECT` over a derived
|
|
152
|
+
(`UNION ALL`) row set.
|
|
153
|
+
- To stay **idempotent** without a unique key to `INSERT IGNORE` against, anti-join the source
|
|
154
|
+
set to the target and insert only the misses:
|
|
155
|
+
|
|
156
|
+
```sql
|
|
157
|
+
INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
|
|
158
|
+
SELECT
|
|
159
|
+
src.uuid,
|
|
160
|
+
src.keyColumn,
|
|
161
|
+
src.otherColumn
|
|
162
|
+
FROM (
|
|
163
|
+
SELECT '<uuid>' AS uuid, '<key>' AS keyColumn, '<value>' AS otherColumn
|
|
164
|
+
UNION ALL SELECT ...
|
|
165
|
+
) AS src
|
|
166
|
+
LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
|
|
167
|
+
WHERE
|
|
168
|
+
existing.id IS NULL;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- Keep the single statement well under `max_allowed_packet` (64 MB default) — a few thousand
|
|
172
|
+
rows is comfortably fine.
|
|
173
|
+
|
|
144
174
|
## Relationship to the rest of 2.0
|
|
145
175
|
|
|
146
176
|
`dbchanges2` is registered as a **2.0 core repo** (`role: core` in `registry.json`) — it is
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-06-18
|
|
10
|
+
owners: [jcardinal, dfranks]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
13
13
|
- worker2/Controller/Index.php
|
|
@@ -126,6 +126,31 @@ curl -X POST https://worker.togahub.com/ \
|
|
|
126
126
|
|
|
127
127
|
Use realistic placeholder values, not empty strings/nulls.
|
|
128
128
|
|
|
129
|
+
## How exceptions & retries work
|
|
130
|
+
|
|
131
|
+
The EB worker dispatcher (`Controller/Index.php::worker()`) wraps your action call in
|
|
132
|
+
`try { } catch (\Throwable $e)`:
|
|
133
|
+
- **Returns normally** → `WorkerJobs.isSuccess = 1`, `dtCompleted` set.
|
|
134
|
+
- **Throws** (any `Throwable`, incl. `Error`) → `isSuccess = 0`, `failureReason` = message +
|
|
135
|
+
`file:line` + full stack trace. (Two earlier phases — DB-logs registration and `initialize()` —
|
|
136
|
+
fail the same way.)
|
|
137
|
+
|
|
138
|
+
Crucially, the worker **always returns HTTP 200, even on a throw**, so SQS deletes the message
|
|
139
|
+
immediately. **There is no DLQ and no automatic retry.** A throw makes the failure *visible* (a
|
|
140
|
+
failed `WorkerJobs` row to alert on) — it does **not** re-run the job.
|
|
141
|
+
|
|
142
|
+
So **do not design "throw so it retries" — it won't.** To actually retry / self-heal:
|
|
143
|
+
- **Re-enqueue** with `_Worker::runTask('Category/Action/Method', [...])` — inserts a fresh
|
|
144
|
+
`WorkerJobs` row + SQS message, so it genuinely re-runs. Carry your own **attempt counter** in the
|
|
145
|
+
parameters to bound it.
|
|
146
|
+
- Or **in-process retry** *within* the method (it's a background worker with a 3600s visibility
|
|
147
|
+
budget — a bounded sleep+retry loop is fine here, unlike a request handler).
|
|
148
|
+
- Or **manual** `_Worker_Infrastructure_Worker::Retry($id)` / the reset-fields SQL, or a periodic
|
|
149
|
+
reconcile cron as a backstop.
|
|
150
|
+
|
|
151
|
+
Rule of thumb: `throw` to surface a genuine failure; `runTask`/in-process loop when the work should
|
|
152
|
+
be reattempted.
|
|
153
|
+
|
|
129
154
|
## Gotchas
|
|
130
155
|
|
|
131
156
|
- Class must be **`abstract`** and methods **`public static`** or routing fails.
|
|
@@ -135,4 +160,8 @@ Use realistic placeholder values, not empty strings/nulls.
|
|
|
135
160
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
136
161
|
|
|
137
162
|
## Change history
|
|
163
|
+
- 2026-06-18 — Documented worker-action **exception/retry semantics**: dispatcher catches `Throwable` →
|
|
164
|
+
`isSuccess=0`+`failureReason`, **always HTTP 200, no DLQ, no auto-retry**; throwing surfaces a failure
|
|
165
|
+
but does not re-run — retry via `_Worker::runTask` re-enqueue (attempt-counter), in-process loop, or
|
|
166
|
+
manual `Retry()`. (dfranks)
|
|
138
167
|
- 2026-06-08 — Documented the worker-action contract (path→class/method convention, parameter typing, webhook/cron templates); replaces the former `/worker2-action` skill. (jcardinal)
|
|
@@ -147,6 +147,17 @@ Shared helpers: `buildCustomFields()` (the field array, used by both create and
|
|
|
147
147
|
it on the payload; the new contract drops it).
|
|
148
148
|
- Every ClickUp `_ApiRequest` calls `setLogging(self::CLICKUP_API_LOGGING_ENABLED=false)`, so ClickUp
|
|
149
149
|
traffic is not written to the Logs DB.
|
|
150
|
+
- **Two `_Component_Api_Netsuite` classes exist — edit the right one.** worker2 calls the **global**
|
|
151
|
+
`_Component_Api_Netsuite`, autoloaded from `_underscore/Component/Api/Netsuite/Netsuite.php`; `api2`
|
|
152
|
+
ships a *separate* `namespace api` copy (`\api\_Component_Api_Netsuite`) for its own use. Editing the
|
|
153
|
+
api2 copy has **no effect** on the worker2 webhook integration — change the `_underscore` one.
|
|
154
|
+
- **NetSuite REST calls are NOT logged to `Logs.Api`.** `_Component_Api_Netsuite::send()` calls
|
|
155
|
+
`setLogging(false)`, so unlike platform traffic and ClickUp/Freshservice/Sentry (which land in the
|
|
156
|
+
logs-cluster `Logs.Api` with full request + `responsePayload`), NetSuite request/response bodies are
|
|
157
|
+
not captured — you can't pull a historical NetSuite response for debugging. That cost us the response
|
|
158
|
+
bodies when diagnosing the [open-orders read-after-write lag](./netsuite-salesorder-open-orders-sync.md);
|
|
159
|
+
removing that `setLogging(false)` is under review (handle the 20–50 KB payload volume the
|
|
160
|
+
prod-appropriate way — retention/sampling/summary — not by defaulting logging off).
|
|
150
161
|
|
|
151
162
|
## Data model
|
|
152
163
|
|
|
@@ -306,6 +317,11 @@ same **skip-if-unchanged** compare on the extracted values, and **actor-identity
|
|
|
306
317
|
trigger a CU→NS write. The NS→CU change-detection above is the complementary backstop, not a substitute.
|
|
307
318
|
|
|
308
319
|
## Change history
|
|
320
|
+
- 2026-06-18 — Documented two NetSuite-client gotchas: **two `_Component_Api_Netsuite` copies** (worker2
|
|
321
|
+
uses the global `_underscore` one; api2 has a separate `namespace api` copy — edit the right one), and
|
|
322
|
+
`send()`'s `setLogging(false)` means **NetSuite calls aren't logged to `Logs.Api`** (no historical
|
|
323
|
+
response bodies; removal under review). Also dropped the debunked dev-laptop rationale from the
|
|
324
|
+
ClickUp-logging note. (dfranks)
|
|
309
325
|
- 2026-06-18 — **Diagnosed the live opportunity-import gap as a deployment misconfig + characterized the
|
|
310
326
|
AMQ burst behavior.** The Opportunity `ue_amq_enqueue` deployment was `Released` but with an **empty
|
|
311
327
|
audience** (~4:41–5:39 pm) so it enqueued nothing; missing opps had no queue row and no worker2 job
|
package/knowledge/INDEX.md
CHANGED
|
@@ -34,6 +34,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
34
34
|
|
|
35
35
|
## Clients
|
|
36
36
|
|
|
37
|
+
- **AIG (Staples Protection Plan)** (`aig`) → [clients/aig/INDEX.md](clients/aig/INDEX.md)
|
|
37
38
|
- **Compass Canada** (`compass-canada`) → [clients/compass-canada/INDEX.md](clients/compass-canada/INDEX.md)
|
|
38
39
|
- **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
|
|
39
40
|
- **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Client: AIG (Staples Protection Plan) `aig`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | AIG protection-plan entitlements arrive as `api2` V2 JSON POSTs. | _underscore/Model/Aig/Entitlement.php, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql |
|
|
6
|
+
| [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: AIG Entitlement Intake & SaleItem Code Resolution
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: API
|
|
5
|
+
client: aig
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-06-19
|
|
9
|
+
owners: ["mhammontree"]
|
|
10
|
+
files:
|
|
11
|
+
- _underscore/Model/Aig/Entitlement.php
|
|
12
|
+
- dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
|
|
13
|
+
related:
|
|
14
|
+
- 2.0/apps/api2/architecture.md
|
|
15
|
+
- 2.0/apps/dbchanges2/architecture.md
|
|
16
|
+
- 2.0/apps/_underscore/architecture.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
AIG protection-plan entitlements arrive as `api2` V2 JSON POSTs. Before the entitlement is
|
|
22
|
+
written, the `_Model_Aig_Entitlement::prePost` interceptor resolves the **sold warranty SKU**
|
|
23
|
+
(`saleItem.partNumber`) against `Client_Aig.Items` to set the `Entitlement.saleItemId` foreign
|
|
24
|
+
key and to attach fulfillment types/methods. If that part number has no matching `Items` row,
|
|
25
|
+
the FK can't resolve and intake fails with **"Missing AIG item ID."** Keeping
|
|
26
|
+
`Client_Aig.Items` populated with AIG's current sale-item catalog is what keeps intake working.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
|
|
30
|
+
- **`_underscore/Model/Aig/Entitlement.php`** — `prePost(&$api, &$payload)` is the PRE-POST
|
|
31
|
+
API payload interceptor (named by the `prePost*` convention) that runs on entitlement intake.
|
|
32
|
+
- **`Client_Aig.Items`** — the catalog the interceptor looks up against (`partNumber` column).
|
|
33
|
+
- **`dbchanges2/Client_Aig/…`** — where new code batches are loaded (see *Uploading new codes*).
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
1. AIG POSTs an entitlement payload (vendor `STS_001`) to the `api2` V2 API.
|
|
38
|
+
2. `_Model_Aig_Entitlement::prePost` runs and:
|
|
39
|
+
- cleans a duplicated-firstName-in-lastName quirk on `contact`;
|
|
40
|
+
- if `payload->item` (the covered **device/unit**) has a `description` but no `itemCategory`,
|
|
41
|
+
sets `itemCategory.name = item.description`;
|
|
42
|
+
- reads `partNumber = payload->saleItem->partNumber` and runs
|
|
43
|
+
`SELECT id FROM Items WHERE partNumber = '{partNumber}' LIMIT 1` to get `$itemId`;
|
|
44
|
+
- uses `$itemId` to load fulfillment **types** (`Items_EntitlementFulfillmentTypes`) and
|
|
45
|
+
**methods** (`Items_EntitlementFulfillmentMethods`) and injects them into the payload.
|
|
46
|
+
3. The V2 engine resolves `Entitlement.saleItemId` (FK → `_Model_Client_Item`) from that same
|
|
47
|
+
`saleItem.partNumber`. **No matching `Items` row → unresolvable FK → "Missing AIG item ID".**
|
|
48
|
+
4. `postPost` then emails the contact a Staples Protection Plan registration link
|
|
49
|
+
(`staplesprotection.togaview.com`).
|
|
50
|
+
|
|
51
|
+
## Data model
|
|
52
|
+
|
|
53
|
+
`Client_Aig.Items` is **dual-purpose** — it holds two different kinds of row, distinguished by
|
|
54
|
+
whether `itemCategoryId` is set:
|
|
55
|
+
|
|
56
|
+
| Row kind | Example `partNumber` | `description` | `itemCategoryId` |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| **Sale item** (warranty SKU) | `ASI-2YG2`, `SP-2D-TAB3` | price-band plan, e.g. `2YR Product under $500: ($30-$49.99)` | **NULL** |
|
|
59
|
+
| **Unit/device item** | `157C`, `1159`, `5022` | device type, e.g. `Chromebook`, `Printer Inkjet` | set → `ItemCategories` |
|
|
60
|
+
|
|
61
|
+
- `Client_Aig.ItemCategories` is a **device-classification** table (`Laptop`, `Printer`,
|
|
62
|
+
`Tablet`) — it classifies the physical hardware (unit items), **not** the warranty sale
|
|
63
|
+
items. Sale items correctly carry `itemCategoryId = NULL`; do not try to assign them a
|
|
64
|
+
category (the price-band plan descriptions don't map to a device type).
|
|
65
|
+
- `ItemCategories.c_premiumTechSupportCategory` (enum `PC`/`Tablet`) is read by the AIG
|
|
66
|
+
ClosedClaims → NetSuite invoicing sync.
|
|
67
|
+
|
|
68
|
+
### SaleItem code schemes
|
|
69
|
+
|
|
70
|
+
AIG migrated their sale-item coding. The catalog spans:
|
|
71
|
+
- **Legacy `SP-*`** — the original scheme (only a handful seeded historically).
|
|
72
|
+
- **New `ASI-*` / `SM-*`** — current STS_001 (Staples) scheme; the bulk of the catalog.
|
|
73
|
+
- **Numeric `SA` codes** (8-digit, e.g. `24664377`) — a separate program sheet in AIG's
|
|
74
|
+
spreadsheet, distinct part-number space (no collisions with the STS codes).
|
|
75
|
+
|
|
76
|
+
## Uploading new codes (the recurring task)
|
|
77
|
+
|
|
78
|
+
When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Description`):
|
|
79
|
+
|
|
80
|
+
1. Load it into `Client_Aig.Items` via a **`dbchanges2/Client_Aig/`** migration (dated
|
|
81
|
+
`YYYY-MM-DD<letter>` per the dbchanges2 contract).
|
|
82
|
+
2. Insert minimally `uuid`, `partNumber`, `description` — everything else (`isActive`,
|
|
83
|
+
`isVisible`, `inventoryType=HYBRID`, timestamps) takes table defaults; **leave
|
|
84
|
+
`itemCategoryId` NULL** for sale items.
|
|
85
|
+
3. Generate **fully-random UUIDs** (e.g. `uuid4`), never the time-based MySQL `UUID()` —
|
|
86
|
+
per the 2.0 standard. Bake literal UUIDs into the SQL since a migration can't call PHP's
|
|
87
|
+
`_String::generateUuid()`.
|
|
88
|
+
4. Use a **single bulk `INSERT`** (see the dbchanges2 architecture rule), made idempotent with
|
|
89
|
+
an anti-join because `Items` has **no unique key on `partNumber`** (so `INSERT IGNORE`
|
|
90
|
+
can't help):
|
|
91
|
+
```sql
|
|
92
|
+
INSERT INTO Items (uuid, partNumber, description)
|
|
93
|
+
SELECT src.uuid, src.partNumber, src.description
|
|
94
|
+
FROM ( SELECT '<uuid>' AS uuid, '<part>' AS partNumber, '<desc>' AS description
|
|
95
|
+
UNION ALL SELECT ... ) AS src
|
|
96
|
+
LEFT JOIN Items existing ON existing.partNumber = src.partNumber
|
|
97
|
+
WHERE existing.id IS NULL;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Client variations
|
|
101
|
+
|
|
102
|
+
This is AIG-specific behavior (the `_Model_Aig_Entitlement` override); other clients do not run
|
|
103
|
+
this interceptor or use this dual-purpose Items pattern.
|
|
104
|
+
|
|
105
|
+
## Gotchas / known issues
|
|
106
|
+
|
|
107
|
+
- **"Missing AIG item ID" = the sale-item code isn't in `Client_Aig.Items`.** This is the
|
|
108
|
+
classic symptom of AIG shipping new codes before the catalog is loaded. Fix = load the codes.
|
|
109
|
+
- **The `partNumber` lookup is raw-interpolated** (`WHERE partNumber = '{$partNumber}'`) — a
|
|
110
|
+
latent SQL-injection point in `_Model_Aig_Entitlement::prePost`. Payload data is partner-
|
|
111
|
+
supplied; should be escaped with `_Database::escape()` or parameterized. Not yet fixed
|
|
112
|
+
(TRUE-79534 was data-load only).
|
|
113
|
+
- **The lookup is not vendor-scoped** (`LIMIT 1` on `partNumber` alone). It relies on
|
|
114
|
+
`partNumber` being globally unique within `Items`; safe today because the code spaces don't
|
|
115
|
+
collide, but a future collision would silently resolve to the wrong item.
|
|
116
|
+
- **Unit/device item codes are a separate source.** The entitlement payload also carries
|
|
117
|
+
`entitlementUnits[].unit.item.partNumber` (e.g. `1167` "Tablet Accessories"); those device
|
|
118
|
+
codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
|
|
119
|
+
- **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
|
|
120
|
+
`INSERT IGNORE`.
|
|
121
|
+
|
|
122
|
+
## Change history
|
|
123
|
+
|
|
124
|
+
- 2026-06-19 — Documented intake flow; loaded 2,200 new SaleItem codes (1,700 STS `ASI-*`/`SM-*`
|
|
125
|
+
+ 500 unique numeric `SA`) into `Client_Aig.Items` via TRUE-79534, fixing "Missing AIG item ID"
|
|
126
|
+
on the new `ASI-*` scheme. Follow-up to TRUE-79441. (mhammontree)
|
|
127
|
+
|
|
128
|
+
## Related docs
|
|
129
|
+
|
|
130
|
+
- [api2 architecture](../../../2.0/apps/api2/architecture.md) — the V2 intake engine + payload interceptors.
|
|
131
|
+
- [dbchanges2 architecture](../../../2.0/apps/dbchanges2/architecture.md) — migration naming + the bulk-insert rule.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "AIG (Staples Protection Plan)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
apps:
|
|
5
|
+
- _underscore
|
|
6
|
+
- api2
|
|
7
|
+
- dbchanges2
|
|
8
|
+
project: API
|
|
9
|
+
client: aig
|
|
10
|
+
type: profile
|
|
11
|
+
status: active
|
|
12
|
+
updated: 2026-06-19
|
|
13
|
+
owners: ["mhammontree"]
|
|
14
|
+
files: []
|
|
15
|
+
related:
|
|
16
|
+
- clients/aig/features/entitlement-intake.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. In the
|
|
22
|
+
2.0 platform AIG is a database-level tenant (`Client_Aig` schema) with its own `_Model_Aig_*`
|
|
23
|
+
class overrides in `_underscore` (most pulling in NetSuite traits). Protection-plan
|
|
24
|
+
**entitlements** are pushed to TOGa via the `api2` V2 JSON API; on intake an interceptor
|
|
25
|
+
resolves the sold warranty SKU against `Client_Aig.Items`, and closed claims are invoiced
|
|
26
|
+
back to AIG through NetSuite.
|
|
27
|
+
|
|
28
|
+
The intake vendor on entitlement payloads is `STS_001` ("STS" = Staples); registration emails
|
|
29
|
+
go out under the Staples Protection Plan brand and link to `staplesprotection.togaview.com`.
|
|
30
|
+
|
|
31
|
+
## Integration touchpoints
|
|
32
|
+
|
|
33
|
+
- **`api2`** — entitlement intake (`/v2` JSON), where the AIG payload is POSTed.
|
|
34
|
+
- **`_underscore`** — `_Model_Aig_*` overrides (Entitlement intake interceptor, Contact sync,
|
|
35
|
+
NetSuite item/SO/invoice traits).
|
|
36
|
+
- **`dbchanges2`** — `Client_Aig/` schema + reference-data migrations (e.g. the SaleItem code
|
|
37
|
+
catalog in `Client_Aig.Items`).
|
|
38
|
+
|
|
39
|
+
See [entitlement-intake.md](features/entitlement-intake.md) for how a payload becomes an
|
|
40
|
+
entitlement and how to load new sale-item codes.
|
package/package.json
CHANGED