toga-ai 1.0.70 → 1.0.72
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.
|
@@ -42,10 +42,16 @@ Per run, after acquiring a Graph client-credentials token:
|
|
|
42
42
|
- **Fallback:** the comma-separated `[teams] organizer_user_ids` list (used only when no group
|
|
43
43
|
is configured or the group resolves no members; group-lookup failures are caught so they
|
|
44
44
|
can't abort the run).
|
|
45
|
-
2. Per organizer
|
|
45
|
+
2. **Per organizer**, `resolveUserId()` ensures a GUID: a value already shaped like a GUID
|
|
46
|
+
(the group path) passes through; a UPN is looked up via `/users/{upn}?$select=id` and
|
|
47
|
+
returns the object-id, or **`null`** if that lookup fails. The loop **fails fast** on null —
|
|
48
|
+
it throws a clear per-organizer error (caught and recorded in `summary['errors']`, the run
|
|
49
|
+
continues for other organizers) rather than sending a UPN to Graph. `listAllTranscripts()`
|
|
50
|
+
also re-asserts the GUID shape before embedding it in the OData literal.
|
|
51
|
+
3. `getAllTranscripts(meetingOrganizerUserId='{guid}', startDateTime, endDateTime)`
|
|
46
52
|
over an incremental window (`MAX(dtCreated)` watermark from the ledger, else `now − lookbackDays`),
|
|
47
53
|
following `@odata.nextLink`.
|
|
48
|
-
|
|
54
|
+
4. Per new transcript (dedup by `transcriptIdentifier`): fetch meeting `subject`+`startDateTime`
|
|
49
55
|
(cached per meeting), classify, download VTT (`?$format=text/vtt`), `putObject` to S3, insert
|
|
50
56
|
ledger row.
|
|
51
57
|
|
|
@@ -91,8 +97,13 @@ process is identical for all.
|
|
|
91
97
|
producing the same 400. Lesson: a "fix" that depends on an ungranted permission and silently
|
|
92
98
|
falls back will look correct in code review but never work in prod. The group-members approach
|
|
93
99
|
avoids the `/users` endpoint entirely.
|
|
94
|
-
- `resolveUserId()`
|
|
95
|
-
|
|
100
|
+
- `resolveUserId()` returns `?string`: a GUID passes through, a UPN is resolved via `/users`,
|
|
101
|
+
and it returns **`null`** when a non-GUID can't be resolved — it never echoes the UPN back.
|
|
102
|
+
The Export loop rejects null and skips that organizer with a clear error, so a UPN can never
|
|
103
|
+
reach Graph. `listAllTranscripts()` re-asserts the GUID shape (`preg_match`) before embedding
|
|
104
|
+
the id in the OData string literal — defense-in-depth so the no-injection invariant can't
|
|
105
|
+
silently regress. Note: the `organizer_user_ids` UPN fallback only actually works in a tenant
|
|
106
|
+
that grants `User.Read.All`; in this tenant it does not, so the group is the only working source.
|
|
96
107
|
- Graph meeting/transcript ids are base64 and contain `+ / =` — they must be `rawurlencode()`'d
|
|
97
108
|
at every Graph call site (`getMeeting`, `downloadTranscriptVtt` fallback URL).
|
|
98
109
|
- `resolveWatermark()` guards against an unparseable stored `dtCreated`: without the guard,
|
|
@@ -102,17 +113,25 @@ process is identical for all.
|
|
|
102
113
|
|
|
103
114
|
## Diagnosing a 400
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
When a 400 recurs, reproduce the Graph call chain **outside** the worker, step-by-step
|
|
117
|
+
(OAuth token → `GET /groups/{id}/members/microsoft.graph.user?$select=id` → `getAllTranscripts`
|
|
118
|
+
with one member's object-id) with curl, reading creds from `Config/production.ini` `[teams]`.
|
|
119
|
+
This surfaces the **full** Graph error body (`error.code` + `innerError`) that the worker
|
|
120
|
+
discards — `curlRequest()` keeps only `error.message`, which is why prod logs showed an opaque
|
|
121
|
+
`UnknownError`. A `400 UnknownError` on a *valid GUID* points to a Teams Application Access
|
|
122
|
+
Policy gap; a `400` only on a UPN means the id was never resolved to a GUID. (A throwaway
|
|
123
|
+
`diagnose-transcripts.sh` was used this way during the fix but is not committed.)
|
|
109
124
|
|
|
110
125
|
## Change history
|
|
111
126
|
|
|
127
|
+
- 2026-06-12 — **Merged (PR #78) and verified in production**: a real `Team/Transcripts/Export`
|
|
128
|
+
run returned 6/6 organizers, 33 found, 33 exported, 0 errors (classified 30 general / 3 Elite).
|
|
129
|
+
Added fail-fast on unresolvable organizers (`resolveUserId` → null, loop throws clear error
|
|
130
|
+
instead of sending a UPN to Graph) and a GUID assertion in `listAllTranscripts`. (ajean)
|
|
112
131
|
- 2026-06-12 — Resolve organizers from the `organizer_group_id` Entra group (object-ids via
|
|
113
132
|
GroupMember.Read.All) instead of per-UPN lookup; fixes the recurring HTTP 400 that PR #77's
|
|
114
133
|
`/users` approach couldn't (no User.Read.All). Hardened watermark parsing, URL-encoding, and
|
|
115
|
-
silent catches.
|
|
134
|
+
silent catches. (ajean)
|
|
116
135
|
|
|
117
136
|
## Related docs
|
|
118
137
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
5
|
| [Compass ASN → ItemFulfillment Auto-Creation](features/asn-to-item-fulfillment.md) | 2.0 | For Compass USA, posting an AdvanceShippingNotice (ASN) auto-creates the ItemFulfillment (IF) on the upstream SalesOrder. | _underscore/Model/Compass/AdvanceShippingNotice.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client_Compass/2026-06-11 - AsnItemTrackingNumberAcl.sql |
|
|
6
|
-
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql |
|
|
6
|
+
| [Compass: Item-Fulfillment TableViews (for-sales-order-items & for-sales-orders, tracking via bridge)](features/item-fulfillment-tracking-tableview.md) | 2.0 | Two sibling Compass TableViews in `Client_Compass` display fulfilled items in toga2-supply, both driven by `TableViews` / `TableViewJoins` / `TableViewFields` c | dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql, dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql, dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql |
|
|
7
7
|
| [Compass MITS PO → SO Item Linking](features/mits-po-to-so-item-linking.md) | 2.0 | MITS sends Compass inbound Purchase Orders (`POST /v2/purchase-orders`) against a Sales Order (`mitsSalesOrder`). | _underscore/Model/Compass/PurchaseOrder.php |
|
|
8
8
|
| [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
|
|
9
9
|
| [Compass Order Lifecycle & Data-Integrity Invariants](workflows/order-lifecycle-and-data-integrity.md) | 2.0 | End-to-end map of how a Compass order flows through the `Client_Compass` (2.0) database and the **expected raw-data shape** at each link/ASN/IF level. | |
|
|
@@ -5,11 +5,12 @@ project: _Underscore
|
|
|
5
5
|
client: compass-usa
|
|
6
6
|
type: client-feature
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-06-
|
|
8
|
+
updated: 2026-06-15
|
|
9
9
|
owners: ["jcardinal"]
|
|
10
10
|
files:
|
|
11
11
|
- dbchanges2/Client_Compass/2026-06-10 - ItemFulfillmentsForSalesOrderItemsTableView.sql
|
|
12
12
|
- dbchanges2/Client_Compass/2026-06-11 - ItemFulfillmentsForSalesOrdersTableView.sql
|
|
13
|
+
- dbchanges2/Client_Compass/2026-06-15a - FixItemFulfillmentTrackingNumberJoins.sql
|
|
13
14
|
related:
|
|
14
15
|
- ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
|
|
15
16
|
---
|
|
@@ -35,8 +36,9 @@ Both views are re-rooted at **`ItemFulfillmentItems` (record 29)** so every fulf
|
|
|
35
36
|
with the unit/tracking chain made **OUTER** so missing units no longer drop rows.
|
|
36
37
|
|
|
37
38
|
**View 13 — `item-fulfillments-for-sales-order-items`:**
|
|
38
|
-
- INNER `SalesOrderItems`; OUTER `ItemFulfillmentItemUnits` → OUTER `Units`
|
|
39
|
-
|
|
39
|
+
- INNER `SalesOrderItems`; OUTER `ItemFulfillmentItemUnits` → OUTER `Units` (kept for Serial #
|
|
40
|
+
/ Asset Tag columns); OUTER `ItemFulfillmentItems_TrackingNumbers` (record **318**) joined
|
|
41
|
+
directly onto `ItemFulfillmentItems` → OUTER `TrackingNumbers` → OUTER `ShippingCarriers`.
|
|
40
42
|
- Columns: **Sales Order Line Number, Quantity Fulfilled (`ItemFulfillmentItems.quantity`),
|
|
41
43
|
Outbound Tracking #, Carrier, Serial #, Asset Tag.**
|
|
42
44
|
|
|
@@ -48,14 +50,19 @@ with the unit/tracking chain made **OUTER** so missing units no longer drop rows
|
|
|
48
50
|
- Default sort re-pointed to the line-number field.
|
|
49
51
|
|
|
50
52
|
Stable Core ids used: records 29 IFI, 30 IFIU, 31 Units, 15 SalesOrderItems, 14 SalesOrders,
|
|
51
|
-
28 ItemFulfillments, 21 Items,
|
|
53
|
+
28 ItemFulfillments, 21 Items, **318 IFI-tracking bridge** (item-level), 62 TrackingNumbers,
|
|
54
|
+
9 ShippingCarriers. (Record 319, the IFIU/unit-level bridge, is no longer used for tracking —
|
|
55
|
+
see Change history 2026-06-15.)
|
|
52
56
|
|
|
53
57
|
## Data model
|
|
54
58
|
|
|
55
59
|
Join keys (RecordField ids): 60 SOI.id / 368 IFI.salesOrderItemId; 360 IF.id / 367 IFI.itemFulfillmentId;
|
|
56
60
|
59 SO.id / 254 SOI.salesOrderId; 105 Item.id / 66 SOI.itemId; 374 IFIU.itemFulfillmentItemId / 365 IFI.id;
|
|
57
|
-
236 Unit.id / 375 IFIU.unitId;
|
|
58
|
-
336 TN.id /
|
|
61
|
+
236 Unit.id / 375 IFIU.unitId; **2178 IFI-bridge.itemFulfillmentItemId / 365 IFI.id**;
|
|
62
|
+
336 TN.id / **2179 IFI-bridge.trackingNumberId**; 21 SC.id / 613 TN.shippingCarrierId.
|
|
63
|
+
(Record 318 fields: 2176 id, 2178 itemFulfillmentItemId, 2179 trackingNumberId, 2180
|
|
64
|
+
returnTrackingNumberId. Superseded unit-level keys: 2183 IFIU-bridge.itemFulfillmentItemUnitId
|
|
65
|
+
/ 372 IFIU.id; 2184 IFIU-bridge.trackingNumberId.)
|
|
59
66
|
|
|
60
67
|
## Gotchas / known issues
|
|
61
68
|
|
|
@@ -64,9 +71,22 @@ Join keys (RecordField ids): 60 SOI.id / 368 IFI.salesOrderItemId; 360 IF.id / 3
|
|
|
64
71
|
ItemFulfillmentItemUnits / 0 Units** → view 12 showed "0 records" while the re-rooted sibling
|
|
65
72
|
view 13 showed the items fine. Fix is structural (re-root + OUTER chain), independent of the
|
|
66
73
|
missing-unit data issue.
|
|
67
|
-
- Tracking
|
|
68
|
-
|
|
69
|
-
(
|
|
74
|
+
- **Tracking must be sourced at the ITEM level** via `ItemFulfillmentItems_TrackingNumbers`
|
|
75
|
+
(record 318), joined directly onto `ItemFulfillmentItems` (rec 29). The original re-root
|
|
76
|
+
(2026-06-10/11) left tracking on the **unit-level** bridge `ItemFulfillmentItemUnits_TrackingNumbers`
|
|
77
|
+
(record 319), which is empty for **non-serialized** lines (no `ItemFulfillmentItemUnits` rows) →
|
|
78
|
+
the OUTER join yielded NULL and the tracking column rendered blank even though the data was
|
|
79
|
+
correct. Symptom: SO **MR243437** (id 105730) showed tracking at the order level but not the
|
|
80
|
+
item level; item 266006 had 0 unit rows but a valid `ItemFulfillmentItems_TrackingNumbers` row
|
|
81
|
+
(→ TN 67248). Fixed 2026-06-15.
|
|
82
|
+
- **Compass imports exactly one tracking number per item fulfillment** (NetSuite does not support
|
|
83
|
+
multiple tracking numbers at the unit level; a different tracking number arrives as a separate
|
|
84
|
+
item fulfillment). So the item-level bridge (318) is always the complete source and re-pointing
|
|
85
|
+
away from the unit-level bridge (319) loses nothing.
|
|
86
|
+
- Re-pointing the bridge is **not** a one-column swap: records 318 and 319 have different
|
|
87
|
+
`RecordFields` ids, so `joinRecordId` (319→318), `joinOnRecordFieldId` (2183→2178),
|
|
88
|
+
`parentRecordFieldId` (372→365), and the downstream `TrackingNumbers` join's parent field
|
|
89
|
+
(2184→2179) all change in lockstep.
|
|
70
90
|
- `TableViews.sortPrimaryTableViewFieldId` is a FK to `TableViewFields`; set it NULL before deleting
|
|
71
91
|
the old fields, then re-point via a multi-table UPDATE (not a self-subquery) to avoid MySQL 1093.
|
|
72
92
|
- `TableViewFields` must be deleted before `TableViewJoins` (`tableViewJoinId` FK).
|
|
@@ -78,6 +98,7 @@ Join keys (RecordField ids): 60 SOI.id / 368 IFI.salesOrderItemId; 360 IF.id / 3
|
|
|
78
98
|
Compass-only. The underlying bridge model is shared (see the _underscore feature doc).
|
|
79
99
|
|
|
80
100
|
## Change history
|
|
101
|
+
- 2026-06-15 — Re-pointed tracking from the unit-level bridge (319) to the item-level bridge `ItemFulfillmentItems_TrackingNumbers` (318) in views 12 & 13; fixes blank item-level tracking on non-serialized lines (e.g. SO MR243437). Migration `2026-06-15a`. (jcardinal)
|
|
81
102
|
- 2026-06-11 — Re-rooted `item-fulfillments-for-sales-orders` (id 12) from Units to ItemFulfillmentItems with OUTER unit/tracking chain; fixes 0-records on orders lacking unit breakdown (e.g. SA132740). (jcardinal)
|
|
82
103
|
- 2026-06-10 — Re-rooted `item-fulfillments-for-sales-order-items` (id 13) and moved tracking to the bridge join. (jcardinal)
|
|
83
104
|
|
package/package.json
CHANGED