toga-ai 1.0.214 → 1.0.216
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 -1
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +40 -1
- package/knowledge/2.0/apps/worker2/INDEX.md +2 -0
- package/knowledge/2.0/apps/worker2/features/etilize-catalog-item-import.md +105 -0
- package/knowledge/2.0/apps/worker2/features/etilize-item-translation-import.md +69 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/profile.md +2 -1
- package/knowledge/clients/quad/profile.md +3 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql |
|
|
7
7
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
8
8
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
9
|
-
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
9
|
+
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). | _underscore/Component/Forecast/SaleImport/SaleImport.php, _underscore/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Invoice.php, worker2/Worker/Netsuite/CashSale.php, worker2/Worker/Netsuite/CreditMemo.php, worker2/Worker/Netsuite/CashRefund.php, worker2/Worker/Netsuite/JournalEntry.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/SalesOrder.php, dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql, test/@dave/test_invoice_lifecycle.php, test/@dave/test_je_lifecycle.php, test/@dave/test_creditmemo_lifecycle.php, test/@dave/test_cashsale_lifecycle.php, test/@dave/test_cashrefund_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js |
|
|
10
10
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php |
|
|
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 |
|
|
@@ -21,6 +21,9 @@ files:
|
|
|
21
21
|
- dbchanges2/Forecast/2026-06-26a - Add journalEntry to Sales transaction type enum.sql
|
|
22
22
|
- test/@dave/test_invoice_lifecycle.php
|
|
23
23
|
- test/@dave/test_je_lifecycle.php
|
|
24
|
+
- test/@dave/test_creditmemo_lifecycle.php
|
|
25
|
+
- test/@dave/test_cashsale_lifecycle.php
|
|
26
|
+
- test/@dave/test_cashrefund_lifecycle.php
|
|
24
27
|
- test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js
|
|
25
28
|
- test/@dave/NetSuite/api-message-queue/dev_ue_api_msg_queue_enqueue.js
|
|
26
29
|
related:
|
|
@@ -71,7 +74,9 @@ Per-record flow in `sync`:
|
|
|
71
74
|
1. REST GET the record (`expandSubResources=true`) via `_Component_Forecast_Db::fetchRecord`.
|
|
72
75
|
2. **Per-type status gate** → if excluded, call `removeAll` and stop. Exclusions: invoice
|
|
73
76
|
`Voided`/`Rejected`; cashSale `Unapproved Payment`; creditMemo `Voided`; cashRefund has
|
|
74
|
-
no gate.
|
|
77
|
+
no gate. **A cashSale created via REST lands in status `Deposited`** (NOT the excluded
|
|
78
|
+
`Unapproved Payment`), so REST-created cash sales pass the gate and import — relevant when
|
|
79
|
+
building cashSale test fixtures.
|
|
75
80
|
3. If `shippingCost != 0`, append a **synthetic SHIPPING line at lineNumber 0** using
|
|
76
81
|
NetSuite item internalId **13500** (a real `Forecast.Items` row).
|
|
77
82
|
4. Per line: resolve the item (with self-heal — see below); skip `itemGroup` items
|
|
@@ -195,6 +200,14 @@ events therefore require this one script deployed on **Invoice, Cash Sale, Credi
|
|
|
195
200
|
Cash Refund**. The drainer (`customscript_ss_amq_drain`) is record-type-agnostic and needs
|
|
196
201
|
no per-type deployment.
|
|
197
202
|
|
|
203
|
+
**To make a UE enqueuer fire for a REST/M2M-driven record, the deployment's AUDIENCE must
|
|
204
|
+
include that integration user/role.** The **"Execute As Role"** field does NOT control
|
|
205
|
+
triggering — it only sets the run-as role; changing it has no effect on whether the webhook
|
|
206
|
+
fires. Adjusting the audience is what made create/edit/delete events fire for REST-created
|
|
207
|
+
test records. Symptom of a misconfigured audience (or a missing per-type deployment): **no
|
|
208
|
+
AMQ queue row is created at all** — SuiteQL the AMQ custom record shows 0, nothing at ngrok,
|
|
209
|
+
nothing at prod `Logs.Webhook`.
|
|
210
|
+
|
|
198
211
|
The enqueuer's `RECORD_TYPE_MAP` (lowercase NS `record.type` → camelCase `recordType` the
|
|
199
212
|
worker2 router PascalCases) maps the four sale types 1:1, plus
|
|
200
213
|
`'journalentry':'journalEntry'` (added this session to both the prod and dev enqueuers); a
|
|
@@ -222,6 +235,11 @@ Actions: `check | create | get | update | delete | sync | recent | amq | deploym
|
|
|
222
235
|
[REST client doc](./netsuite-rest-client.md)); the framework was **not** edited this session.
|
|
223
236
|
- `amq` action SuiteQLs the AMQ custom record; `deployments` SuiteQLs `scriptdeployment`+`script`
|
|
224
237
|
to show which enqueuer fires per record type (used to find the dual-deployment trap below).
|
|
238
|
+
- **Per-type siblings** `test_creditmemo_lifecycle.php`, `test_cashsale_lifecycle.php`,
|
|
239
|
+
`test_cashrefund_lifecycle.php` (and `test_je_lifecycle.php`) mirror this harness, each with its
|
|
240
|
+
own `create()` POSTing via `_ApiRequest` with the `~`-delimited Location regex workaround.
|
|
241
|
+
**cashRefund and creditMemo require a header-level location** on create. The create webhook can
|
|
242
|
+
lag **~20s** before the row appears (read-after-write/processing) — wait before asserting.
|
|
225
243
|
|
|
226
244
|
## Gotchas / known issues
|
|
227
245
|
- **Testing locally pollutes PROD unless you UNDEPLOY the prod AMQ enqueuer first.** Each sale
|
|
@@ -238,6 +256,16 @@ Actions: `check | create | get | update | delete | sync | recent | amq | deploym
|
|
|
238
256
|
for the test id). The **dev** enqueuer's `debugWrap` envelope
|
|
239
257
|
`{action,parameters:{payload,headers}}` is required because a local worker2 has no
|
|
240
258
|
`WebhookIngestion` Lambda to wrap the raw body.
|
|
259
|
+
- **The legacy 5-min pull cron can leak a live test record into PROD `Forecast.Sales`
|
|
260
|
+
regardless of whether the new webhook code is deployed.** Because all testing creates real
|
|
261
|
+
(tiny, ±$0.01) transactions in the *single* prod NetSuite account, if a test record is
|
|
262
|
+
still alive when `worker/crons/toga2/forecast2/import_sales.php` runs (every 5 min), the
|
|
263
|
+
cron pulls it into prod `Forecast.Sales` — this is independent of the webhook (prod has no
|
|
264
|
+
Sales handler deployed). Observed repeatedly across test batches; each leaked row was an
|
|
265
|
+
exact test match (customerId 2 / item 25 / revenue ±0.01) and was removed from the prod
|
|
266
|
+
writer with a guarded `DELETE`. **Mitigations:** keep the create→delete window short (most
|
|
267
|
+
records slip between cron runs), or pause `import_sales` during a batch; the daily
|
|
268
|
+
discrepancy-fix cleans any straggler anyway since the record is deleted in NetSuite.
|
|
241
269
|
- **No local app logs for debug-path webhooks — verify arrival via the ngrok inspector, and decode
|
|
242
270
|
the body.** The legacy `{action,parameters}` debug path writes **no** `WorkerJobs`/`Logs.Webhook`
|
|
243
271
|
locally. Confirm inbound arrival at the ngrok request inspector
|
|
@@ -259,6 +287,17 @@ Actions: `check | create | get | update | delete | sync | recent | amq | deploym
|
|
|
259
287
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
260
288
|
|
|
261
289
|
## Change history
|
|
290
|
+
- 2026-06-26 — **Hardened + verified the full webhook path for all four sale types end-to-end**
|
|
291
|
+
(create→insert, edit→update, delete→removeAll) with sign conventions reconfirmed (invoice/cashSale
|
|
292
|
+
+, creditMemo/cashRefund −). Recorded durable gotchas: a UE enqueuer fires for a REST/M2M user only
|
|
293
|
+
when the deployment's **AUDIENCE** includes that user — **"Execute As Role" does NOT control
|
|
294
|
+
triggering** (no-AMQ-row is the symptom); a **REST-created cashSale lands in status `Deposited`**, not
|
|
295
|
+
the excluded `Unapproved Payment`, so it imports; the create webhook can lag **~20s**. Sharpened the
|
|
296
|
+
prod-leak note: the legacy 5-min `import_sales.php` cron pulls a live test record into prod
|
|
297
|
+
`Forecast.Sales` independent of webhook deployment (exact ±$0.01 test matches deleted from the prod
|
|
298
|
+
writer; mitigate by short create→delete windows / pausing the cron). Added per-type harnesses
|
|
299
|
+
`test_{creditmemo,cashsale,cashrefund}_lifecycle.php`. JE php-reviewer hardening applied
|
|
300
|
+
(partner-reversal fetch isolated in try/catch, null-safe SuiteQL `items[0]`). (dfranks)
|
|
262
301
|
- 2026-06-26 — **Extended the importer to a fifth transaction type, `journalEntry`** (GL
|
|
263
302
|
revenue/cost adjustments). New thin handler `_Worker_Netsuite_JournalEntry`
|
|
264
303
|
(post/put→`syncJournalEntry`, delete→`removeAllJournalEntry`); engine gained
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
| [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
|
|
9
9
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
10
10
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
11
|
+
| [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
|
|
12
|
+
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
11
13
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Notification/Email.php, dbchanges2/Core/2026-05-21 - Monitors.sql |
|
|
12
14
|
| [NetSuite → TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
13
15
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Etilize Catalog Item Import & Refresh
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Etilize/Items.php
|
|
13
|
+
related:
|
|
14
|
+
- ./creating-worker-actions.md
|
|
15
|
+
- ./etilize-item-translation-import.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. The
|
|
21
|
+
abstract worker class `_Worker_Etilize_Items` ingests an item catalog (items, vendor
|
|
22
|
+
items, costs, prices, personas) and enriches each item with features and images from
|
|
23
|
+
the Etilize datasheet. All three actions write **directly to `DB_CLIENT`** via raw
|
|
24
|
+
`_Query` — there is **no api2 round-trip**. Used today by **Compass Canada** (single
|
|
25
|
+
price) and **Quad** (multi-currency), but nothing in the code is client-specific.
|
|
26
|
+
|
|
27
|
+
## Public actions
|
|
28
|
+
|
|
29
|
+
Routes are `Etilize/Items/<Method>` (see [creating-worker-actions](./creating-worker-actions.md)).
|
|
30
|
+
|
|
31
|
+
1. `ImportItemsFromCsvFile(string $client, string $s3Url, string $catalogUuid)`
|
|
32
|
+
— **normal import**: one `ItemPrice` + `VendorItemCost` per item with **NULL
|
|
33
|
+
currency**, using the Cost / Lease Price columns (e.g. Compass).
|
|
34
|
+
2. `ImportItemsWithCurrencyPricesFromCsvFile(string $client, string $s3Url, string $catalogUuid)`
|
|
35
|
+
— **per-currency import**: each currency-coded column becomes its own
|
|
36
|
+
`ItemPrice` + `VendorItemCost` (e.g. Quad).
|
|
37
|
+
3. `RefreshItemsFromEtilize(string $client, array $items, ?string $catalog = null)`
|
|
38
|
+
where `items = [{partNumber, etilizeId}]` — looks each item up by `partNumber`
|
|
39
|
+
and, only if its stored `c_etilizeProductId` differs from the supplied `etilizeId`,
|
|
40
|
+
updates it and re-pulls the item's features + images from the Etilize datasheet.
|
|
41
|
+
Returns a per-item changed/unchanged summary.
|
|
42
|
+
|
|
43
|
+
## How the import works
|
|
44
|
+
|
|
45
|
+
Both import actions delegate to the shared private
|
|
46
|
+
`runItemCsvImport(..., bool $useCurrencyPrices)` — `useCurrencyPrices` is the only
|
|
47
|
+
difference between the two public entry points.
|
|
48
|
+
|
|
49
|
+
1. **Setup** — resolve `clientId` from `Core.Clients` by `clientIdentifier`, register
|
|
50
|
+
the client DB, and resolve `catalogId` from `catalogUuid` against the `Catalogs` table.
|
|
51
|
+
2. **Download & parse CSV** — pulled from S3 with `_Cloud::getFileFromS3`. **Row 1 =
|
|
52
|
+
headers, row 2 = a field-description row that is skipped, data starts at row 3.**
|
|
53
|
+
Columns are mapped **by header name** via case-insensitive regex patterns, so clients
|
|
54
|
+
whose columns are in a **different order** both work (Compass vs. Quad) — order is never
|
|
55
|
+
assumed.
|
|
56
|
+
3. **Per data row, find-or-create:**
|
|
57
|
+
- the **Item** — resolving `manufacturerId` / `itemCategoryId` / `itemTypeId` /
|
|
58
|
+
`assetTypeId` by name via a generic find-or-create over `Manufacturers`,
|
|
59
|
+
`ItemCategories`, `ItemTypes`, `AssetTypes`; set in the catalog with flags.
|
|
60
|
+
- the **VendorItem** — vendor resolved by name; `overridePoSubmissionIntegrationId`
|
|
61
|
+
left NULL.
|
|
62
|
+
- **pricing** — either a single NULL-currency row, or one row per currency column
|
|
63
|
+
(see Multi-currency below).
|
|
64
|
+
- **personas** — each CSV persona token resolved by numeric id **or** by name against
|
|
65
|
+
`Personas`, linked through the `Personas_Items` bridge table.
|
|
66
|
+
4. **Etilize enrichment** (from the `getProduct` datasheet):
|
|
67
|
+
- **features** → `Features` (find-or-create by name, Text measure) + `ItemFeatures` +
|
|
68
|
+
`ItemCategoryFeatureGroups` + `ItemCategoryFeatures`.
|
|
69
|
+
- **images** → `Images` (find-or-create by `sourceUrl`) + a single `ItemImages` row
|
|
70
|
+
holding the four size-slot FKs: thumbnail / low / medium / high mapped to Etilize
|
|
71
|
+
sizes **160 / 225 / 650 / 900**.
|
|
72
|
+
|
|
73
|
+
## Multi-currency
|
|
74
|
+
|
|
75
|
+
- The set of **valid currency codes is read per-client at runtime** from that client's own
|
|
76
|
+
`Currencies` table via `loadClientCurrencyCodes()` — **never hardcoded** — so each client
|
|
77
|
+
only uses the currencies it actually has configured.
|
|
78
|
+
- A small **alias map** normalizes CSV header spellings to the real `Currencies.code`
|
|
79
|
+
(e.g. a `PER` header → `PEN`).
|
|
80
|
+
- A CSV currency column is used **only if** its normalized code matches one of the client's
|
|
81
|
+
configured currencies. For each match the import creates an `ItemPrice` + `VendorItemCost`
|
|
82
|
+
with that `currencyId` (`priceType` `each`).
|
|
83
|
+
|
|
84
|
+
## Etilize re-pull (`RefreshItemsFromEtilize`)
|
|
85
|
+
|
|
86
|
+
- Item located by `partNumber`. The Etilize id is updated **only when** the supplied
|
|
87
|
+
`etilizeId` differs from the stored `c_etilizeProductId`.
|
|
88
|
+
- The feature/image re-pull is **find-or-create** and **does not delete stale features**
|
|
89
|
+
— this is intentional.
|
|
90
|
+
|
|
91
|
+
## Gotchas
|
|
92
|
+
|
|
93
|
+
- CSV layout is fixed at the top: header row, a **skipped** description row, then data
|
|
94
|
+
from row 3. Feeding a two-row (header+data) file would treat the first data row as the
|
|
95
|
+
description row.
|
|
96
|
+
- Columns are matched by header regex, not position — a renamed/missing header silently
|
|
97
|
+
drops that column rather than mis-mapping by index.
|
|
98
|
+
- Currency columns not present in the client's `Currencies` table are ignored, so a
|
|
99
|
+
catalog that ships an unconfigured currency simply won't price it.
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
- 2026-06-26 — Documented `_Worker_Etilize_Items`: header-mapped S3 CSV import with
|
|
103
|
+
shared `runItemCsvImport`, normal (NULL-currency) vs. per-currency pricing, per-client
|
|
104
|
+
runtime currency resolution, persona linking, Etilize feature/image enrichment, and the
|
|
105
|
+
id-diff-only `RefreshItemsFromEtilize` re-pull. (bala)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Etilize Item Translation Import
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-26
|
|
10
|
+
owners: [bala]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Etilize/ItemTranslations.php
|
|
13
|
+
related:
|
|
14
|
+
- ./creating-worker-actions.md
|
|
15
|
+
- ./etilize-catalog-item-import.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English**
|
|
21
|
+
item text from Etilize into the client's `ItemTranslations` table. English is never
|
|
22
|
+
written — it stays in the `Items` table. The class writes **directly to `DB_CLIENT`**
|
|
23
|
+
via raw `_Query` (**no api2**). Used today to give **Compass Canada** French (fr-CA)
|
|
24
|
+
catalog text without duplicating English, but the code is client-generic.
|
|
25
|
+
|
|
26
|
+
`ItemTranslations` columns: `itemId`, `languageId`, `title`, `description`,
|
|
27
|
+
`longDescription`, with `UNIQUE(itemId, languageId)`.
|
|
28
|
+
|
|
29
|
+
## Public actions
|
|
30
|
+
|
|
31
|
+
Routes are `Etilize/ItemTranslations/<Method>` (see
|
|
32
|
+
[creating-worker-actions](./creating-worker-actions.md)).
|
|
33
|
+
|
|
34
|
+
1. `ImportAllItemsForLanguage(string $client, string $languageCode, ?string $languageName = null, ?string $catalog = null)`
|
|
35
|
+
— translates **all** items that have a `c_etilizeProductId`.
|
|
36
|
+
2. `ImportItemsLanguageByPartNumbers(string $client, string $languageCode, array $partNumbers, ?string $languageName = null, ?string $catalog = null)`
|
|
37
|
+
— translates only the given part numbers (uses a `partNumber IN (...)` filter).
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
1. Resolve `clientId` from `Core.Clients` by `clientIdentifier` and register the client DB.
|
|
42
|
+
2. **Find-or-create the Language** by `code` (e.g. `fr-CA`).
|
|
43
|
+
3. **Derive the Etilize locale** from the language code: `fr-CA` → `fr_ca` (lowercased,
|
|
44
|
+
hyphen → underscore).
|
|
45
|
+
4. Load the client's `Items` that have a **non-empty `c_etilizeProductId`** (scoped to the
|
|
46
|
+
given part numbers for the by-part-numbers action).
|
|
47
|
+
5. For each item, call the Etilize **Spex Live `getProduct`** API (`appId` 231136, catalog
|
|
48
|
+
default `na`) for that locale.
|
|
49
|
+
6. Extract the translated **`title`** (Etilize description **type 2**) and
|
|
50
|
+
**`longDescription`** (Etilize description **type 1**).
|
|
51
|
+
7. **Upsert** into `ItemTranslations` via `INSERT ... ON DUPLICATE KEY UPDATE` using
|
|
52
|
+
`COALESCE(VALUES(col), col)` so a blank new value never overwrites existing text.
|
|
53
|
+
|
|
54
|
+
Items **not published in the requested locale are skipped**.
|
|
55
|
+
|
|
56
|
+
## Gotchas
|
|
57
|
+
|
|
58
|
+
- The unique key is `(itemId, languageId)`; re-running an import upserts rather than
|
|
59
|
+
duplicating, and the `COALESCE` guard means a blank Etilize value will not clear an
|
|
60
|
+
existing translation.
|
|
61
|
+
- Only items with a `c_etilizeProductId` are translatable — items without an Etilize id
|
|
62
|
+
are silently skipped.
|
|
63
|
+
- English is intentionally never written here; it lives in the `Items` table.
|
|
64
|
+
|
|
65
|
+
## Change history
|
|
66
|
+
- 2026-06-26 — Documented `_Worker_Etilize_ItemTranslations`: per-language Etilize
|
|
67
|
+
`getProduct` import into `ItemTranslations` (title=desc type 2, longDescription=type 1),
|
|
68
|
+
locale derived from language code, COALESCE upsert that never overwrites with blanks,
|
|
69
|
+
all-items vs. by-part-numbers actions. (bala)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
## 2.0 framework
|
|
18
18
|
|
|
19
19
|
- **_underscore** (_Underscore) _(framework core)_ — 16 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
20
|
-
- **worker2** (Worker) —
|
|
20
|
+
- **worker2** (Worker) — 17 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
21
21
|
- **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
22
22
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
23
23
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -7,12 +7,13 @@ apps:
|
|
|
7
7
|
- toga2-supply
|
|
8
8
|
- toga2-commerce
|
|
9
9
|
- worker
|
|
10
|
+
- worker2
|
|
10
11
|
- dbchanges2
|
|
11
12
|
project: _Underscore
|
|
12
13
|
client: compass-canada
|
|
13
14
|
type: profile
|
|
14
15
|
status: active
|
|
15
|
-
updated: 2026-06-
|
|
16
|
+
updated: 2026-06-26
|
|
16
17
|
owners: [jcardinal, bala]
|
|
17
18
|
files: []
|
|
18
19
|
related:
|
|
@@ -6,13 +6,14 @@ apps:
|
|
|
6
6
|
- api2
|
|
7
7
|
- toga2-supply
|
|
8
8
|
- toga2-commerce
|
|
9
|
+
- worker2
|
|
9
10
|
- dbchanges2
|
|
10
11
|
project: _Underscore
|
|
11
12
|
client: quad
|
|
12
13
|
type: profile
|
|
13
14
|
status: active
|
|
14
|
-
updated: 2026-06-
|
|
15
|
-
owners: ["jcardinal"]
|
|
15
|
+
updated: 2026-06-26
|
|
16
|
+
owners: ["jcardinal", "bala"]
|
|
16
17
|
files: []
|
|
17
18
|
related:
|
|
18
19
|
- ../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
package/package.json
CHANGED