toga-ai 1.0.971 → 1.0.973

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.
Files changed (68) hide show
  1. package/knowledge/2.0/apps/api2/features/api-payload-interceptors.md +9 -5
  2. package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +6 -1
  3. package/knowledge/2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md +7 -6
  4. package/knowledge/2.0/apps/worker2/features/service-request-sales-order-generation.md +9 -7
  5. package/knowledge/2.0/apps/worker2/features/talos-pricing-cogs-model.md +222 -0
  6. package/knowledge/clients/adyen/features/service-request-salesorder-netsuite.md +29 -21
  7. package/knowledge/clients/adyen/profile.md +4 -6
  8. package/knowledge/clients/compass-usa/features/talos-supply-assistant.md +67 -0
  9. package/knowledge/clients/tow-foundation/features/talos-tenant.md +59 -0
  10. package/knowledge/clients/true/features/talos-internal-tenant.md +72 -0
  11. package/knowledge/clients/true/features/toga-hd-voice-demo.md +62 -0
  12. package/knowledge/standalone/apps/ai-bdr/INDEX.md +9 -0
  13. package/knowledge/standalone/apps/ai-bdr/architecture.md +149 -0
  14. package/knowledge/standalone/apps/ai-bdr/features/call-orchestration.md +211 -0
  15. package/knowledge/standalone/apps/ai-bdr/features/vapi-integration.md +152 -0
  16. package/knowledge/standalone/apps/ai-bdr/workflows/new-campaign-onboarding.md +115 -0
  17. package/knowledge/standalone/apps/ai-bdr/workflows/safe-call-loop-testing.md +90 -0
  18. package/knowledge/standalone/apps/bdr/INDEX.md +13 -0
  19. package/knowledge/standalone/apps/bdr/architecture.md +51 -0
  20. package/knowledge/standalone/apps/bdr/features/bdr-web-funnel-plan.md +290 -0
  21. package/knowledge/standalone/apps/bdr/features/campaign-chat-handoff.md +119 -0
  22. package/knowledge/standalone/apps/bdr/features/landing-call-flow.md +162 -0
  23. package/knowledge/standalone/apps/bdr/features/landing-chat-drawer.md +128 -0
  24. package/knowledge/standalone/apps/bdr/features/live-call-status.md +108 -0
  25. package/knowledge/standalone/apps/bdr/features/security-landing-page.md +150 -0
  26. package/knowledge/standalone/apps/bdr/features/web-funnel-app.md +309 -0
  27. package/knowledge/standalone/apps/bdr/features/web-funnel-content-model.md +102 -0
  28. package/knowledge/standalone/apps/talos/INDEX.md +7 -0
  29. package/knowledge/standalone/apps/talos/architecture.md +54 -0
  30. package/knowledge/standalone/apps/talos/features/auth-session-bff.md +55 -0
  31. package/knowledge/standalone/apps/talos/features/chat-frontend.md +97 -0
  32. package/knowledge/standalone/apps/talos-backend/INDEX.md +33 -0
  33. package/knowledge/standalone/apps/talos-backend/architecture.md +182 -0
  34. package/knowledge/standalone/apps/talos-backend/features/aegra-api.md +133 -0
  35. package/knowledge/standalone/apps/talos-backend/features/ai-service-endpoints.md +71 -0
  36. package/knowledge/standalone/apps/talos-backend/features/auth-and-access-control.md +114 -0
  37. package/knowledge/standalone/apps/talos-backend/features/background-jobs-and-scheduling.md +85 -0
  38. package/knowledge/standalone/apps/talos-backend/features/bedrock-models-and-cost-attribution.md +108 -0
  39. package/knowledge/standalone/apps/talos-backend/features/blp.md +166 -0
  40. package/knowledge/standalone/apps/talos-backend/features/client-integration-contract.md +111 -0
  41. package/knowledge/standalone/apps/talos-backend/features/code-interpreter-and-documents.md +89 -0
  42. package/knowledge/standalone/apps/talos-backend/features/composio-integrations.md +80 -0
  43. package/knowledge/standalone/apps/talos-backend/features/deep-agent-harness.md +173 -0
  44. package/knowledge/standalone/apps/talos-backend/features/deployment.md +91 -0
  45. package/knowledge/standalone/apps/talos-backend/features/infrastructure-and-environments.md +140 -0
  46. package/knowledge/standalone/apps/talos-backend/features/knowledge-base-search.md +85 -0
  47. package/knowledge/standalone/apps/talos-backend/features/mcp-servers.md +93 -0
  48. package/knowledge/standalone/apps/talos-backend/features/memory.md +87 -0
  49. package/knowledge/standalone/apps/talos-backend/features/observability.md +82 -0
  50. package/knowledge/standalone/apps/talos-backend/features/realtime-voice-and-dictation.md +84 -0
  51. package/knowledge/standalone/apps/talos-backend/features/talos-agent.md +66 -0
  52. package/knowledge/standalone/apps/talos-backend/features/tenant-configuration.md +158 -0
  53. package/knowledge/standalone/apps/talos-backend/features/toga-platform-mcp.md +87 -0
  54. package/knowledge/standalone/apps/talos-backend/features/usage-dashboard-api.md +76 -0
  55. package/knowledge/standalone/apps/talos-backend/workflows/authoring-a-blp.md +301 -0
  56. package/knowledge/standalone/apps/talos-backend/workflows/debugging-agent-behavior.md +100 -0
  57. package/knowledge/standalone/apps/talos-backend/workflows/debugging-production.md +153 -0
  58. package/knowledge/standalone/apps/talos-backend/workflows/documentation-and-gitbook.md +61 -0
  59. package/knowledge/standalone/apps/talos-backend/workflows/local-prod-db-clone.md +60 -0
  60. package/knowledge/standalone/apps/talos-backend/workflows/production-release.md +97 -0
  61. package/knowledge/standalone/apps/talos-backend/workflows/toga-supply-client-onboarding.md +227 -0
  62. package/knowledge/standalone/apps/voice-to-voice/INDEX.md +8 -0
  63. package/knowledge/standalone/apps/voice-to-voice/architecture.md +56 -0
  64. package/knowledge/standalone/apps/voice-to-voice/features/livekit-pipeline.md +69 -0
  65. package/knowledge/standalone/apps/voice-to-voice/features/post-call-lambda.md +66 -0
  66. package/knowledge/standalone/apps/voice-to-voice/features/ticket-api-integration.md +65 -0
  67. package/knowledge/standalone/standards/python.md +109 -0
  68. package/package.json +1 -1
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
10
- owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal"]
9
+ updated: 2026-10-08
10
+ owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - api2/Controller/Index.php
@@ -211,7 +211,7 @@ A `postPost`/`postPut` hook that hands work to worker2 (`_Worker::runTask`) sits
211
211
  5. **The pattern that works:**
212
212
  - Read the record **inside the same request** with `internalApiRequest`, wrapped in `getIsReadHostEnabled()` / `setIsReadHostEnabled(false)` and restored in a **`finally`** (pin the read to the writer, then put the flag back for everyone else).
213
213
  - **Superseded 2026-10-05 for queueing: pass only the uuid, never the object** — a nested record can exceed the SQS 256KB cap and the job is lost. The worker reads the record itself with a bounded wait for the commit. Rule + reference: [runTask parameters are ids only](../../worker2/features/creating-worker-actions.md#-runtask-parameters-are-idsuuids-only--never-record-objects). (The Elite sales-order push still carries `?object $salesOrder` — legacy.) The writer-pinned read stays correct when the hook itself needs the record.
214
- - Pass **`false`** for `internalApiRequest`'s 5th argument (`$throwExceptionsOnError`). Its throw is guarded by `!empty($this->response->messages)` on the **shared** response object, and `json_encode([])` is the **truthy** string `"[]"` — so it throws even with **zero real errors** once any message (even a warning) is present anywhere in the request. **Not theoretical:** it took Elite's production `POST /v2/sales-orders` down on 2026-08-20, from the *parent* model's own `internalApiRequest` (see [the inherited-chain section](#-activating-a-clients-first-interceptor-row-also-switches-on-the-whole-inherited-chain)). The throw site is `V2.php:2416`.
214
+ - Pass **`false`** for `internalApiRequest`'s 5th argument (`$throwExceptionsOnError`). Its throw is guarded by `!empty($this->response->messages)` on the **shared** response object, and `json_encode([])` is the **truthy** string `"[]"` — so it throws even with **zero real errors** once any message (even a warning) is present anywhere in the request — **including the OUTER request's own warnings** collected before the internal call (e.g. `WO-1`, request depth below an interceptor row's `minDepth`; the worker posts at `depth -1`). **Not theoretical:** it took Elite's production `POST /v2/sales-orders` down on 2026-08-20, from the *parent* model's own `internalApiRequest` (see [the inherited-chain section](#-activating-a-clients-first-interceptor-row-also-switches-on-the-whole-inherited-chain)). The throw site is `V2.php` ~L2436 (open bug, 2026-10-08, needs its own ticket).
215
215
 
216
216
  **Two more failure modes on this path:**
217
217
  - **Config drift kills it before it starts.** `_Worker::runTask`'s non-debug path reads `[cloud] aws_worker_queue_url` / `aws_worker_queue_region`, and `_Config::__callStatic` **throws on a missing key even inside a group that exists** — an uncaught throw from `postPost` surfaces as **HTTP 500 `EO-1` with a null envelope and no log row**. See [_Config group access](../../_underscore/features/config-group-access.md).
@@ -225,10 +225,10 @@ A client whose `ApiPayloadInterceptors` set is **empty** has never run `postPost
225
225
  1. Elite's `postPost` calls `parent::postPost()` first (correctly — approvals must still be created);
226
226
  2. the parent, `_underscore/Model/Client/SalesOrder.php:359`, reads `/approval-templates` via `internalApiRequest` with **throwing left ON**;
227
227
  3. Elite has **zero `ApprovalTemplates`** rows, so that nested GET returns empty and puts a message on the **shared** response object;
228
- 4. `V2.php:2416` does `json_encode(array_filter($messages, ...ERROR...))` → the string `"[]"` → **truthy** → throw, with no real error in it;
228
+ 4. `V2.php` ~L2436 does `json_encode(array_filter($messages, ...ERROR...))` → the string `"[]"` → **truthy** → throw, with no real error in it;
229
229
  5. the controller catches it, returns `EO-1`, and **rolls back** the write.
230
230
 
231
- **Reading the signature:** an exception whose **message is `[]`** on a write route means *this* — a message-array truthiness throw. Nothing is wrong with the payload, and the client's own interceptor code may never have executed. **Two fixes, both in shared code:** pass **`false`** as `internalApiRequest`'s 5th argument in the **parent** (narrow, per-caller); or fix **`V2.php:2416`** to test the **filtered array** rather than `json_encode()` of it — which fixes **every** caller on this path. Senior-owned: raise it rather than patching one caller.
231
+ **Reading the signature:** an exception whose **message is `[]`** on a write route means *this* — a message-array truthiness throw. Nothing is wrong with the payload, and the client's own interceptor code may never have executed. **Two fixes, both in shared code:** pass **`false`** as `internalApiRequest`'s 5th argument in the **parent** (narrow, per-caller); or fix **`V2.php` ~L2436** to look only at messages added **after `$savedMessageCount`** and throw only when that filtered **array** has a real `TYPE_ERROR` — which fixes **every** caller on this path. Senior-owned: raise it rather than patching one caller.
232
232
 
233
233
  **Before activating a row for a client that has none:** walk the parent chain for the derived method and confirm the data each inherited step depends on actually exists for that tenant (here: at least one `ApprovalTemplates` row). Same reason activation must be staged `isActive = 0` first — see Gotchas.
234
234
 
@@ -263,6 +263,7 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
263
263
  - **⚠ A throw from a PRE interceptor leaves NO `Logs_<Client>.Api` row.** The transaction log is written near the END of `_Component_Api_V2::execute()` (~L2225-2271), *after* route processing (~L1742-2043), so an exception raised from `prePost` never reaches it and the rejected request looks like it never arrived. See [V2 request logging](./request-logging.md#-a-throw-from-a-pre-interceptor-leaves-no-api-log-row-at-all).
264
264
  - **⚠ Client behaviour never goes on `_Model_Client_X`.** Every tenant with a row for that record inherits it through `parent::` calls. Put it on `_Model_<Client>_X`.
265
265
  - **A client's rows may be in `Core` (with `clientId`), not in `Client_<X>`** — check both before concluding a client has no interceptors.
266
+ - **⚠ The same hook in BOTH `Core` (with `clientId`) and `Client_<X>` fires TWICE** — both tables are read, nothing dedupes. Adyen (2026-10-08): Core ids 15–17 duplicated client rows 1, 2, 5 → double worker jobs and double NetSuite queueing; fixed by deactivating the Core rows. Keep each `(record, phase, method)` in one table only. See [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md#gotchas).
266
267
  - **The method name is derived, so a typo'd enum silently misses.** A row with `prePostProcessing = 'PRE'`, `httpMethod = 'PUT'` resolves to `prePut`, not `prePost`; the engine finds no method and moves on.
267
268
  - **Registration is per-environment.** Like `RecordFields` / `RecordScripts` / ACL rows, this is metadata — "works in prod, not in beta/dev-sandbox" is the signature of drift, not of a bad deploy. See the [non-prod metadata drift repair workflow](../../dbchanges2/workflows/nonprod-metadata-drift-repair.md).
268
269
  - **⚠ An in-request read-after-write in an interceptor works on beta and fails 100% in production.** Reads default to the **reader** connection, which cannot see the request's own uncommitted row; dev-sandbox points writer and reader at the same endpoint, so it hides the bug. Pin with `setIsReadHostEnabled(false)` (restore in `finally`).
@@ -287,3 +288,6 @@ The reverse direction: not "queue a worker from an interceptor" but **"run the p
287
288
  - [Email template sending](../../_underscore/features/email-template-sending.md)
288
289
  - [Elite SalesOrder → NetSuite push](../../../clients/elite/features/salesorder-netsuite-push.md)
289
290
  - [Compass sales-order line renumbering](../../../clients/compass-usa/features/sales-order-line-renumbering.md)
291
+
292
+ ## Change history
293
+ - 2026-10-08 — `internalApiRequest` "[]" throw also fires on the OUTER request's warnings (e.g. WO-1); throw site now ~L2436; Core + client duplicate rows fire twice. (rgirish)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-06
9
+ updated: 2026-10-08
10
10
  owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
11
11
  files:
12
12
  - worker2/Worker/
@@ -151,6 +151,7 @@ in the deployed worker code, so ship the code deploy and the dbchanges2 cron-row
151
151
  ```bash
152
152
  curl -X POST https://worker.togahub.com/ \
153
153
  -H "Content-Type: application/json" \
154
+ -H "Authorization: <worker key>" \
154
155
  -d '{
155
156
  "action": "Category/MyAction/DoSomething",
156
157
  "parameters": { "param1": "example_value", "count": 5 }
@@ -159,6 +160,9 @@ curl -X POST https://worker.togahub.com/ \
159
160
 
160
161
  Use realistic placeholder values, not empty strings/nulls.
161
162
 
163
+ - Needs the worker key in `Authorization` (never paste the value into docs, tickets or chat).
164
+ - Runs **synchronously** and creates **no `Core.WorkerJobs` row** — the SQS path (`_Worker::runTask` → `WorkerProductionQueue`) is the only one that logs a job. Use it to re-run a single action (e.g. one service request); read the result from the HTTP reply.
165
+
162
166
  ### The HTTP reply is PREFIXED — never `JSON.parse` it raw
163
167
 
164
168
  The dispatcher returns your action's return value **wrapped in the same composed success
@@ -539,6 +543,7 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
539
543
  commit-before-SQS transaction pattern that the worker relies on.
540
544
 
541
545
  ## Change history
546
+ - 2026-10-08 — Direct POST needs the worker key in `Authorization`, runs synchronously, writes no `WorkerJobs` row. (rgirish)
542
547
  - 2026-10-06 — Absent-row section: a 200 + MessageId send can still produce no row for up to 1h (failed first delivery hidden by the visibility timeout). (ajean)
543
548
  - 2026-10-05 — Rule: `runTask` parameters are ids/uuids only; worker reads with bounded wait; producer catch must `_Error::captureException`. (jcardinal)
544
549
  - 2026-10-05 — Added read-host read-after-write gotcha for worker actions (TRUE-82185) (mhammontree)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
10
- owners: ["bala", "jcardinal"]
9
+ updated: 2026-10-08
10
+ owners: ["bala", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite/SalesOrder.php
13
13
  - test/@Bala/tests/netsuite_salesorder_payload_tests.php
@@ -105,10 +105,11 @@ Four helpers gained a transfer-order branch, each **defaulting to the old behavi
105
105
  `fetchSalesOrder($ctx, $uuid, $isTransferOrder = false)`, `writeBackNetSuiteId()` (route), and
106
106
  `buildNetSuiteOrder()`, which now prefers an explicit `$order->otherRefNum` **before** the
107
107
  ticket-number → service-request-number chain. **`CreateNetSuite` / `UpdateNetSuite` / `Sync` keep
108
- their original signatures**, and the sales-order path is provably unchanged because
109
- `_Model_Client_SalesOrder` declares **no `otherRefNum` field** — the new branch is unreachable from a
110
- sales order. When you add a precedence like that, verify the *other* model does not have the field
111
- rather than assuming it will not be set.
108
+ their original signatures**. `_Model_Client_SalesOrder` declares **no `otherRefNum` field**, but the
109
+ branch **is** reachable for a sales order when the caller sets it on the queued snapshot: since
110
+ 2026-10-08 `_Model_Adyen_SalesOrder::queueNetsuiteCreate()` sets `otherRefNum` = employee name for
111
+ SR-generated orders (see [Adyen reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md)).
112
+ When you add a precedence like that, check every producer of the snapshot, not only the model fields.
112
113
 
113
114
  ### The job carries the order snapshot — the worker does not re-read it
114
115
 
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-08
10
10
  owners: ["snaredla", "bala", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Sync/ServiceRequest.php
@@ -37,9 +37,9 @@ _Worker_Sync_ServiceRequest::GenerateSalesOrderFromServiceRequest(
37
37
  ): string
38
38
  ```
39
39
 
40
- Queued from a client's `service-requests` `postPost` interceptor; invocable directly for testing like any worker action. Consumers: **Elite** ([desk intake](../../../clients/elite/features/togadesk-service-request-intake.md)) and **Adyen** ([reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md), built 2026-09-30, not deployed).
40
+ Queued from a client's `service-requests` `postPost` interceptor; invocable directly for testing like any worker action. Consumers: **Elite** ([desk intake](../../../clients/elite/features/togadesk-service-request-intake.md)) and **Adyen** ([reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md)).
41
41
 
42
- **Per-client config = `SALES_ORDER_DEFAULTS_BY_CLIENT`**, keyed by `Core.Clients.clientIdentifier`: `locationUuid` (required — NetSuite rejects an order with no location) + `salesOrderTypeUuid` (nullable). Elite → location `3ca47b91-…`, type drop-ship `5994b81d-…`; Adyen → location `191c75ff-f02e-4dda-abc6-c8d26c35da62`, no type. **A client missing from the map throws** — onboarding a client to this worker = add a map entry. The uuids are the same in beta and prod.
42
+ **Per-client config = `SALES_ORDER_DEFAULTS_BY_CLIENT`**, keyed by `Core.Clients.clientIdentifier`: `locationUuid` (required — NetSuite rejects an order with no location) + `shippingMethodId` (fallback when the SR names none). Elite → location `3ca47b91-…`, ship method 1; Adyen → location `191c75ff-f02e-4dda-abc6-c8d26c35da62` (`Locations` id 1 → NS location 70), ship method **5** (Ground). Sales order type is one constant for every client: drop-ship `5994b81d-…` (`SALES_ORDER_TYPE_UUID`). **A client missing from the map throws before any API call** — onboarding a client to this worker = add a map entry with **that tenant's own** row ids (verify the location has `c_netsuiteInternalLocationId`). The uuids are the same in beta and prod.
43
43
 
44
44
  **Proven end to end on dev-sandbox** (`https://api.dev.sandbox.togahub.com/v2`): one service request produced **SA100004** plus purchase order **P1**, both bridge rows linked, and a **rerun returned early** rather than duplicating.
45
45
 
@@ -55,8 +55,8 @@ Steps:
55
55
  1. **Resolve the tenant.** `getClientContextFromCore()` looks up by `Core.Clients.clientIdentifier`, registers the client DB connection, returns Toga API credentials. (Deliberate local copy of the same private in `_Worker_Netsuite_SalesOrder`; lift to a shared parent when a third caller appears.)
56
56
  2. **Guard.** `fetchServiceRequestFromToga2()` joins `SalesOrders` on `serviceRequestId`. If a sales order already exists, **return early** with a summary — makes the action idempotent under SQS redelivery and manual reruns. A request already in the **`Exception`** stage is skipped for the same reason.
57
57
  3. **Reconcile the requester's address** (`reconcileRequesterAddressInToga2`) and **opt the requester into Order Updates** (`optInRequesterToOrderUpdatesInToga2`).
58
- 4. **Build and POST the sales order** (`buildSalesOrderPayloadForToga2`): customer by uuid, `serviceRequestId`, `ticketId`, `locationId` + type from the client's map, `shippingMethodId` = the SR's, else **`ShippingMethods` id 1**, and `shipToAddress` **only when the request has a deliverable address**. Lines come from `fetchServiceRequestLinesFromToga2()`: one line per SR **unit**; if the SR has no units, one line per **bundle item** (`ServiceRequests_Bundles` → `BundleItems` → `Items`). Each line is quantity 1, price 0, `lineNumber` assigned by the worker. No units and no bundle items → throws (no empty order).
59
- 5. **Generate purchase orders** (`tryGeneratePurchaseOrdersInToga2` → `generatePurchaseOrdersInToga2`): re-read created lines with `fetchSalesOrderItemsFromToga2()`, resolve a vendor per item with `fetchVendorItemsFromToga2()`, group lines by vendor, POST **one PO per vendor** with line numbers restarted per order. Items with no vendor are listed in the job result as `No vendor for: …` — the job does **not** fail (Adyen has no `VendorItems`, so it gets no PO).
58
+ 4. **Build and POST the sales order** (`buildSalesOrderPayloadForToga2`): customer by uuid, `serviceRequestId`, `ticketId`, `location` from the client's map, drop-ship type, `shippingMethodId` = the SR's, else the client's map value, and `shipToAddress` **only when the request has a deliverable address**. Lines: one per SR **unit** (qty 1); if the SR has no units, one per **bundle item** from `fetchServiceRequestBundleItemsFromToga2()` (`ServiceRequests_Bundles` → `BundleItems` → `Items`, `BundleItems.isActive = 1`, `BundleItems.serviceRequestTypeId` NULL or equal to the SR's type, qty = `BundleItems.quantity`). Price 0, `lineNumber` assigned by the worker. No units and no bundle items → throws (no empty order).
59
+ 5. **Generate purchase orders — units path only.** A bundle-only SR gets **no** PO (result: `No purchase orders: the Service Request has no units.`). (`tryGeneratePurchaseOrdersInToga2` → `generatePurchaseOrdersInToga2`): re-read created lines with `fetchSalesOrderItemsFromToga2()`, resolve a vendor per item with `fetchVendorItemsFromToga2()`, group lines by vendor, POST **one PO per vendor** with line numbers restarted per order. Items with no vendor are listed in the job result as `No vendor for: …` — the job does **not** fail.
60
60
  6. **On a rejected sales-order POST**, `moveServiceRequestToExceptionInToga2()` parks the request in the `Exception` stage so the failure is visible *and* a redelivered job short-circuits on the step-2 stage guard.
61
61
 
62
62
  ### Vendor resolution replaces Prudential's hardcoded vendor
@@ -93,8 +93,9 @@ The three structural checks (string resolves → producer matches → intercepto
93
93
  - **⚠ Before nesting a child collection in a payload, confirm the child is registered in `Core.InherentRecordChildren` under that parent record.** `salesOrderItemsCommittedUnits` was briefly nested under sales-order lines; it is **impossible** — record 152 has **zero** `Core.InherentRecordChildren` parent registrations under record 15, nothing writes that table via the API at all, and its model's `unitId` is a foreign key to `_Model_Client_User`, not to a unit. A nested key the API does not recognize is not obviously rejected; run the `InherentRecordChildren` check rather than assuming.
94
94
  - **`shipToAddress` carries `childPolicy CREATE`** — posting an all-null address shape **creates a blank address row**. `hasDeliveryAddressForServiceRequest()` omits the key entirely instead.
95
95
  - **Line numbers are assigned by the worker and re-checked.** After the POST the worker maps created lines back by `lineNumber`; a line it cannot map means the API renumbered them, and that is **reported** rather than silently dropped from the PO.
96
- - **⚠ Do not add Prudential to the map without a review.** The worker now reads bundle items, but only as plain qty-1 lines — not Prudential's `BundleItems.preferredVendorId` / Dell flow. Prudential is not in `SALES_ORDER_DEFAULTS_BY_CLIENT`, so a run for it throws.
97
- - **Fallback ship method `ShippingMethods` id 1 is a per-tenant row id**, not a known method. When the SR names none, verify id 1 is the right NetSuite-mapped method for that client (open).
96
+ - **⚠ Do not add Prudential to the map without a review.** Bundle items become plain SO lines with no PO — not Prudential's `BundleItems.preferredVendorId` / Dell flow. Prudential is not in `SALES_ORDER_DEFAULTS_BY_CLIENT`, so a run for it throws.
97
+ - **⚠ Map ids are per-tenant row ids — never copy one client's entry.** Elite's ids broke Adyen: `ShippingMethods` id 1 does not exist in `Client_Adyen` (FK failure), and Adyen `Locations` id 2 has no `c_netsuiteInternalLocationId` (NetSuite push failed).
98
+ - **`SalesOrders.serviceRequestId` is the only duplicate guard.** A NetSuite order entered by hand has it NULL, so re-running an old SR whose order was hand-made creates a **second** order. Link or skip those SRs first (Adyen backlog: [reclaim flow](../../../clients/adyen/features/service-request-salesorder-netsuite.md#gotchas)).
98
99
  - **⚠ Inbound NetSuite sync can duplicate an order this worker created (all clients, open).** `library/app/api/toga2.php:1002` matches sales orders only by `c_netsuiteInternalSalesOrderId`. If the inbound sync runs after NetSuite has the order but before the push writes the id back, it imports a second copy.
99
100
  - **Placement:** cross-system sync workers live under `worker2/Worker/Sync/`. This worker does **not** belong in `Worker/Netsuite/` (which keeps the NetSuite push, `Worker/Netsuite/SalesOrder.php`) and not in `Worker/Client/`.
100
101
  - **⚠ Renaming this action is a TWO-REPO, single-deploy change.** The method name here and the `runTask(action: 'Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest', …)` **string** in `_underscore/Model/Elite/ServiceRequest.php:44` must land and deploy **together** — the rename from `GenerateSalesOrder` on 2026-08-12 spans both. Ship either alone and the dispatcher targets a name that no longer exists; because `postPost` wraps `runTask` in try/catch + `error_log`, Sales Order generation then stops **silently**. Same exposure for `Sync/SalesOrderStatus/PostSalesOrderStatusToTogadesk1`. See [creating worker actions](./creating-worker-actions.md).
@@ -112,6 +113,7 @@ Recorded so nobody re-debugs them as bugs. All four confirmed against Elite's da
112
113
  - The worker is otherwise complete; these are data/config gaps for the client to close.
113
114
 
114
115
  ## Change history
116
+ - 2026-10-08 — Map gains `shippingMethodId` (replaces hardcoded id 1); bundle lines filtered by `isActive` + SR type, qty from `BundleItems.quantity`; no POs for bundle-only SRs. (rgirish)
115
117
  - 2026-09-30 — Per-client `SALES_ORDER_DEFAULTS_BY_CLIENT` (sets `locationId`, unknown client throws); bundle-item lines when an SR has no units; no-vendor items reported, not failed; Adyen added as second consumer. Not deployed. (rgirish)
116
118
 
117
119
  ## Related
@@ -0,0 +1,222 @@
1
+ ---
2
+ title: Pricing & COGS Model (Talos Pricing Calculator)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-07
10
+ owners: [jcardinal, akhokhani]
11
+ files:
12
+ - worker2/Worker/Talos/Pricing.php
13
+ - tools/_/app/talos/estimator.php
14
+ - dbchanges2/Team/2026-08-03a - TalosPricingTokenCostModel.sql
15
+ - dbchanges2/Team/2026-09-17a - TalosInteractionPricingAndContractTerms.sql
16
+ - dbchanges2/Team/2026-09-17b - RemoveTalosAnnualMaintenanceFee.sql
17
+ related:
18
+ - ../../../../standalone/apps/talos-backend/architecture.md
19
+ - ../../../../standalone/apps/talos-backend/features/aegra-api.md
20
+ - ../../../../standalone/apps/talos-backend/features/observability.md
21
+ - talos-pricing-automation.md
22
+ - ../../../../1.0/apps/tools/features/talos-pricing-ui.md
23
+ ---
24
+
25
+ Cost basis + pricing methodology for selling Talos chat and voice; open to re-price Talos or understand COGS drivers, token rates, and the band/org-fee model.
26
+
27
+ ## Summary
28
+
29
+ Cost drivers, measured production rates, and pricing decisions so anyone can re-price Talos without re-deriving the model.
30
+
31
+ COGS token economics below remain valid. **The delivery mechanism has pivoted** (2026-06-29) from the per-client Excel workbook (TALOS Pricing Calculator v9) to a **database-backed platform**: a Team-DB system of record + an onboarding/dashboard UI in the **tools** app (1.0) + **worker2** cron automation that calibrates Langfuse usage to AWS actuals each month. Why: Excel can't scale and forced sales to guess inputs (esp. avg conversations/user) they don't know but we already have in Langfuse. See [talos-pricing-ui](../../../../1.0/apps/tools/features/talos-pricing-ui.md) and [talos-pricing-automation](talos-pricing-automation.md).
32
+
33
+ Treat the numbers as the **April 2026 baseline** (token unit price measured July 2026); re-measure before relying on them later.
34
+
35
+ ## How it works
36
+
37
+ ### Cost methodology — measured token unit price (decision, 2026-08-03) ← CURRENT
38
+
39
+ **Supersedes the Langfuse calibration-factor method below.** Cost is tokens, priced at the real AWS rate:
40
+
41
+ ```
42
+ unitCostPer1kTokens = AWS Bedrock-family actual $ / measured tokens
43
+ clientMonthlyCost = client tokens × unitCostPer1kTokens
44
+ ```
45
+
46
+ **Measured July 2026:** $1,013.76 / 258.3M tokens = **$0.00392447 per 1k tokens**.
47
+
48
+ - **Why not cost-per-conversation.** Measured on True, questions per conversation range **1.0–61.0** (median 2.1, p90 6.5) — a 60× spread. A conversation cannot anchor a price.
49
+ - **Why not usage-API dollars.** The usage API reports cost **~2.5× below** the real AWS bill because it undercounts prompt caching (cache reads are 47% of True's tokens). Its **token counts** are a real measurement and are used; **its dollars never are.**
50
+ - The per-client **calibration factor** is retired.
51
+
52
+ #### Workload profiles replace the feature checklist (decision, 2026-08-03)
53
+
54
+ Service offering (CHAT / VOICE_TO_VOICE / NATURAL_VOICE) describes **modality** and says nothing about cost. Cost is driven by tool-call **intensity**, varying by what the client uses Talos for. Measured on True: **42% `mcp_or_other`** (app integrations), **26% `code_interpreter`**, only **19% `knowledge_base`** — the opposite of what was assumed.
55
+
56
+ Cost per tool call is near flat across categories ($0.046–$0.062; `canvas` the outlier at $0.122), so **a single unit price × an intensity figure** models this and a per-tool price list is unnecessary. Three seeded profiles in `TalosWorkloadProfiles`: **retrieval** / **general** (measured from True) / **analytics** (estimated).
57
+
58
+ #### Model mix is a bigger lever than any contracted feature
59
+
60
+ Measured on True: `amazon.nova-pro` serves **70%** of LLM calls at $0.00092/1k while `claude-sonnet-4-5` serves **25%** at $0.00142/1k (76.3% cache). A **routing change moves COGS more than any feature flag**, so `TalosUsageModelMonthly` records model mix per client-month — cheap now, expensive to backfill, and the most likely explanation for a future margin swing.
61
+
62
+ #### App integrations are recorded but NOT yet priced
63
+
64
+ `tokensPerIntegrationPerUser` is seeded **0**, `isValidated 0`. Integrations are almost certainly the largest cost driver (`mcp_or_other` = 42% of True's cost), but a slope needs **≥2 clients** with a known integration count and real usage, and there is exactly one. Inventing a coefficient repeats the old $2.00/user default mistake. The plumbing is wired so the estimate, band recommendations, and tool-mix chart all respond the moment a real figure is set. Method recorded in the migration: **regress each client's `mcp_or_other` tokens-per-user against its `appIntegrationCount`.**
65
+
66
+ #### orgFeeShareOfRevenue (decision, 2026-08-03)
67
+
68
+ Default **0.25**. Fixes a double-count: the per-user fee was priced to hit the full target margin on its own, so the org-fee recommendation collapsed to ~$0.07 on a $1,036 contract, making the one adjustable lever useless. The seat fee now covers `(1 − share)` of target revenue and the flat fee covers the rest — **effective price per user is identical, only the split changes**. `share = 0` reduces exactly to the old formula. Verified to reconcile to the target margin to the cent.
69
+
70
+ ### Cost methodology — calibrate Langfuse to AWS (2026-06-29) — SUPERSEDED
71
+
72
+ > Retired 2026-08-03 by the token unit price above. Kept for context on pre-August figures.
73
+
74
+ Langfuse provides the **structure** (granular per-feature / per-user breakdown); AWS provides the **absolute dollars**. Each month, per client:
75
+
76
+ ```
77
+ calibration_factor = AWS_actual / Langfuse_costCorrected
78
+ ```
79
+
80
+ Apply the factor to Langfuse's granular figures so structure comes from Langfuse, dollars from AWS. New clients (no AWS history) use a **global blended factor** (`SUM(aws) / SUM(langfuse)` across clients) until they have their own rolling 3-month factor. **Langfuse badly undercounts cache cost** (sample: $201 recorded vs $1,088 corrected) — always use the **cache-corrected** Langfuse figure.
81
+
82
+ ### Chat COGS model
83
+
84
+ Chat AI cost is driven primarily by **LLM tokens on Amazon Bedrock**.
85
+
86
+ **Bedrock token rates (per 1M tokens):**
87
+
88
+ | Token class | Rate |
89
+ |---|---|
90
+ | Fresh input | $3.00 |
91
+ | Cached-instruction read | $0.30 |
92
+ | Cache write | $3.75 |
93
+ | Output | $15.00 |
94
+
95
+ **Measured per-exchange production averages** (tenant DB, April 2026, ~3,610 exchanges):
96
+
97
+ | Metric | Tokens / exchange |
98
+ |---|---|
99
+ | Cached instructions | 15,013 |
100
+ | Cache write | 2,830 |
101
+ | Output | 527 |
102
+
103
+ **Per-feature token adders (measured):**
104
+
105
+ | Feature | Tokens per use | Sample size |
106
+ |---|---|---|
107
+ | Document search | 4,747 / search | 542 searches |
108
+ | Web search | 765 / search | 120 searches |
109
+ | Code execution | 205 / run | 929 runs |
110
+ | App action | 1,086 / action | 71 actions |
111
+
112
+ **Assumed feature occurrences per conversation:** document search 2, web search 1, code execution 2, app actions 5 **per connected app**.
113
+
114
+ **Compute and fixed infrastructure:**
115
+ - **Code session compute** — Bedrock AgentCore ~$0.05 / session; ~80% of code-enabled conversations trigger a session.
116
+ - **Platform / hosting** — $910 / month fixed.
117
+ - **Document-search index** — OpenSearch Serverless, +$350 / month, charged **only when Document Search is enabled**.
118
+
119
+ **Conversation model constants (v8/v9 engine):**
120
+ - System overhead: 200 tokens / conversation
121
+ - Base fresh: 727 tokens / exchange
122
+ - History accumulation factor: ×7
123
+ - Exchanges per conversation: 15
124
+
125
+ > **Gotcha (flagged for technical review):** history accumulates over ×7 but the per-exchange cost is multiplied by 15 exchanges. This mismatch is a known quirk, **preserved as-is in v9 for number parity with v8** — do not "fix" it silently, as it would change every quoted price.
126
+
127
+ **Measured cost-per-exchange benchmarks by assistant type (April 2026):**
128
+
129
+ | Assistant type | Cost / exchange |
130
+ |---|---|
131
+ | Contact Centre | $0.0223 |
132
+ | HR / Compliance | $0.0354 |
133
+ | General Purpose | $0.0435 |
134
+ | Sales / CRM | $0.0829 |
135
+ | Technical / Data-heavy | $0.1139 |
136
+ | Enterprise heavy | $0.1261 |
137
+
138
+ ### Voice COGS model
139
+
140
+ Talos Voice (voice-to-voice) is priced **per minute**, using the AWS STT/TTS approach. Per-minute drivers:
141
+
142
+ | Driver | Rate / basis |
143
+ |---|---|
144
+ | LiveKit | $50/mo plan incl. 5,000 min; $0.01/min overage |
145
+ | Amazon Transcribe (STT) | $0.024 / min |
146
+ | Amazon Polly Generative (TTS) | $30 / 1M chars at ~400 chars/min |
147
+ | LLM tokens | ~800 fresh, ~5,000 cached, ~300 output per minute, at the chat token rates above |
148
+ | S3 recording storage | $0.023 / GB-mo at ~0.5 MB/min over the retention window |
149
+
150
+ ### Pricing methodology (decision)
151
+
152
+ `TalosClients.pricingModel` ENUM(`PER_USER`,`PER_INTERACTION`) selects one of two revenue models. Both share the same COGS basis above, the same **org fee = margin lever**, and the same consecutive-breach smoothing.
153
+
154
+ **Model A — per-user (default, unchanged):**
155
+ 1. **Two-part price.** Client pays a **flat monthly org fee** (the adjustable margin lever) PLUS a **per-user fee that steps by user-count band**.
156
+ 2. **Locked band schedule.** The per-user **band rate schedule** is fixed at **contract signing** (`TalosPricingBands`, per-user-model only). When user count crosses a band, the per-user fee auto-steps to the **pre-agreed band rate**; then the **org fee** is adjusted to restore margin. The per-user fee **never changes outside the agreed band schedule**.
157
+ 3. **Smoothing rule.** Actual margin is tracked monthly per client. The org fee change is **only recommended after** margin sits outside the min/max band for a configurable number of **consecutive** months (`consecutiveMonths`, per-client; default 3).
158
+
159
+ > **Pivot note (2026-06-29):** the earlier model used a *fixed* per-user fee + flat fee solved to the band midpoint. The current model makes the per-user fee a **locked band schedule** (steps with headcount) and uses the **org fee** as the margin lever.
160
+
161
+ #### Model B — per-interaction (decision, 2026-09-17)
162
+
163
+ For use cases where per-seat makes no sense (many interactions, no fixed user base). An **interaction = one conversation** (`TalosUsageMonthly.conversations`). Output is a **single price per interaction — no volume bands** (`TalosPricingBands` stays per-user-model-only), stored as scalar `perInteractionFee` / `recommendedPerInteractionFee` on `TalosClients`. Cost basis per interaction:
164
+ - **chat:** `tokensPerInteraction / 1000 × unitCostPer1kTokens` (from `TalosWorkloadProfiles.tokensPerInteraction`).
165
+ - **voice:** `avgMinutesPerInteraction × voiceCostPerMinute` (voice-only input; see voice cost factor below).
166
+
167
+ Revenue = `orgFee + interactions × perInteractionFee`. The org fee is still the monthly lever; the per-interaction rate is locked at signing.
168
+
169
+ #### Contract-term discount ramp (decision, 2026-09-17)
170
+
171
+ Fixed 1/3/5-year term dropdown → `contractTermMonths` 12/36/60. A **per-year discount ramp anchored to year 1** (percent always off year-1 price): default Y1 0%, Y2 5%, Y3+ 10%, stored as three editable policy rows `termDiscount.year1/year2/year3plus` in `TalosCostFactors` (the generic Settings page already renders/saves them). Whole-year terms only, **no proration**. Applies to **both** pricing models.
172
+
173
+ > **Gate (decision):** the ramp applies **only** to contracts that set a term (`contractTermMonths NOT NULL`). Existing no-term clients are unchanged — **no retroactive discount**.
174
+
175
+ #### Contract fees + amortization (decision, 2026-09-17)
176
+
177
+ Two **one-time** fees on `TalosClients`: `implementationFee` + `trainingFee`. Amortized to a monthly figure for **display only**:
178
+ ```
179
+ amortizedFeeMonthly = (implementationFee + trainingFee) / contractTermMonths
180
+ ```
181
+ Guard `contractTermMonths NULL`/0 → `amortizedFeeMonthly` is 0.
182
+
183
+ #### Two margins: recurring drives, blended displays (decision, CTO review 2026-09-17)
184
+
185
+ Keep **two** margins separate:
186
+ - **Recurring margin** (`TalosFeeRecommendations` revenue/marginPct) = org fee + ramped usage revenue vs. cost. This **alone** drives the monthly fee-change recommendation and the consecutive-breach streak.
187
+ - **Blended margin** (`amortizedFeeMonthly` + `blendedMarginPct`, new columns) **includes** the amortized fees and is **display-only**.
188
+
189
+ Why: amortizing one-time fees into the streak input under-fires recommendations mid-term and over-fires an accounting-artifact recommendation at term rollover. The monthly lever for **both** models is the org fee (usage/interaction rate locked at signing); **no new recommendation enum/column** is needed.
190
+
191
+ ### Delivery (current — database platform)
192
+
193
+ Lives across three repos (schema home + topology in the platform architecture doc):
194
+ - **Team DB (9 `Talos*` tables)** — `TalosClients`, `TalosPricingBands`, `TalosCostFactors`, `TalosUsageMonthly` + `TalosUsageFeatureMonthly`, `TalosAwsActualsMonthly`, `TalosClientUserCounts`, `TalosCalibrationMonthly`, `TalosFeeRecommendations`. (`dbchanges2/Team/...TalosPricingTables.sql`)
195
+ - **tools app (1.0)** — onboarding (locks the band schedule), pricing dashboard, usage benchmarks, technical-only Cost Factors editor + `App_Talos_Estimator`.
196
+ - **worker2 (2.0)** — monthly crons: import AWS actuals, recompute calibration + margins/recommendations, send leadership report.
197
+
198
+ The previous **TALOS Pricing Calculator v9** Excel workbook (Sales / Voice / Technical / Actuals / Engine tabs, per-client file under the shared AI drive) is **superseded** by this platform but documents the same economics.
199
+
200
+ ## Gotchas
201
+
202
+ - **Numbers are an April 2026 baseline.** Token rates, AWS prices, and measured per-exchange averages drift — re-measure from the tenant DB before quoting later.
203
+ - **History ×7 vs. ×15 exchanges quirk** (see Chat model) — intentional parity hack, not a bug to silently correct.
204
+ - **OpenSearch +$350/mo is conditional** — only counts when Document Search is enabled; do not bake it into every quote.
205
+ - **Voice cost is per-minute, not a token multiplier.** For **per-interaction voice**, cost = `avgMinutesPerInteraction × voiceCostPerMinute` — a new `TalosCostFactors` policy row (seeded **0.055**, `isValidated=0` UNMEASURED, built from AWS published per-minute rates). This **replaces** the old 0.25 / 0.50 modality multipliers for voice. Chat still uses the token basis.
206
+ - **Voice multipliers 0.25 / 0.50 are UNVALIDATED** — supplied second-hand, never measured, and may be **inverted**. Flagged in the UI. Do not quote voice from them. (Superseded by `voiceCostPerMinute` for per-interaction voice.)
207
+ - **Pricing math is duplicated across two frameworks — guard drift.** `App_Talos_Estimator` (1.0, tools) and `_Worker_Talos_Pricing` (2.0, worker2) cannot share PHP, so every constant lives in Team-DB `TalosCostFactors` policy rows that **both** read, and a golden-master **parity test** locks them together (fixture `test/talos/pricing-parity-fixture.json` + a CLI test per repo that reflection-seeds the settings cache and calls the real methods: `tools/_/app/talos/estimator.parity.test.php`, `worker2/Worker/Talos/PricingParityTest.php`; 11/11 pass both). Never hardcode a pricing constant in one class. **Open:** extract the worker's composed `RecomputeMargins` margin formula into its own callable so parity can assert it directly; `marginPct` 0-vs-null on zero-revenue differs between the two classes (pre-existing).
208
+ - **`infraFixedMonthly` is 0** — shared platform cost has never been measured, so band recommendations come out flat. That flat table is the **honest output, not a bug**.
209
+ - **Internal tenants are costed but not quoted** — True (TOGA Technology) feeds the platform unit price and is excluded from fee recommendations; see `talos-pricing-automation`.
210
+ - **Do not embed the absolute drive path as canonical** — the calculator lives under the team shared AI drive; the path can move.
211
+
212
+ ## Change history
213
+ - 2026-10-07 — moved to 2.0/worker2 (framework axis per CONVENTIONS) (akhokhani)
214
+ - 2026-09-25 — renamed product references/frontmatter project from "TOGa IQ" to "TOGa Talos" (rebrand; TOGa IQ is now the separate `toga25-iq` app) (jcardinal)
215
+
216
+ ## Related
217
+
218
+ - [../architecture.md](../../../../standalone/apps/talos-backend/architecture.md)
219
+ - [aegra-api.md](../../../../standalone/apps/talos-backend/features/aegra-api.md)
220
+ - [observability.md](../../../../standalone/apps/talos-backend/features/observability.md)
221
+ - [../../worker2/features/talos-pricing-automation.md](talos-pricing-automation.md)
222
+ - [../../../1.0/apps/tools/features/talos-pricing-ui.md](../../../../1.0/apps/tools/features/talos-pricing-ui.md)
@@ -5,8 +5,8 @@ repo: _underscore
5
5
  project: _Underscore
6
6
  client: adyen
7
7
  type: client-feature
8
- status: draft
9
- updated: 2026-10-01
8
+ status: active
9
+ updated: 2026-10-08
10
10
  owners: ["rgirish"]
11
11
  files:
12
12
  - _underscore/Model/Adyen/ServiceRequest.php
@@ -22,38 +22,46 @@ related:
22
22
  - ../profile.md
23
23
  ---
24
24
 
25
- Adyen's Reclaim SR → SO → NetSuite chain (same as Elite, but items come from a seeded bundle because Adyen sends no serials); open for the prePost bundle attach, the 2026-09-30a setup migration, deploy order, or duplicate risks.
25
+ Adyen's Reclaim SR → SO → NetSuite chain (same as Elite, but items come from a seeded bundle because Adyen sends no serials); open for the prePost bundle attach, the setup migration, the employee-name NetSuite PO #, the Core/client interceptor double-fire, or the backlog SR↔NS order map.
26
26
 
27
27
  ## Summary
28
28
 
29
- Adyen's API (`Apis` id 4) posts Service Requests, all of type **"Reclaim"**, and **cannot send serial numbers**. The chain is the same as Elite's: SR `postPost` → worker2 builds the Sales Order → SO `postPost` → NetSuite push. The only Adyen difference: the SR's items come from a seeded **"Reclaim" bundle**, attached in `prePost`.
29
+ Adyen's API (`Apis` id 4, used by ServiceNow) posts Service Requests, all of type **"Reclaim"**, and **cannot send serial numbers**. Chain = Elite's: SR `postPost` → worker2 builds the Sales Order → SO `postPost` → NetSuite push. Adyen differences: items come from a seeded **"Reclaim" bundle** attached in `prePost`, and the NetSuite PO # is the **employee name**.
30
30
 
31
- **Status (2026-10-01): code + migration written, NOT run or deployed anywhere, not tested end to end.**
31
+ **Status (2026-10-08):** code committed in worker2 + `_underscore`; prod `Client_Adyen.ApiPayloadInterceptors` rows 1, 2, 5 active; prod Core duplicates removed (see Gotchas).
32
32
 
33
33
  ## How it works
34
34
 
35
- 1. `_Model_Adyen_ServiceRequest::prePost` — when type is `Reclaim` and the payload has **no units and no bundles**, adds `serviceRequestBundles: [{bundle:{uuid:'23717330-7672-4590-9e3d-7f4097c18723'}}]` (`RECLAIM_BUNDLE_UUID`).
36
- 2. `_Model_Adyen_ServiceRequest::postPost` — same as Elite's: queues `Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest`.
37
- 3. The worker turns the bundle's items into SO lines (qty 1 each) and sets location "Adyen" from its client map — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md). No PO: Adyen has no `VendorItems` (job reports `No vendor for: …`, does not fail).
38
- 4. The existing `_Model_Adyen_SalesOrder::postPost` queues `Netsuite/SalesOrder/CreateNetSuite` ([shared push](../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md)).
35
+ 1. `_Model_Adyen_ServiceRequest::prePost` — type `Reclaim` with **no units and no bundles** → adds `serviceRequestBundles: [{bundle:{uuid:'23717330-7672-4590-9e3d-7f4097c18723'}}]` (`RECLAIM_BUNDLE_UUID`).
36
+ 2. `_Model_Adyen_ServiceRequest::postPost` — queues `Sync/ServiceRequest/GenerateSalesOrderFromServiceRequest`.
37
+ 3. Worker turns the bundle items into SO lines; location + ship method come from Adyen's map entry (`Locations` id 1 → NS location 70 "Adyen"; `ShippingMethods` id 5 Ground). No PO for bundle-only SRs — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md).
38
+ 4. `_Model_Adyen_SalesOrder::postPost` → `queueNetsuiteCreate()` reads the order back (writer-pinned, `throwExceptionsOnError = false`), and **for SR-generated orders sets `otherRefNum` = employee name** from the SO contact, then queues `Netsuite/SalesOrder/CreateNetSuite` ([shared push](../../../2.0/apps/worker2/features/netsuite-salesorder-outbound-push.md)), which prefers `otherRefNum` over ticket/SR number.
39
+ - `buildEmployeeName()`: Adyen often sends the full name in **both** first and last name; if they match (case-insensitive) it uses one copy.
39
40
 
40
- **Why a bundle, not units:** a unit with only an item (no serial) matches an **existing** unit, so every SR would link the same unit — see [nested writes](../../../2.0/apps/api2/features/nested-relationship-writes.md). `ServiceRequests_Bundles.bundleId` is `MATCH` (links only), and Prudential already posts bundles daily with 201.
41
+ **Why a bundle, not units:** a unit with only an item (no serial) matches an **existing** unit, so every SR would link the same unit — see [nested writes](../../../2.0/apps/api2/features/nested-relationship-writes.md). `ServiceRequests_Bundles.bundleId` is `MATCH` (links only).
42
+
43
+ ### Decisions (PM, 2026-10-08)
44
+ - Reclaim bundle = `SVC-FS-OFFBOARD-ADN` (NS 1579821) + **`Misc Laptop`** (NS 1545437), both $0. Keep Misc Laptop — **not** "Apple Macbook 14in" (NS 1625438).
45
+ - NetSuite PO # (`otherrefnum`) = employee name, matching the hand-entered offboarding orders so ops can search NetSuite by name.
41
46
 
42
47
  ### Setup migration — `dbchanges2/Client_Adyen/2026-09-30a - ServiceRequestSalesOrderSetup.sql`
43
- - `ApiPayloadInterceptors`: record 35 (`service-requests`) PRE+POST / POST, scoped to Adyen API; record 14 (`sales-orders`) POST/POST and POST/PUT, `apiId` NULL (the worker writes as a different API identity).
44
- - Items `SVC-FS-OFFBOARD-ADN` (NS 1579821) + `Misc Laptop` (NS 1545437, SERIALIZED); "Reclaim" bundle + 2 `BundleItems`.
45
- - Location "Adyen" (NS location 70); ShippingCarrier FedEx + ShippingMethod Ground (NS 318627).
46
- - `Customers.c_netsuiteInternalCustomerId = 31054`.
48
+ - `ApiPayloadInterceptors`: record 35 (`service-requests`) PRE+POST / POST, `apiId` 4 only; record 14 (`sales-orders`) POST/POST and POST/PUT, `apiId` NULL (the worker writes as a different API identity).
49
+ - Items, "Reclaim" bundle + 2 `BundleItems`, Location "Adyen" (NS 70), FedEx Ground (NS 318627), `Customers.c_netsuiteInternalCustomerId = 31054`.
50
+
51
+ These copy Adyen's hand-made NetSuite reclaim orders (e.g. 290879, 288260, 279871).
47
52
 
48
- These values copy Adyen's hand-made NetSuite reclaim orders (e.g. 290879, 288260, 279871): always `SVC-FS-OFFBOARD-ADN` + one device item, location 70, ship method 318627.
53
+ ### Backlog — 8 Reclaim requests ↔ NetSuite SOs (2026-10-08)
54
+ NS SOs: 290879, 292873, 292882, 292874, 292875, 292876, 288261, 292872. **290879 and 288261 were hand-entered and are NOT linked to their SRs in Toga** (`SalesOrders.serviceRequestId` NULL).
49
55
 
50
56
  ## Gotchas
51
57
 
52
- - **⚠ Deploy order: worker2 → `_underscore` → SQL.** Running the SQL first registers a PRE/POST interceptor whose `prePost` method does not exist yet, so api2 fails **every** Adyen SR POST.
53
- - **Before 2026-09-30a, `Client_Adyen` had 0 `ApiPayloadInterceptors` rows** — the Adyen model hooks committed 2026-08-24 never fired. "Nothing happened" on Adyen is a data check first.
54
- - **Open — Adyen resending the same SR creates a duplicate.** `c_externalReferenceId` is not unique (SR100024 already duplicates SR100021 — see [profile](../profile.md)).
55
- - **Open — NetSuite PO # (`otherrefnum`) will be the SR number**, not the employee name the hand-made orders carry. The profile's SR↔NetSuite matching reads that field.
56
- - **Open — shipping method fallback** and the inbound-sync duplicate race are shared worker issues — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md#gotchas).
58
+ - **⚠ Never re-run the worker for an SR whose NS order was hand-entered.** The worker's only duplicate guard is `SalesOrders.serviceRequestId`; hand-made orders have it NULL, so a re-run creates a second order (and a second NetSuite push). Link the SR first.
59
+ - **⚠ Adyen hooks ran TWICE until 2026-10-08.** Prod `Core.ApiPayloadInterceptors` ids 15, 16, 17 (`clientId 48`, `minDepth 1`, records 35 + 14) duplicated `Client_Adyen` rows 1, 2, 5 — double worker jobs, and would double-queue NetSuite pushes. Their `minDepth 1` vs the worker's `depth -1` also raised warning **WO-1**, which trips the `internalApiRequest` `"[]"` throw (see [interceptors](../../../2.0/apps/api2/features/api-payload-interceptors.md)). Deactivated; the client rows still fire. Never re-activate them.
60
+ - **⚠ Deploy order: worker2 → `_underscore` → SQL.** SQL first registers a PRE row whose `prePost` does not exist yet → every Adyen SR POST fails.
61
+ - **Before 2026-09-30a, `Client_Adyen` had 0 client `ApiPayloadInterceptors` rows** — "nothing happened" on Adyen is a data check first.
62
+ - **Open — Adyen resending the same SR creates a duplicate.** `c_externalReferenceId` is not unique (SR100024 duplicates SR100021 — see [profile](../profile.md)).
63
+ - **Open — inbound-sync duplicate race** is a shared worker issue — see [SR → SO generation](../../../2.0/apps/worker2/features/service-request-sales-order-generation.md#gotchas).
57
64
 
58
65
  ## Change history
59
- - 2026-09-30 — Built: Reclaim bundle attach in `prePost`, Elite-style `postPost`, setup migration 2026-09-30a. Not deployed. (rgirish)
66
+ - 2026-10-08 — NetSuite PO # = employee name (`buildEmployeeName`); Adyen map entry (location id 1, ship method 5); prod Core interceptor duplicates 15–17 deactivated; Misc Laptop kept (PM). (rgirish)
67
+ - 2026-09-30 — Built: Reclaim bundle attach in `prePost`, Elite-style `postPost`, setup migration 2026-09-30a. (rgirish)
@@ -12,7 +12,7 @@ project: Worker
12
12
  client: adyen
13
13
  type: profile
14
14
  status: draft
15
- updated: 2026-10-01
15
+ updated: 2026-10-08
16
16
  owners: ["rgirish"]
17
17
  files:
18
18
  - worker/crons/toga2/netsuite/sync_togasupply_adyen.php
@@ -47,7 +47,7 @@ Adyen live TOGa Supply client (`Client_Adyen`) on the 1.0 NetSuite→TOGa Supply
47
47
  - **NetSuite → TOGa Supply sync.** Per-client thin wrapper over the shared engine — full mechanics on [per-client sync](../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
48
48
  - **Transfer orders ENABLED** (`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS = true`). Adyen's NetSuite transfer orders arrive **as NetSuite sales orders**, classified by `TRANSFER_ORDER_DETECTION_MODE__ZERO_DOLLAR_HOLD` — `holdInvoice === true` **AND** `total === 0`, same rule as GroWrk (see [GroWrk transfer order flow](../growrk/features/transfer-order-flow.md)). Mode is now declared explicitly in Adyen's launcher; `App_Api_Toga2::isTransferOrder()` throws if it is missing, so **`library` and `worker` must deploy together** for Adyen.
49
49
  - **IDs:** `Core.Clients` id **48**; NetSuite customer **31054** ("4950 Adyen"); Adyen API = `Apis` id 4.
50
- - **Reclaim SR → Sales Order → NetSuite** (built 2026-09-30, not deployed): same chain as Elite, items from a seeded "Reclaim" bundle because Adyen sends no serials — see [Adyen reclaim flow](./features/service-request-salesorder-netsuite.md).
50
+ - **Reclaim SR → Sales Order → NetSuite**: same chain as Elite, items from a seeded "Reclaim" bundle (Adyen sends no serials); NetSuite PO # = employee name — see [Adyen reclaim flow](./features/service-request-salesorder-netsuite.md) (also holds the 8-order backlog map).
51
51
  - **`Client_Adyen`** is one of the client DBs with surface-layer / ACL drift noted on the [ACL permission chain](../../2.0/apps/_underscore/features/acl-permission-chain.md) and [surface-layer schema](../../2.0/apps/dbchanges2/features/surface-layer-schema.md) docs.
52
52
 
53
53
  ## Gotchas
@@ -84,10 +84,8 @@ Adyen tests the tracking API on **prod**. `2026-09-22a - AdyenApiTestSeed.sql` s
84
84
  **Known data gaps (2026-09-28):**
85
85
 
86
86
  1. SR100024 duplicates SR100021 (same Adyen ref 48748808) — a lookup by that ref returns **2 SRs**.
87
- 2. SR100026 → NS SO 290879, not fulfilled yet.
88
- 3. SR100027–30 have no NS SO.
89
- 4. NS SO 288261 has no SR in Toga.
90
- 5. SR100000/01/02/25 are Adyen test SRs with no NS order.
87
+ 2. Items 2–4 of the 2026-09-28 list (SR100026, SR100027–30, NS SO 288261) are superseded by the **2026-10-08 backlog map** — 8 Reclaim SRs ↔ NS SOs, 290879 + 288261 hand-entered and not linked in Toga. See [reclaim flow → Backlog](./features/service-request-salesorder-netsuite.md#backlog--8-reclaim-requests--netsuite-sos-2026-10-08).
88
+ 3. SR100000/01/02/25 are Adyen test SRs with no NS order.
91
89
 
92
90
  ### Open defect — `Client_Adyen.Items` missing `isFulfillable` (prod AND beta)
93
91
 
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Compass USA Talos Supply Assistant
3
+ framework: "standalone"
4
+ repo: talos-backend
5
+ project: TOGa Talos
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-10-08
10
+ owners: [akhokhani]
11
+ files:
12
+ - talos-backend/deployments/tenants/compass-usa/manifest.json
13
+ - talos-backend/deployments/tenants/compass-usa/persona.txt
14
+ - talos-backend/deployments/tenants/compass-usa/toga-supply.blp
15
+ - talos-backend/deployments/tenants/compass-usa/candidates/2026-10-07/instruction-publication.json
16
+ - talos-backend/mcp-servers/toga-platform-mcp/src/toga_platform_mcp/tools/supply/view_query.py
17
+ related:
18
+ - ../profile.md
19
+ - ../../../standalone/apps/talos-backend/workflows/toga-supply-client-onboarding.md
20
+ - ../../../standalone/apps/talos-backend/features/toga-platform-mcp.md
21
+ - ../../../2.0/apps/toga25-supply/features/talos-integration.md
22
+ - ../../../standalone/apps/talos-backend/features/tenant-configuration.md
23
+ ---
24
+
25
+ Compass USA's Talos assistant answers read-only Supply questions under the user's api2 permissions; open for tenant identity, published instruction evidence and launch checks.
26
+
27
+ ## Summary
28
+
29
+ Canonical org is `Compass_Usa`, external slug/name `Compass Group`, database `tenant_compass_usa`. Assistant `toga-supply` (“TOGa Supply”) uses the shared `deep_agent`, not a client-specific graph. This page owns Compass differences; shared wiring and provisioning live in the linked Supply/MCP docs.
30
+
31
+ ## How it works
32
+
33
+ The saved production survey **as of 2026-10-07** found assistant `9d4b80d2-e592-5e75-bd0d-c09994d93e56` at version 8 with Sonnet 5 caching. Customer-facing/read-only metadata was set. Limits were 32k context, recursion 32, 12 model calls/run, 120/thread and 24 tool calls/run. Plan/HITL/CI/artifacts/skills/writing blocks/tool routing were off at assistant level; spend governance and BLP config were on.
34
+
35
+ Persona `toga-supply` had the same stored instruction length as legacy assistant `persona_prompt`, an embedding, orchestration off, category `blp`, domain TOGa Supply Operations and MCP `toga-platform`. Its exact native tool junction contained only `query_business_logic`; catalog MCP link contained only user-bearer toga-platform. There were no KB/skill/starter assignments. Shared platform MCP selects Supply versus Commerce from authenticated request context; adding an enabled catalog row alone is not the customer's authorized tool set.
36
+
37
+ The October 7 candidate/publication record stored assistant v8, BLP **1.3.0 with 12 blocks**, 1024-dimensional embedding, preserved settings/assignments and successful DB readback. It explicitly recorded **`live_answer_tested:false`**. Persona/BLP publication and MCP code deployment are independent: local `view_query.py` grounds UI questions in authorized views, but the saved research did not identify the deployed MCP source SHA.
38
+
39
+ ### Customer answer rules
40
+
41
+ Use customer terminology Kit/Bundle, authorized UI status stages from Pending Initial Approval through Fulfilled, and short read-only answers. Omit vendor/purchasing internals from customer responses. UI links follow the actual TableView filter contract: `/sales-orders?sales-orders=...`, `/items?items=...`, `/bundles?bundles=...`. Do not substitute raw API fields for UI filter/status semantics. Compass Canada is outside this tenant's scope.
42
+
43
+ ### Launch evidence and open gates
44
+
45
+ Production backend instructions were read back and the survey counted 73 successful runs over its preceding 30 days. This proves backend use, not a launched Supply panel. Production Core/Client Surface grants were not inspected, and the handoff explicitly did not change panel enablement or access grants. Validate the intended non-admin user's `talos-assistant` bundle and active `/users/me` slug entitlement before calling launch complete.
46
+
47
+ Supply's inspected production provider still lacked wired server cancellation, persisted transcript reload and run-ID rejoin. These cross-app gates remain in [Supply integration](../../../2.0/apps/toga25-supply/features/talos-integration.md).
48
+
49
+ ## Gotchas
50
+
51
+ - Tenant `feature_flags.blp=false` conflicts with assistant BLP/junction configuration. Assembly checks both; no `query_business_logic` calls were observed in the survey's last 30 days. Treat BLP execution as needing a runtime check.
52
+ - Tenant `mcp=false` did not prevent observed platform tool calls. A boolean catalog/flag audit cannot replace per-run context and business ACL verification.
53
+ - Enabled static-key ClickUp/toga-db catalog rows were present but unbound to this customer assistant. Review their tenant visibility before exposing another persona.
54
+ - `model_defaults.summarization=sonnet-4-5` was absent from its AIP registry; assistant Haiku 4.5 masked that fallback problem. Effective config, not a single row, determines behavior.
55
+ - Instruction publication does not certify deployed MCP source, customer access or answer quality. Keep all three evidence records separate.
56
+
57
+ ## Related
58
+
59
+ - [profile](../profile.md)
60
+ - [toga supply client onboarding](../../../standalone/apps/talos-backend/workflows/toga-supply-client-onboarding.md)
61
+ - [toga platform mcp](../../../standalone/apps/talos-backend/features/toga-platform-mcp.md)
62
+ - [talos integration](../../../2.0/apps/toga25-supply/features/talos-integration.md)
63
+ - [tenant configuration](../../../standalone/apps/talos-backend/features/tenant-configuration.md)
64
+
65
+ ## Change history
66
+
67
+ - 2026-10-08 — Reconciled local source and dated 2026-10-07 research into the canonical KB (akhokhani).