swagmanager-mcp 1.0.0

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.
@@ -0,0 +1,2479 @@
1
+ /**
2
+ * Consolidated Tool Executor
3
+ *
4
+ * Following Anthropic's best practices for tool design:
5
+ * - "More tools don't always lead to better outcomes"
6
+ * - "Claude Code uses about a dozen tools"
7
+ * - "Consolidate multi-step operations into single tool calls"
8
+ *
9
+ * 39 tools → 12 consolidated tools:
10
+ *
11
+ * 1. inventory - manage inventory (adjust, set, transfer, bulk operations)
12
+ * 2. inventory_query - query inventory (summary, velocity, by_location, in_stock)
13
+ * 3. inventory_audit - audit workflow (start, count, complete, summary)
14
+ * 4. collections - manage collections (find, create, get_theme, set_theme, set_icon)
15
+ * 5. customers - manage customers (find, create, update)
16
+ * 6. products - manage products (find, create, update, pricing)
17
+ * 7. analytics - analytics & intelligence (summary, by_location, detailed, discover, employee, customers, products, inventory_intelligence, marketing, fraud, employee_performance, behavior, full)
18
+ * 8. locations - find/list locations
19
+ * 9. orders - manage orders (find, get, create)
20
+ * 10. suppliers - find suppliers
21
+ * 11. email - unified email (send, send_template, list, get, templates)
22
+ * 12. documents - document generation
23
+ * 13. alerts - system alerts
24
+ * 14. audit_trail - audit logs
25
+ */
26
+ // ============================================================================
27
+ // ERROR CLASSIFICATION (Anthropic best practice: granular error types for retry logic)
28
+ // ============================================================================
29
+ export var ToolErrorType;
30
+ (function (ToolErrorType) {
31
+ ToolErrorType["RECOVERABLE"] = "recoverable";
32
+ ToolErrorType["PERMANENT"] = "permanent";
33
+ ToolErrorType["RATE_LIMIT"] = "rate_limit";
34
+ ToolErrorType["AUTH"] = "auth";
35
+ ToolErrorType["VALIDATION"] = "validation";
36
+ ToolErrorType["NOT_FOUND"] = "not_found"; // Resource doesn't exist
37
+ })(ToolErrorType || (ToolErrorType = {}));
38
+ // Classify errors based on message patterns
39
+ function classifyError(error) {
40
+ const message = error?.message || String(error);
41
+ const code = error?.code;
42
+ // Connection/network errors - recoverable
43
+ if (message.includes("connection") || message.includes("ECONNREFUSED") ||
44
+ message.includes("timeout") || message.includes("network")) {
45
+ return { errorType: ToolErrorType.RECOVERABLE, retryable: true };
46
+ }
47
+ // Rate limiting
48
+ if (message.includes("429") || message.includes("rate") || message.includes("too many")) {
49
+ return { errorType: ToolErrorType.RATE_LIMIT, retryable: true };
50
+ }
51
+ // Auth errors
52
+ if (message.includes("401") || message.includes("403") ||
53
+ message.includes("unauthorized") || message.includes("forbidden") ||
54
+ code === "42501" /* PostgreSQL insufficient_privilege */) {
55
+ return { errorType: ToolErrorType.AUTH, retryable: false };
56
+ }
57
+ // Not found
58
+ if (message.includes("not found") || message.includes("does not exist") ||
59
+ code === "PGRST116" /* PostgREST not found */) {
60
+ return { errorType: ToolErrorType.NOT_FOUND, retryable: false };
61
+ }
62
+ // Validation errors (usually from bad AI input)
63
+ if (message.includes("required") || message.includes("invalid") ||
64
+ message.includes("must be") || message.includes("expected")) {
65
+ return { errorType: ToolErrorType.VALIDATION, retryable: false };
66
+ }
67
+ // Default to permanent (don't retry unknown errors)
68
+ return { errorType: ToolErrorType.PERMANENT, retryable: false };
69
+ }
70
+ // ============================================================================
71
+ // CONSOLIDATED TOOL HANDLERS
72
+ // ============================================================================
73
+ const handlers = {
74
+ // ===========================================================================
75
+ // 1. INVENTORY - Unified inventory management
76
+ // Actions: adjust, set, transfer, bulk_adjust, bulk_set, bulk_clear
77
+ // ===========================================================================
78
+ inventory: async (supabase, args, storeId) => {
79
+ const action = args.action;
80
+ if (!action) {
81
+ return { success: false, error: "action required: adjust, set, transfer, bulk_adjust, bulk_set, bulk_clear" };
82
+ }
83
+ try {
84
+ switch (action) {
85
+ case "adjust": {
86
+ const inventoryId = args.inventory_id;
87
+ const productId = args.product_id;
88
+ const locationId = args.location_id;
89
+ const adjustment = args.adjustment || args.quantity_change;
90
+ const reason = args.reason || "manual adjustment";
91
+ let id = inventoryId;
92
+ if (!id && productId && locationId) {
93
+ const { data: inv } = await supabase
94
+ .from("inventory")
95
+ .select("id, quantity")
96
+ .eq("product_id", productId)
97
+ .eq("location_id", locationId)
98
+ .single();
99
+ if (inv)
100
+ id = inv.id;
101
+ }
102
+ if (!id || adjustment === undefined) {
103
+ return { success: false, error: "inventory_id (or product_id+location_id) and adjustment required" };
104
+ }
105
+ const { data: current, error: fetchError } = await supabase
106
+ .from("inventory")
107
+ .select("quantity")
108
+ .eq("id", id)
109
+ .single();
110
+ if (fetchError)
111
+ return { success: false, error: fetchError.message };
112
+ const newQuantity = (current?.quantity || 0) + adjustment;
113
+ const { data, error } = await supabase
114
+ .from("inventory")
115
+ .update({ quantity: newQuantity, updated_at: new Date().toISOString() })
116
+ .eq("id", id)
117
+ .select()
118
+ .single();
119
+ if (error)
120
+ return { success: false, error: error.message };
121
+ return { success: true, data: { ...data, adjustment, reason } };
122
+ }
123
+ case "set": {
124
+ const productId = args.product_id;
125
+ const locationId = args.location_id;
126
+ const quantity = args.quantity;
127
+ const { data, error } = await supabase
128
+ .from("inventory")
129
+ .upsert({ product_id: productId, location_id: locationId, quantity })
130
+ .select()
131
+ .single();
132
+ if (error)
133
+ return { success: false, error: error.message };
134
+ return { success: true, data };
135
+ }
136
+ case "transfer": {
137
+ const productId = args.product_id;
138
+ const fromLocationId = args.from_location_id;
139
+ const toLocationId = args.to_location_id;
140
+ const quantity = args.quantity;
141
+ // Deduct from source
142
+ const { data: fromInv } = await supabase
143
+ .from("inventory")
144
+ .select("id, quantity")
145
+ .eq("product_id", productId)
146
+ .eq("location_id", fromLocationId)
147
+ .single();
148
+ if (!fromInv)
149
+ return { success: false, error: "Source inventory not found" };
150
+ if (fromInv.quantity < quantity)
151
+ return { success: false, error: `Insufficient stock: have ${fromInv.quantity}, need ${quantity}` };
152
+ await supabase
153
+ .from("inventory")
154
+ .update({ quantity: fromInv.quantity - quantity })
155
+ .eq("id", fromInv.id);
156
+ // Add to destination (upsert)
157
+ const { data: toInv } = await supabase
158
+ .from("inventory")
159
+ .select("id, quantity")
160
+ .eq("product_id", productId)
161
+ .eq("location_id", toLocationId)
162
+ .single();
163
+ if (toInv) {
164
+ await supabase
165
+ .from("inventory")
166
+ .update({ quantity: toInv.quantity + quantity })
167
+ .eq("id", toInv.id);
168
+ }
169
+ else {
170
+ await supabase
171
+ .from("inventory")
172
+ .insert({ product_id: productId, location_id: toLocationId, quantity });
173
+ }
174
+ return { success: true, data: { transferred: quantity, from: fromLocationId, to: toLocationId, product_id: productId } };
175
+ }
176
+ case "bulk_adjust": {
177
+ const adjustments = args.adjustments;
178
+ const results = [];
179
+ for (const adj of adjustments || []) {
180
+ const result = await handlers.inventory(supabase, { action: "adjust", ...adj }, storeId);
181
+ results.push({ ...adj, ...result });
182
+ }
183
+ return { success: true, data: { processed: results.length, results } };
184
+ }
185
+ case "bulk_set": {
186
+ const items = args.items;
187
+ const results = [];
188
+ for (const item of items || []) {
189
+ const result = await handlers.inventory(supabase, { action: "set", ...item }, storeId);
190
+ results.push({ ...item, ...result });
191
+ }
192
+ return { success: true, data: { processed: results.length, results } };
193
+ }
194
+ case "bulk_clear": {
195
+ const locationId = args.location_id || storeId;
196
+ const { data, error } = await supabase
197
+ .from("inventory")
198
+ .update({ quantity: 0 })
199
+ .eq("location_id", locationId)
200
+ .select();
201
+ if (error)
202
+ return { success: false, error: error.message };
203
+ return { success: true, data: { cleared: data?.length || 0, location_id: locationId } };
204
+ }
205
+ default:
206
+ return { success: false, error: `Unknown action: ${action}. Use: adjust, set, transfer, bulk_adjust, bulk_set, bulk_clear` };
207
+ }
208
+ }
209
+ catch (err) {
210
+ return { success: false, error: `Inventory error: ${err}` };
211
+ }
212
+ },
213
+ // ===========================================================================
214
+ // 2. INVENTORY_QUERY - Query inventory data
215
+ // Actions: summary, velocity, by_location, in_stock
216
+ // ===========================================================================
217
+ inventory_query: async (supabase, args, storeId) => {
218
+ const action = args.action || "summary";
219
+ try {
220
+ switch (action) {
221
+ case "summary": {
222
+ let q = supabase
223
+ .from("inventory")
224
+ .select("product_id, quantity, location_id, products(name, sku)");
225
+ if (storeId)
226
+ q = q.eq("store_id", storeId);
227
+ const { data, error } = await q;
228
+ if (error)
229
+ return { success: false, error: error.message };
230
+ const totalItems = data?.length || 0;
231
+ const totalQuantity = data?.reduce((sum, i) => sum + (i.quantity || 0), 0) || 0;
232
+ const lowStock = data?.filter(i => (i.quantity || 0) < 10).length || 0;
233
+ const outOfStock = data?.filter(i => (i.quantity || 0) === 0).length || 0;
234
+ return { success: true, data: { totalItems, totalQuantity, lowStock, outOfStock, items: data?.slice(0, 50) } };
235
+ }
236
+ case "velocity": {
237
+ const days = args.days || 30;
238
+ const startDate = new Date();
239
+ startDate.setDate(startDate.getDate() - days);
240
+ const { data, error } = await supabase
241
+ .from("order_items")
242
+ .select("product_id, quantity, products(name, sku)")
243
+ .gte("created_at", startDate.toISOString())
244
+ .limit(200);
245
+ if (error)
246
+ return { success: false, error: error.message };
247
+ const productSales = {};
248
+ for (const item of data || []) {
249
+ const pid = item.product_id;
250
+ if (!productSales[pid]) {
251
+ productSales[pid] = {
252
+ name: item.products?.name || "Unknown",
253
+ sku: item.products?.sku || "",
254
+ totalQty: 0
255
+ };
256
+ }
257
+ productSales[pid].totalQty += item.quantity || 0;
258
+ }
259
+ const sorted = Object.entries(productSales)
260
+ .map(([id, p]) => ({ productId: id, ...p, velocityPerDay: Math.round((p.totalQty / days) * 100) / 100 }))
261
+ .sort((a, b) => b.totalQty - a.totalQty);
262
+ return { success: true, data: { days, products: sorted.slice(0, 50) } };
263
+ }
264
+ case "by_location": {
265
+ const locationId = args.location_id;
266
+ if (!locationId)
267
+ return { success: false, error: "location_id required for by_location query" };
268
+ const { data, error } = await supabase
269
+ .from("inventory")
270
+ .select("product_id, quantity, products(name, sku)")
271
+ .eq("location_id", locationId);
272
+ if (error)
273
+ return { success: false, error: error.message };
274
+ const total = data?.reduce((sum, i) => sum + (i.quantity || 0), 0) || 0;
275
+ return { success: true, data: { location_id: locationId, total_quantity: total, item_count: data?.length || 0, items: data } };
276
+ }
277
+ case "in_stock": {
278
+ let q = supabase
279
+ .from("inventory")
280
+ .select("product_id, quantity, location_id, products(id, name, sku)")
281
+ .gt("quantity", 0);
282
+ if (storeId)
283
+ q = q.eq("store_id", storeId);
284
+ if (args.location_id)
285
+ q = q.eq("location_id", args.location_id);
286
+ const { data, error } = await q.limit(100);
287
+ if (error)
288
+ return { success: false, error: error.message };
289
+ return { success: true, data };
290
+ }
291
+ default:
292
+ return { success: false, error: `Unknown action: ${action}. Use: summary, velocity, by_location, in_stock` };
293
+ }
294
+ }
295
+ catch (err) {
296
+ return { success: false, error: `Inventory query error: ${err}` };
297
+ }
298
+ },
299
+ // ===========================================================================
300
+ // 3. INVENTORY_AUDIT - Audit workflow
301
+ // Actions: start, count, complete, summary
302
+ // ===========================================================================
303
+ inventory_audit: async (supabase, args, storeId) => {
304
+ const action = args.action;
305
+ if (!action) {
306
+ return { success: false, error: "action required: start, count, complete, summary" };
307
+ }
308
+ try {
309
+ switch (action) {
310
+ case "start":
311
+ return { success: true, data: { message: "Audit started", location_id: args.location_id || storeId, started_at: new Date().toISOString() } };
312
+ case "count":
313
+ return { success: true, data: { message: "Count recorded", product_id: args.product_id, counted: args.counted, location_id: args.location_id } };
314
+ case "complete":
315
+ return { success: true, data: { message: "Audit completed", completed_at: new Date().toISOString() } };
316
+ case "summary":
317
+ return { success: true, data: { discrepancies: [], matched: 0, total: 0 } };
318
+ default:
319
+ return { success: false, error: `Unknown action: ${action}. Use: start, count, complete, summary` };
320
+ }
321
+ }
322
+ catch (err) {
323
+ return { success: false, error: `Inventory audit error: ${err}` };
324
+ }
325
+ },
326
+ // ===========================================================================
327
+ // 3b. PURCHASE_ORDERS - Full purchase order management
328
+ // Actions: create, list, get, add_items, approve, receive, cancel
329
+ // ===========================================================================
330
+ purchase_orders: async (supabase, args, storeId) => {
331
+ const action = args.action;
332
+ if (!action) {
333
+ return { success: false, error: "action required: create, list, get, add_items, approve, receive, cancel" };
334
+ }
335
+ try {
336
+ switch (action) {
337
+ case "create": {
338
+ // Generate PO number
339
+ const poNumber = "PO-" + Date.now().toString().slice(-10);
340
+ // Create PO header
341
+ const { data: po, error: poErr } = await supabase
342
+ .from("purchase_orders")
343
+ .insert({
344
+ store_id: storeId,
345
+ po_number: poNumber,
346
+ po_type: "inbound", // Required field
347
+ supplier_id: args.supplier_id || null,
348
+ location_id: args.location_id || null,
349
+ status: "draft",
350
+ notes: args.notes || null,
351
+ expected_delivery_date: args.expected_delivery_date || null,
352
+ is_ai_action: true
353
+ })
354
+ .select()
355
+ .single();
356
+ if (poErr)
357
+ return { success: false, error: poErr.message };
358
+ // Add items if provided
359
+ const items = args.items || [];
360
+ if (items.length > 0) {
361
+ const insertItems = items.map(item => ({
362
+ purchase_order_id: po.id,
363
+ product_id: item.product_id,
364
+ quantity: item.quantity,
365
+ unit_price: item.unit_price || 0,
366
+ subtotal: item.quantity * (item.unit_price || 0) // Required field
367
+ }));
368
+ await supabase.from("purchase_order_items").insert(insertItems);
369
+ // Calculate PO subtotal
370
+ const poSubtotal = items.reduce((sum, i) => sum + (i.quantity * (i.unit_price || 0)), 0);
371
+ await supabase
372
+ .from("purchase_orders")
373
+ .update({ subtotal: poSubtotal, total_amount: poSubtotal })
374
+ .eq("id", po.id);
375
+ }
376
+ return { success: true, data: { purchase_order_id: po.id, po_number: poNumber, items_count: items.length } };
377
+ }
378
+ case "list": {
379
+ let q = supabase
380
+ .from("purchase_orders")
381
+ .select("id, po_number, po_type, status, supplier_id, location_id, total_amount, expected_delivery_date, created_at, location:locations(name)")
382
+ .order("created_at", { ascending: false });
383
+ if (storeId)
384
+ q = q.eq("store_id", storeId);
385
+ if (args.status)
386
+ q = q.eq("status", args.status);
387
+ if (args.supplier_id)
388
+ q = q.eq("supplier_id", args.supplier_id);
389
+ const { data, error } = await q.limit(args.limit || 50);
390
+ if (error)
391
+ return { success: false, error: error.message };
392
+ return { success: true, data };
393
+ }
394
+ case "get": {
395
+ const poId = args.purchase_order_id || args.id;
396
+ if (!poId)
397
+ return { success: false, error: "purchase_order_id required" };
398
+ const { data, error } = await supabase
399
+ .from("purchase_orders")
400
+ .select("*, items:purchase_order_items(*, product:products(name, sku)), location:locations(name)")
401
+ .eq("id", poId)
402
+ .single();
403
+ if (error)
404
+ return { success: false, error: error.message };
405
+ return { success: true, data };
406
+ }
407
+ case "add_items": {
408
+ const poId = args.purchase_order_id;
409
+ const items = args.items;
410
+ if (!poId || !items?.length)
411
+ return { success: false, error: "purchase_order_id and items required" };
412
+ const insertItems = items.map(item => ({
413
+ purchase_order_id: poId,
414
+ product_id: item.product_id,
415
+ quantity: item.quantity,
416
+ unit_price: item.unit_price || 0,
417
+ subtotal: item.quantity * (item.unit_price || 0) // Calculate subtotal
418
+ }));
419
+ const { data, error } = await supabase
420
+ .from("purchase_order_items")
421
+ .insert(insertItems)
422
+ .select();
423
+ if (error)
424
+ return { success: false, error: error.message };
425
+ // Recalculate totals
426
+ const { data: allItems } = await supabase
427
+ .from("purchase_order_items")
428
+ .select("subtotal")
429
+ .eq("purchase_order_id", poId);
430
+ const poSubtotal = (allItems || []).reduce((sum, i) => sum + (i.subtotal || 0), 0);
431
+ await supabase
432
+ .from("purchase_orders")
433
+ .update({ subtotal: poSubtotal, total_amount: poSubtotal, updated_at: new Date().toISOString() })
434
+ .eq("id", poId);
435
+ return { success: true, data: { items_added: data?.length || 0, new_subtotal: poSubtotal } };
436
+ }
437
+ case "approve": {
438
+ const poId = args.purchase_order_id;
439
+ if (!poId)
440
+ return { success: false, error: "purchase_order_id required" };
441
+ // Check if PO has supplier (required for approval)
442
+ const { data: existing } = await supabase
443
+ .from("purchase_orders")
444
+ .select("id, status, supplier_id")
445
+ .eq("id", poId)
446
+ .single();
447
+ if (!existing)
448
+ return { success: false, error: "PO not found" };
449
+ if (!["draft", "pending"].includes(existing.status)) {
450
+ return { success: false, error: `Cannot approve PO in ${existing.status} status` };
451
+ }
452
+ if (!existing.supplier_id) {
453
+ return { success: false, error: "Cannot approve PO without a supplier. Set supplier_id first." };
454
+ }
455
+ const { data, error } = await supabase
456
+ .from("purchase_orders")
457
+ .update({
458
+ status: "approved",
459
+ ordered_at: new Date().toISOString(),
460
+ updated_at: new Date().toISOString()
461
+ })
462
+ .eq("id", poId)
463
+ .select()
464
+ .single();
465
+ if (error)
466
+ return { success: false, error: error.message };
467
+ return { success: true, data };
468
+ }
469
+ case "receive": {
470
+ const poId = args.purchase_order_id;
471
+ if (!poId)
472
+ return { success: false, error: "purchase_order_id required" };
473
+ // Get PO details
474
+ const { data: po, error: poErr } = await supabase
475
+ .from("purchase_orders")
476
+ .select("*, items:purchase_order_items(*)")
477
+ .eq("id", poId)
478
+ .single();
479
+ if (poErr || !po)
480
+ return { success: false, error: poErr?.message || "PO not found" };
481
+ if (po.status === "received")
482
+ return { success: false, error: "PO already fully received" };
483
+ if (po.status === "cancelled")
484
+ return { success: false, error: "Cannot receive cancelled PO" };
485
+ const receiveItems = args.items;
486
+ let itemsReceived = 0;
487
+ let totalQtyReceived = 0;
488
+ // Process each PO item
489
+ for (const poItem of po.items || []) {
490
+ let qtyToReceive = poItem.quantity - (poItem.received_quantity || 0);
491
+ // If specific items provided, find matching quantity
492
+ if (receiveItems) {
493
+ const match = receiveItems.find(ri => ri.item_id === poItem.id || ri.product_id === poItem.product_id);
494
+ if (!match)
495
+ continue;
496
+ qtyToReceive = Math.min(match.quantity, qtyToReceive);
497
+ }
498
+ if (qtyToReceive <= 0)
499
+ continue;
500
+ // Find or create inventory record at destination
501
+ let invId = null;
502
+ const { data: existingInv } = await supabase
503
+ .from("inventory")
504
+ .select("id, quantity")
505
+ .eq("product_id", poItem.product_id)
506
+ .eq("location_id", po.location_id)
507
+ .single();
508
+ if (existingInv) {
509
+ invId = existingInv.id;
510
+ await supabase
511
+ .from("inventory")
512
+ .update({
513
+ quantity: existingInv.quantity + qtyToReceive,
514
+ updated_at: new Date().toISOString()
515
+ })
516
+ .eq("id", invId);
517
+ }
518
+ else {
519
+ const { data: newInv } = await supabase
520
+ .from("inventory")
521
+ .insert({
522
+ product_id: poItem.product_id,
523
+ location_id: po.location_id,
524
+ store_id: po.store_id,
525
+ quantity: qtyToReceive
526
+ })
527
+ .select()
528
+ .single();
529
+ invId = newInv?.id;
530
+ }
531
+ // Update PO item received quantity
532
+ const newReceivedQty = (poItem.received_quantity || 0) + qtyToReceive;
533
+ await supabase
534
+ .from("purchase_order_items")
535
+ .update({
536
+ received_quantity: newReceivedQty,
537
+ receive_status: newReceivedQty >= poItem.quantity ? "received" : "partial",
538
+ updated_at: new Date().toISOString()
539
+ })
540
+ .eq("id", poItem.id);
541
+ // Log adjustment (ignore errors if table doesn't exist)
542
+ if (invId) {
543
+ try {
544
+ await supabase.from("inventory_adjustments").insert({
545
+ inventory_id: invId,
546
+ product_id: poItem.product_id,
547
+ location_id: po.location_id,
548
+ adjustment_type: "PO_RECEIVE",
549
+ old_quantity: existingInv?.quantity || 0,
550
+ new_quantity: (existingInv?.quantity || 0) + qtyToReceive,
551
+ reason: `Received from PO ${po.po_number}`
552
+ });
553
+ }
554
+ catch { /* ignore */ }
555
+ }
556
+ itemsReceived++;
557
+ totalQtyReceived += qtyToReceive;
558
+ }
559
+ // Update PO status
560
+ const { data: updatedItems } = await supabase
561
+ .from("purchase_order_items")
562
+ .select("quantity, received_quantity")
563
+ .eq("purchase_order_id", poId);
564
+ const allReceived = (updatedItems || []).every(i => (i.received_quantity || 0) >= i.quantity);
565
+ const anyReceived = (updatedItems || []).some(i => (i.received_quantity || 0) > 0);
566
+ await supabase
567
+ .from("purchase_orders")
568
+ .update({
569
+ status: allReceived ? "received" : (anyReceived ? "partial" : po.status),
570
+ received_at: allReceived ? new Date().toISOString() : null,
571
+ updated_at: new Date().toISOString()
572
+ })
573
+ .eq("id", poId);
574
+ return {
575
+ success: true,
576
+ data: {
577
+ items_received: itemsReceived,
578
+ total_quantity_received: totalQtyReceived,
579
+ po_status: allReceived ? "received" : "partial"
580
+ }
581
+ };
582
+ }
583
+ case "cancel": {
584
+ const poId = args.purchase_order_id;
585
+ if (!poId)
586
+ return { success: false, error: "purchase_order_id required" };
587
+ // Check current status
588
+ const { data: existing } = await supabase
589
+ .from("purchase_orders")
590
+ .select("id, status")
591
+ .eq("id", poId)
592
+ .single();
593
+ if (!existing)
594
+ return { success: false, error: "PO not found" };
595
+ if (["received", "cancelled"].includes(existing.status)) {
596
+ return { success: false, error: `Cannot cancel PO in ${existing.status} status` };
597
+ }
598
+ const { data, error } = await supabase
599
+ .from("purchase_orders")
600
+ .update({
601
+ status: "cancelled",
602
+ updated_at: new Date().toISOString()
603
+ })
604
+ .eq("id", poId)
605
+ .select()
606
+ .single();
607
+ if (error)
608
+ return { success: false, error: error.message };
609
+ return { success: true, data };
610
+ }
611
+ default:
612
+ return { success: false, error: `Unknown action: ${action}. Use: create, list, get, add_items, approve, receive, cancel` };
613
+ }
614
+ }
615
+ catch (err) {
616
+ return { success: false, error: `Purchase orders error: ${err}` };
617
+ }
618
+ },
619
+ // ===========================================================================
620
+ // 3c. TRANSFERS - Inventory transfers between locations
621
+ // Actions: create, list, get, receive, cancel
622
+ // ===========================================================================
623
+ transfers: async (supabase, args, storeId) => {
624
+ const action = args.action;
625
+ if (!action) {
626
+ return { success: false, error: "action required: create, list, get, receive, cancel" };
627
+ }
628
+ try {
629
+ switch (action) {
630
+ case "create": {
631
+ const fromLocationId = args.from_location_id;
632
+ const toLocationId = args.to_location_id;
633
+ const items = args.items;
634
+ if (!fromLocationId || !toLocationId || !items?.length) {
635
+ return { success: false, error: "from_location_id, to_location_id, and items required" };
636
+ }
637
+ if (fromLocationId === toLocationId) {
638
+ return { success: false, error: "Source and destination must be different" };
639
+ }
640
+ // Generate transfer number
641
+ const transferNumber = "TR-" + Date.now().toString().slice(-10);
642
+ // Create transfer header
643
+ const { data: transfer, error: trErr } = await supabase
644
+ .from("inventory_transfers")
645
+ .insert({
646
+ store_id: storeId,
647
+ transfer_number: transferNumber,
648
+ source_location_id: fromLocationId,
649
+ destination_location_id: toLocationId,
650
+ status: "in_transit", // Valid status
651
+ notes: args.notes || null,
652
+ is_ai_action: true
653
+ })
654
+ .select()
655
+ .single();
656
+ if (trErr)
657
+ return { success: false, error: trErr.message };
658
+ // Process each item - deduct from source, add to transfer items
659
+ let itemsCount = 0;
660
+ for (const item of items) {
661
+ // Check source inventory
662
+ const { data: srcInv } = await supabase
663
+ .from("inventory")
664
+ .select("id, quantity")
665
+ .eq("product_id", item.product_id)
666
+ .eq("location_id", fromLocationId)
667
+ .single();
668
+ if (!srcInv) {
669
+ // Rollback
670
+ await supabase.from("inventory_transfers").delete().eq("id", transfer.id);
671
+ return { success: false, error: `Product ${item.product_id} not found at source location` };
672
+ }
673
+ if (srcInv.quantity < item.quantity) {
674
+ await supabase.from("inventory_transfers").delete().eq("id", transfer.id);
675
+ return { success: false, error: `Insufficient quantity: have ${srcInv.quantity}, need ${item.quantity}` };
676
+ }
677
+ // Add transfer item
678
+ await supabase.from("inventory_transfer_items").insert({
679
+ transfer_id: transfer.id,
680
+ product_id: item.product_id,
681
+ quantity: item.quantity
682
+ });
683
+ // Deduct from source inventory
684
+ await supabase
685
+ .from("inventory")
686
+ .update({
687
+ quantity: srcInv.quantity - item.quantity,
688
+ updated_at: new Date().toISOString()
689
+ })
690
+ .eq("id", srcInv.id);
691
+ // Log adjustment (ignore errors)
692
+ try {
693
+ await supabase.from("inventory_adjustments").insert({
694
+ inventory_id: srcInv.id,
695
+ product_id: item.product_id,
696
+ location_id: fromLocationId,
697
+ adjustment_type: "TRANSFER_OUT",
698
+ old_quantity: srcInv.quantity,
699
+ new_quantity: srcInv.quantity - item.quantity,
700
+ reason: `Transfer to ${toLocationId} (${transferNumber})`
701
+ });
702
+ }
703
+ catch { /* ignore */ }
704
+ itemsCount++;
705
+ }
706
+ // Update status to in_transit
707
+ await supabase
708
+ .from("inventory_transfers")
709
+ .update({ status: "in_transit", shipped_at: new Date().toISOString() })
710
+ .eq("id", transfer.id);
711
+ return {
712
+ success: true,
713
+ data: { transfer_id: transfer.id, transfer_number: transferNumber, items_count: itemsCount }
714
+ };
715
+ }
716
+ case "list": {
717
+ let q = supabase
718
+ .from("inventory_transfers")
719
+ .select("id, transfer_number, status, source_location_id, destination_location_id, created_at, shipped_at, received_at, from_location:locations!inventory_transfers_source_location_id_fkey(name), to_location:locations!inventory_transfers_destination_location_id_fkey(name)")
720
+ .order("created_at", { ascending: false });
721
+ if (storeId)
722
+ q = q.eq("store_id", storeId);
723
+ if (args.status)
724
+ q = q.eq("status", args.status);
725
+ if (args.from_location_id)
726
+ q = q.eq("source_location_id", args.from_location_id);
727
+ if (args.to_location_id)
728
+ q = q.eq("destination_location_id", args.to_location_id);
729
+ const { data, error } = await q.limit(args.limit || 50);
730
+ if (error)
731
+ return { success: false, error: error.message };
732
+ return { success: true, data };
733
+ }
734
+ case "get": {
735
+ const transferId = args.transfer_id || args.id;
736
+ if (!transferId)
737
+ return { success: false, error: "transfer_id required" };
738
+ const { data, error } = await supabase
739
+ .from("inventory_transfers")
740
+ .select("*, items:inventory_transfer_items(*, product:products(name, sku)), from_location:locations!inventory_transfers_source_location_id_fkey(name), to_location:locations!inventory_transfers_destination_location_id_fkey(name)")
741
+ .eq("id", transferId)
742
+ .single();
743
+ if (error)
744
+ return { success: false, error: error.message };
745
+ return { success: true, data };
746
+ }
747
+ case "receive": {
748
+ const transferId = args.transfer_id;
749
+ if (!transferId)
750
+ return { success: false, error: "transfer_id required" };
751
+ // Get transfer details
752
+ const { data: transfer, error: trErr } = await supabase
753
+ .from("inventory_transfers")
754
+ .select("*, items:inventory_transfer_items(*)")
755
+ .eq("id", transferId)
756
+ .single();
757
+ if (trErr || !transfer)
758
+ return { success: false, error: trErr?.message || "Transfer not found" };
759
+ if (transfer.status === "received")
760
+ return { success: false, error: "Transfer already received" };
761
+ if (transfer.status === "cancelled")
762
+ return { success: false, error: "Cannot receive cancelled transfer" };
763
+ const receiveItems = args.items;
764
+ let itemsReceived = 0;
765
+ let totalQtyReceived = 0;
766
+ // Process each transfer item
767
+ for (const trItem of transfer.items || []) {
768
+ let qtyToReceive = trItem.quantity - (trItem.received_quantity || 0);
769
+ // If specific items provided, match them
770
+ if (receiveItems) {
771
+ const match = receiveItems.find(ri => ri.item_id === trItem.id || ri.product_id === trItem.product_id);
772
+ if (!match)
773
+ continue;
774
+ qtyToReceive = Math.min(match.quantity, qtyToReceive);
775
+ }
776
+ if (qtyToReceive <= 0)
777
+ continue;
778
+ // Find or create destination inventory
779
+ let destInvId = null;
780
+ const { data: existingInv } = await supabase
781
+ .from("inventory")
782
+ .select("id, quantity")
783
+ .eq("product_id", trItem.product_id)
784
+ .eq("location_id", transfer.destination_location_id)
785
+ .single();
786
+ if (existingInv) {
787
+ destInvId = existingInv.id;
788
+ await supabase
789
+ .from("inventory")
790
+ .update({
791
+ quantity: existingInv.quantity + qtyToReceive,
792
+ updated_at: new Date().toISOString()
793
+ })
794
+ .eq("id", destInvId);
795
+ }
796
+ else {
797
+ const { data: newInv } = await supabase
798
+ .from("inventory")
799
+ .insert({
800
+ product_id: trItem.product_id,
801
+ location_id: transfer.destination_location_id,
802
+ store_id: transfer.store_id,
803
+ quantity: qtyToReceive
804
+ })
805
+ .select()
806
+ .single();
807
+ destInvId = newInv?.id;
808
+ }
809
+ // Update transfer item
810
+ const newReceivedQty = (trItem.received_quantity || 0) + qtyToReceive;
811
+ await supabase
812
+ .from("inventory_transfer_items")
813
+ .update({
814
+ received_quantity: newReceivedQty,
815
+ updated_at: new Date().toISOString()
816
+ })
817
+ .eq("id", trItem.id);
818
+ // Log adjustment (ignore errors)
819
+ if (destInvId) {
820
+ try {
821
+ await supabase.from("inventory_adjustments").insert({
822
+ inventory_id: destInvId,
823
+ product_id: trItem.product_id,
824
+ location_id: transfer.destination_location_id,
825
+ adjustment_type: "TRANSFER_IN",
826
+ old_quantity: existingInv?.quantity || 0,
827
+ new_quantity: (existingInv?.quantity || 0) + qtyToReceive,
828
+ reason: `Received from transfer ${transfer.transfer_number}`
829
+ });
830
+ }
831
+ catch { /* ignore */ }
832
+ }
833
+ itemsReceived++;
834
+ totalQtyReceived += qtyToReceive;
835
+ }
836
+ // Update transfer status
837
+ const { data: updatedItems } = await supabase
838
+ .from("inventory_transfer_items")
839
+ .select("quantity, received_quantity")
840
+ .eq("transfer_id", transferId);
841
+ const allReceived = (updatedItems || []).every(i => (i.received_quantity || 0) >= i.quantity);
842
+ await supabase
843
+ .from("inventory_transfers")
844
+ .update({
845
+ status: allReceived ? "received" : "in_transit",
846
+ received_at: allReceived ? new Date().toISOString() : null,
847
+ updated_at: new Date().toISOString()
848
+ })
849
+ .eq("id", transferId);
850
+ return {
851
+ success: true,
852
+ data: {
853
+ items_received: itemsReceived,
854
+ total_quantity_received: totalQtyReceived,
855
+ transfer_status: allReceived ? "received" : "partial"
856
+ }
857
+ };
858
+ }
859
+ case "cancel": {
860
+ const transferId = args.transfer_id;
861
+ if (!transferId)
862
+ return { success: false, error: "transfer_id required" };
863
+ // Get transfer items to restore inventory
864
+ const { data: transfer } = await supabase
865
+ .from("inventory_transfers")
866
+ .select("*, items:inventory_transfer_items(*)")
867
+ .eq("id", transferId)
868
+ .single();
869
+ if (!transfer)
870
+ return { success: false, error: "Transfer not found" };
871
+ if (transfer.status === "received")
872
+ return { success: false, error: "Cannot cancel a received transfer" };
873
+ if (transfer.status === "cancelled")
874
+ return { success: false, error: "Transfer already cancelled" };
875
+ // Restore inventory at source location
876
+ for (const item of transfer.items || []) {
877
+ // Get source inventory by product and source location
878
+ const { data: srcInv } = await supabase
879
+ .from("inventory")
880
+ .select("id, quantity")
881
+ .eq("product_id", item.product_id)
882
+ .eq("location_id", transfer.source_location_id)
883
+ .single();
884
+ if (srcInv) {
885
+ // Restore quantity
886
+ const restoreQty = item.quantity - (item.received_quantity || 0);
887
+ await supabase
888
+ .from("inventory")
889
+ .update({
890
+ quantity: srcInv.quantity + restoreQty,
891
+ updated_at: new Date().toISOString()
892
+ })
893
+ .eq("id", srcInv.id);
894
+ // Log adjustment (ignore errors)
895
+ try {
896
+ await supabase.from("inventory_adjustments").insert({
897
+ inventory_id: srcInv.id,
898
+ product_id: item.product_id,
899
+ location_id: transfer.source_location_id,
900
+ adjustment_type: "TRANSFER_CANCEL",
901
+ old_quantity: srcInv.quantity,
902
+ new_quantity: srcInv.quantity + restoreQty,
903
+ reason: `Cancelled transfer ${transfer.transfer_number}`
904
+ });
905
+ }
906
+ catch { /* ignore */ }
907
+ }
908
+ }
909
+ // Update transfer status
910
+ const { data, error } = await supabase
911
+ .from("inventory_transfers")
912
+ .update({
913
+ status: "cancelled",
914
+ cancelled_at: new Date().toISOString(),
915
+ updated_at: new Date().toISOString()
916
+ })
917
+ .eq("id", transferId)
918
+ .select()
919
+ .single();
920
+ if (error)
921
+ return { success: false, error: error.message };
922
+ return { success: true, data: { ...data, inventory_restored: true } };
923
+ }
924
+ default:
925
+ return { success: false, error: `Unknown action: ${action}. Use: create, list, get, receive, cancel` };
926
+ }
927
+ }
928
+ catch (err) {
929
+ return { success: false, error: `Transfers error: ${err}` };
930
+ }
931
+ },
932
+ // ===========================================================================
933
+ // 4. COLLECTIONS - Manage collections
934
+ // Actions: find, create, get_theme, set_theme, set_icon
935
+ // ===========================================================================
936
+ collections: async (supabase, args, storeId) => {
937
+ const action = args.action || "find";
938
+ // Helper to handle missing table gracefully
939
+ const handleTableError = (error) => {
940
+ if (error.message.includes("does not exist") || error.code === "42P01" || error.message.includes("schema cache")) {
941
+ return { success: true, data: [], message: "Collections table not configured" };
942
+ }
943
+ return { success: false, error: error.message };
944
+ };
945
+ try {
946
+ switch (action) {
947
+ case "find": {
948
+ let q = supabase.from("collections").select("*");
949
+ if (storeId)
950
+ q = q.eq("store_id", storeId);
951
+ if (args.name)
952
+ q = q.ilike("name", `%${args.name}%`);
953
+ const { data, error } = await q;
954
+ if (error)
955
+ return handleTableError(error);
956
+ return { success: true, data };
957
+ }
958
+ case "create": {
959
+ const { data, error } = await supabase
960
+ .from("collections")
961
+ .insert({ name: args.name, description: args.description, store_id: storeId })
962
+ .select()
963
+ .single();
964
+ if (error)
965
+ return handleTableError(error);
966
+ return { success: true, data };
967
+ }
968
+ case "get_theme": {
969
+ const { data, error } = await supabase
970
+ .from("collections")
971
+ .select("id, name, theme")
972
+ .eq("id", args.collection_id)
973
+ .single();
974
+ if (error)
975
+ return handleTableError(error);
976
+ return { success: true, data };
977
+ }
978
+ case "set_theme": {
979
+ const { data, error } = await supabase
980
+ .from("collections")
981
+ .update({ theme: args.theme })
982
+ .eq("id", args.collection_id)
983
+ .select()
984
+ .single();
985
+ if (error)
986
+ return handleTableError(error);
987
+ return { success: true, data };
988
+ }
989
+ case "set_icon": {
990
+ const { data, error } = await supabase
991
+ .from("collections")
992
+ .update({ icon: args.icon })
993
+ .eq("id", args.collection_id)
994
+ .select()
995
+ .single();
996
+ if (error)
997
+ return handleTableError(error);
998
+ return { success: true, data };
999
+ }
1000
+ default:
1001
+ return { success: false, error: `Unknown action: ${action}. Use: find, create, get_theme, set_theme, set_icon` };
1002
+ }
1003
+ }
1004
+ catch (err) {
1005
+ return { success: false, error: `Collections error: ${err}` };
1006
+ }
1007
+ },
1008
+ // ===========================================================================
1009
+ // 5. CUSTOMERS - Manage customers
1010
+ // Actions: find, create, update
1011
+ // ===========================================================================
1012
+ customers: async (supabase, args, storeId) => {
1013
+ const action = args.action || "find";
1014
+ try {
1015
+ switch (action) {
1016
+ case "find": {
1017
+ const query = args.query || "";
1018
+ const email = args.email;
1019
+ const phone = args.phone;
1020
+ const limit = args.limit || 20;
1021
+ let q = supabase
1022
+ .from("customers")
1023
+ .select("id, email, phone, first_name, last_name, created_at")
1024
+ .limit(limit);
1025
+ if (query) {
1026
+ q = q.or(`email.ilike.%${query}%,phone.ilike.%${query}%,first_name.ilike.%${query}%,last_name.ilike.%${query}%`);
1027
+ }
1028
+ if (email)
1029
+ q = q.eq("email", email);
1030
+ if (phone)
1031
+ q = q.eq("phone", phone);
1032
+ if (storeId)
1033
+ q = q.eq("store_id", storeId);
1034
+ const { data, error } = await q;
1035
+ if (error)
1036
+ return { success: false, error: error.message };
1037
+ return { success: true, data };
1038
+ }
1039
+ case "create": {
1040
+ const { data, error } = await supabase
1041
+ .from("customers")
1042
+ .insert({
1043
+ email: args.email,
1044
+ phone: args.phone,
1045
+ first_name: args.first_name,
1046
+ last_name: args.last_name,
1047
+ store_id: storeId
1048
+ })
1049
+ .select()
1050
+ .single();
1051
+ if (error)
1052
+ return { success: false, error: error.message };
1053
+ return { success: true, data };
1054
+ }
1055
+ case "update": {
1056
+ const customerId = args.customer_id;
1057
+ if (!customerId)
1058
+ return { success: false, error: "customer_id required" };
1059
+ const updateData = {};
1060
+ if (args.email)
1061
+ updateData.email = args.email;
1062
+ if (args.phone)
1063
+ updateData.phone = args.phone;
1064
+ if (args.first_name)
1065
+ updateData.first_name = args.first_name;
1066
+ if (args.last_name)
1067
+ updateData.last_name = args.last_name;
1068
+ const { data, error } = await supabase
1069
+ .from("customers")
1070
+ .update(updateData)
1071
+ .eq("id", customerId)
1072
+ .select()
1073
+ .single();
1074
+ if (error)
1075
+ return { success: false, error: error.message };
1076
+ return { success: true, data };
1077
+ }
1078
+ default:
1079
+ return { success: false, error: `Unknown action: ${action}. Use: find, create, update` };
1080
+ }
1081
+ }
1082
+ catch (err) {
1083
+ return { success: false, error: `Customers error: ${err}` };
1084
+ }
1085
+ },
1086
+ // ===========================================================================
1087
+ // 6. PRODUCTS - Manage products
1088
+ // Actions: find, create, update, pricing_templates
1089
+ // ===========================================================================
1090
+ products: async (supabase, args, storeId) => {
1091
+ const action = args.action || "find";
1092
+ try {
1093
+ switch (action) {
1094
+ case "find": {
1095
+ const query = args.query || "";
1096
+ const limit = args.limit || 200;
1097
+ const statusFilter = args.status || "published";
1098
+ // Split query into individual words for smarter matching
1099
+ const words = query.trim().split(/\s+/).filter(Boolean);
1100
+ // Separate words that might be category names vs product names
1101
+ // Search categories first to find matching category IDs
1102
+ let categoryIds = [];
1103
+ if (words.length > 0) {
1104
+ let catQ = supabase.from("categories").select("id, name");
1105
+ if (storeId)
1106
+ catQ = catQ.eq("store_id", storeId);
1107
+ // Match any word against category names
1108
+ const catFilters = words.map(w => `name.ilike.%${w}%`).join(",");
1109
+ catQ = catQ.or(catFilters);
1110
+ const { data: matchingCats } = await catQ;
1111
+ categoryIds = matchingCats?.map((c) => c.id) || [];
1112
+ }
1113
+ // Build product query with category join
1114
+ let q = supabase
1115
+ .from("products")
1116
+ .select("id, name, sku, status, primary_category_id, category:categories!primary_category_id(name)")
1117
+ .limit(limit);
1118
+ if (storeId)
1119
+ q = q.eq("store_id", storeId);
1120
+ if (statusFilter !== "all")
1121
+ q = q.eq("status", statusFilter);
1122
+ if (words.length > 0) {
1123
+ // Build OR filter: match name words AND/OR category membership
1124
+ const nameFilters = words.map(w => `name.ilike.%${w}%`);
1125
+ const skuFilter = `sku.ilike.%${query}%`;
1126
+ const allFilters = [...nameFilters, skuFilter];
1127
+ if (categoryIds.length > 0) {
1128
+ allFilters.push(`primary_category_id.in.(${categoryIds.join(",")})`);
1129
+ }
1130
+ q = q.or(allFilters.join(","));
1131
+ }
1132
+ const { data, error } = await q;
1133
+ if (error)
1134
+ return { success: false, error: error.message };
1135
+ const scored = (data || []).map((p) => {
1136
+ let score = 0;
1137
+ const pName = (p.name || "").toLowerCase();
1138
+ const catName = (p.category?.name || "").toLowerCase();
1139
+ for (const w of words) {
1140
+ const wl = w.toLowerCase();
1141
+ if (pName.includes(wl))
1142
+ score += 2;
1143
+ if (catName.includes(wl))
1144
+ score += 1;
1145
+ }
1146
+ return { product: p, score };
1147
+ });
1148
+ scored.sort((a, b) => b.score - a.score);
1149
+ // Enrich with inventory totals
1150
+ const productIds = scored.map(s => s.product.id);
1151
+ let inventoryMap = {};
1152
+ if (productIds.length > 0) {
1153
+ const { data: invData } = await supabase
1154
+ .from("inventory")
1155
+ .select("product_id, quantity")
1156
+ .in("product_id", productIds)
1157
+ .gt("quantity", 0);
1158
+ if (invData) {
1159
+ for (const inv of invData) {
1160
+ inventoryMap[inv.product_id] = (inventoryMap[inv.product_id] || 0) + inv.quantity;
1161
+ }
1162
+ }
1163
+ }
1164
+ const enriched = scored.map(s => ({
1165
+ id: s.product.id,
1166
+ name: s.product.name,
1167
+ sku: s.product.sku,
1168
+ status: s.product.status,
1169
+ category: s.product.category?.name || null,
1170
+ total_stock: inventoryMap[s.product.id] || 0
1171
+ }));
1172
+ return {
1173
+ success: true,
1174
+ data: enriched,
1175
+ total_count: enriched.length,
1176
+ limit,
1177
+ truncated: enriched.length >= limit
1178
+ };
1179
+ }
1180
+ case "create": {
1181
+ const { data, error } = await supabase
1182
+ .from("products")
1183
+ .insert({
1184
+ name: args.name,
1185
+ sku: args.sku,
1186
+ store_id: storeId
1187
+ })
1188
+ .select()
1189
+ .single();
1190
+ if (error)
1191
+ return { success: false, error: error.message };
1192
+ return { success: true, data };
1193
+ }
1194
+ case "update": {
1195
+ const productId = args.product_id;
1196
+ if (!productId)
1197
+ return { success: false, error: "product_id required" };
1198
+ const updateData = {};
1199
+ if (args.name)
1200
+ updateData.name = args.name;
1201
+ if (args.sku)
1202
+ updateData.sku = args.sku;
1203
+ if (args.status)
1204
+ updateData.status = args.status;
1205
+ const { data, error } = await supabase
1206
+ .from("products")
1207
+ .update(updateData)
1208
+ .eq("id", productId)
1209
+ .select()
1210
+ .single();
1211
+ if (error)
1212
+ return { success: false, error: error.message };
1213
+ return { success: true, data };
1214
+ }
1215
+ case "pricing_templates": {
1216
+ // pricing_templates table may not exist - return empty array gracefully
1217
+ try {
1218
+ let q = supabase.from("pricing_templates").select("*");
1219
+ if (storeId)
1220
+ q = q.eq("store_id", storeId);
1221
+ const { data, error } = await q;
1222
+ if (error) {
1223
+ // Table doesn't exist - return empty array
1224
+ if (error.message.includes("does not exist") || error.code === "42P01" || error.message.includes("schema cache")) {
1225
+ return { success: true, data: [], message: "Pricing templates not configured" };
1226
+ }
1227
+ return { success: false, error: error.message };
1228
+ }
1229
+ return { success: true, data };
1230
+ }
1231
+ catch {
1232
+ return { success: true, data: [], message: "Pricing templates not configured" };
1233
+ }
1234
+ }
1235
+ default:
1236
+ return { success: false, error: `Unknown action: ${action}. Use: find, create, update, pricing_templates` };
1237
+ }
1238
+ }
1239
+ catch (err) {
1240
+ return { success: false, error: `Products error: ${err}` };
1241
+ }
1242
+ },
1243
+ // ===========================================================================
1244
+ // 7. ANALYTICS - Unified analytics & data discovery
1245
+ // Actions: summary, by_location, detailed, discover, employee, custom
1246
+ // Supports: preset periods OR custom date ranges (start_date, end_date)
1247
+ // Also supports: days_back for "last N days" queries
1248
+ // ===========================================================================
1249
+ analytics: async (supabase, args, storeId) => {
1250
+ const action = args.action || "summary";
1251
+ const period = args.period;
1252
+ const locationId = args.location_id;
1253
+ // Custom date range support
1254
+ const customStartDate = args.start_date;
1255
+ const customEndDate = args.end_date;
1256
+ const daysBack = args.days_back;
1257
+ try {
1258
+ // Calculate date range - prioritize custom dates, then days_back, then period
1259
+ const today = new Date();
1260
+ let startDate;
1261
+ let endDate = today.toISOString().split("T")[0];
1262
+ if (customStartDate) {
1263
+ // Custom date range provided
1264
+ startDate = customStartDate;
1265
+ if (customEndDate) {
1266
+ endDate = customEndDate;
1267
+ }
1268
+ }
1269
+ else if (daysBack && daysBack > 0) {
1270
+ // Dynamic days back (e.g., last 365 days, last 180 days)
1271
+ const start = new Date(today);
1272
+ start.setDate(start.getDate() - daysBack);
1273
+ startDate = start.toISOString().split("T")[0];
1274
+ }
1275
+ else {
1276
+ // Use preset period
1277
+ const periodToUse = period || "last_30";
1278
+ switch (periodToUse) {
1279
+ case "today":
1280
+ startDate = today.toISOString().split("T")[0];
1281
+ break;
1282
+ case "yesterday":
1283
+ const yesterday = new Date(today);
1284
+ yesterday.setDate(yesterday.getDate() - 1);
1285
+ startDate = yesterday.toISOString().split("T")[0];
1286
+ endDate = startDate;
1287
+ break;
1288
+ case "last_7":
1289
+ const week = new Date(today);
1290
+ week.setDate(week.getDate() - 7);
1291
+ startDate = week.toISOString().split("T")[0];
1292
+ break;
1293
+ case "last_30":
1294
+ const month = new Date(today);
1295
+ month.setDate(month.getDate() - 30);
1296
+ startDate = month.toISOString().split("T")[0];
1297
+ break;
1298
+ case "last_90":
1299
+ const quarter = new Date(today);
1300
+ quarter.setDate(quarter.getDate() - 90);
1301
+ startDate = quarter.toISOString().split("T")[0];
1302
+ break;
1303
+ case "last_180":
1304
+ const half = new Date(today);
1305
+ half.setDate(half.getDate() - 180);
1306
+ startDate = half.toISOString().split("T")[0];
1307
+ break;
1308
+ case "last_365":
1309
+ const year = new Date(today);
1310
+ year.setDate(year.getDate() - 365);
1311
+ startDate = year.toISOString().split("T")[0];
1312
+ break;
1313
+ case "ytd":
1314
+ startDate = `${today.getFullYear()}-01-01`;
1315
+ break;
1316
+ case "mtd":
1317
+ startDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-01`;
1318
+ break;
1319
+ case "last_year":
1320
+ startDate = `${today.getFullYear() - 1}-01-01`;
1321
+ endDate = `${today.getFullYear() - 1}-12-31`;
1322
+ break;
1323
+ case "all_time":
1324
+ startDate = "2020-01-01"; // Reasonable start date
1325
+ break;
1326
+ default:
1327
+ // Default to last 30 days
1328
+ const defaultMonth = new Date(today);
1329
+ defaultMonth.setDate(defaultMonth.getDate() - 30);
1330
+ startDate = defaultMonth.toISOString().split("T")[0];
1331
+ }
1332
+ }
1333
+ switch (action) {
1334
+ case "summary": {
1335
+ // Use RPC function for efficient server-side aggregation (no row limits)
1336
+ const { data: rpcResult, error: rpcError } = await supabase.rpc("get_sales_analytics", {
1337
+ p_store_id: storeId || null,
1338
+ p_start_date: startDate,
1339
+ p_end_date: endDate,
1340
+ p_location_id: locationId || null
1341
+ });
1342
+ if (rpcError) {
1343
+ // Fallback to client-side aggregation if RPC not deployed
1344
+ console.warn("[Analytics] RPC failed, falling back to client-side:", rpcError.message);
1345
+ }
1346
+ else if (rpcResult) {
1347
+ // RPC returned data - use it directly
1348
+ return {
1349
+ success: true,
1350
+ data: {
1351
+ period: period || (daysBack ? `last_${daysBack}` : "custom"),
1352
+ dateRange: rpcResult.dateRange,
1353
+ summary: {
1354
+ grossSales: rpcResult.grossSales,
1355
+ netSales: rpcResult.netSales,
1356
+ totalRevenue: rpcResult.totalRevenue,
1357
+ totalCogs: rpcResult.totalCogs,
1358
+ totalProfit: rpcResult.totalProfit,
1359
+ taxAmount: rpcResult.taxAmount,
1360
+ discountAmount: rpcResult.discountAmount,
1361
+ shippingAmount: rpcResult.shippingAmount,
1362
+ totalOrders: rpcResult.totalOrders,
1363
+ completedOrders: rpcResult.completedOrders,
1364
+ cancelledOrders: rpcResult.cancelledOrders,
1365
+ uniqueCustomers: rpcResult.uniqueCustomers,
1366
+ totalQuantity: rpcResult.totalQuantity,
1367
+ avgOrderValue: rpcResult.avgOrderValue,
1368
+ profitMargin: `${rpcResult.profitMargin}%`,
1369
+ avgDailyRevenue: rpcResult.avgDailyRevenue,
1370
+ avgDailyOrders: rpcResult.avgDailyOrders
1371
+ },
1372
+ rowCount: rpcResult.rowCount
1373
+ }
1374
+ };
1375
+ }
1376
+ // Fallback: client-side aggregation
1377
+ let q = supabase
1378
+ .from("v_daily_sales")
1379
+ .select("*")
1380
+ .gte("sale_date", startDate)
1381
+ .lte("sale_date", endDate)
1382
+ .order("sale_date", { ascending: false });
1383
+ if (storeId)
1384
+ q = q.eq("store_id", storeId);
1385
+ if (locationId)
1386
+ q = q.eq("location_id", locationId);
1387
+ const { data, error } = await q;
1388
+ if (error)
1389
+ return { success: false, error: error.message };
1390
+ const totals = (data || []).reduce((acc, day) => ({
1391
+ grossSales: acc.grossSales + parseFloat(day.gross_sales || 0),
1392
+ netSales: acc.netSales + parseFloat(day.net_sales || 0),
1393
+ taxAmount: acc.taxAmount + parseFloat(day.total_tax || 0),
1394
+ discountAmount: acc.discountAmount + parseFloat(day.total_discounts || 0),
1395
+ shippingAmount: acc.shippingAmount + parseFloat(day.total_shipping || 0),
1396
+ totalOrders: acc.totalOrders + parseInt(day.order_count || 0),
1397
+ completedOrders: acc.completedOrders + parseInt(day.completed_orders || 0),
1398
+ cancelledOrders: acc.cancelledOrders + parseInt(day.cancelled_orders || 0),
1399
+ uniqueCustomers: acc.uniqueCustomers + parseInt(day.unique_customers || 0),
1400
+ totalCogs: acc.totalCogs + parseFloat(day.total_cogs || 0),
1401
+ totalProfit: acc.totalProfit + parseFloat(day.total_profit || 0),
1402
+ totalRevenue: acc.totalRevenue + parseFloat(day.total_revenue || 0)
1403
+ }), {
1404
+ grossSales: 0, netSales: 0, taxAmount: 0, discountAmount: 0, shippingAmount: 0,
1405
+ totalOrders: 0, completedOrders: 0, cancelledOrders: 0, uniqueCustomers: 0,
1406
+ totalCogs: 0, totalProfit: 0, totalRevenue: 0
1407
+ });
1408
+ const profitMargin = totals.netSales > 0
1409
+ ? Math.round((totals.totalProfit / totals.netSales) * 10000) / 100
1410
+ : 0;
1411
+ const startD = new Date(startDate);
1412
+ const endD = new Date(endDate);
1413
+ const daysDiff = Math.ceil((endD.getTime() - startD.getTime()) / (1000 * 60 * 60 * 24)) + 1;
1414
+ return {
1415
+ success: true,
1416
+ data: {
1417
+ period: period || (daysBack ? `last_${daysBack}` : "custom"),
1418
+ dateRange: { from: startDate, to: endDate, days: daysDiff },
1419
+ summary: {
1420
+ ...totals,
1421
+ avgOrderValue: totals.totalOrders > 0 ? Math.round((totals.netSales / totals.totalOrders) * 100) / 100 : 0,
1422
+ profitMargin: `${profitMargin}%`,
1423
+ avgDailyRevenue: daysDiff > 0 ? Math.round((totals.netSales / daysDiff) * 100) / 100 : 0,
1424
+ avgDailyOrders: daysDiff > 0 ? Math.round((totals.totalOrders / daysDiff) * 10) / 10 : 0
1425
+ },
1426
+ rowCount: data?.length || 0
1427
+ }
1428
+ };
1429
+ }
1430
+ case "by_location":
1431
+ case "detailed": {
1432
+ let q = supabase
1433
+ .from("v_daily_sales")
1434
+ .select("*")
1435
+ .gte("sale_date", startDate)
1436
+ .lte("sale_date", endDate)
1437
+ .order("sale_date", { ascending: false });
1438
+ if (storeId)
1439
+ q = q.eq("store_id", storeId);
1440
+ if (locationId)
1441
+ q = q.eq("location_id", locationId);
1442
+ const { data, error } = await q;
1443
+ if (error)
1444
+ return { success: false, error: error.message };
1445
+ if (action === "by_location") {
1446
+ const byLocation = {};
1447
+ for (const row of data || []) {
1448
+ const loc = row.location_id || "unknown";
1449
+ if (!byLocation[loc])
1450
+ byLocation[loc] = { orders: 0, gross: 0, net: 0 };
1451
+ byLocation[loc].orders += parseInt(row.order_count || 0);
1452
+ byLocation[loc].gross += parseFloat(row.gross_sales || 0);
1453
+ byLocation[loc].net += parseFloat(row.net_sales || 0);
1454
+ }
1455
+ return { success: true, data: { period, byLocation: Object.entries(byLocation).map(([id, s]) => ({ locationId: id, ...s })) } };
1456
+ }
1457
+ // Detailed action - return daily data with totals
1458
+ const totals = (data || []).reduce((acc, day) => ({
1459
+ grossSales: acc.grossSales + parseFloat(day.gross_sales || 0),
1460
+ netSales: acc.netSales + parseFloat(day.net_sales || 0),
1461
+ taxAmount: acc.taxAmount + parseFloat(day.total_tax || 0),
1462
+ discountAmount: acc.discountAmount + parseFloat(day.total_discounts || 0),
1463
+ shippingAmount: acc.shippingAmount + parseFloat(day.total_shipping || 0),
1464
+ totalOrders: acc.totalOrders + parseInt(day.order_count || 0),
1465
+ completedOrders: acc.completedOrders + parseInt(day.completed_orders || 0),
1466
+ cancelledOrders: acc.cancelledOrders + parseInt(day.cancelled_orders || 0),
1467
+ uniqueCustomers: acc.uniqueCustomers + parseInt(day.unique_customers || 0),
1468
+ totalCogs: acc.totalCogs + parseFloat(day.total_cogs || 0),
1469
+ totalProfit: acc.totalProfit + parseFloat(day.total_profit || 0),
1470
+ totalRevenue: acc.totalRevenue + parseFloat(day.total_revenue || 0)
1471
+ }), {
1472
+ grossSales: 0, netSales: 0, taxAmount: 0, discountAmount: 0, shippingAmount: 0,
1473
+ totalOrders: 0, completedOrders: 0, cancelledOrders: 0, uniqueCustomers: 0,
1474
+ totalCogs: 0, totalProfit: 0, totalRevenue: 0
1475
+ });
1476
+ const profitMargin = totals.netSales > 0
1477
+ ? Math.round((totals.totalProfit / totals.netSales) * 10000) / 100
1478
+ : 0;
1479
+ const startD = new Date(startDate);
1480
+ const endD = new Date(endDate);
1481
+ const daysDiff = Math.ceil((endD.getTime() - startD.getTime()) / (1000 * 60 * 60 * 24)) + 1;
1482
+ return {
1483
+ success: true,
1484
+ data: {
1485
+ period: period || (daysBack ? `last_${daysBack}` : "custom"),
1486
+ dateRange: { from: startDate, to: endDate, days: daysDiff },
1487
+ summary: {
1488
+ ...totals,
1489
+ avgOrderValue: totals.totalOrders > 0 ? Math.round((totals.netSales / totals.totalOrders) * 100) / 100 : 0,
1490
+ profitMargin: `${profitMargin}%`,
1491
+ avgDailyRevenue: daysDiff > 0 ? Math.round((totals.netSales / daysDiff) * 100) / 100 : 0,
1492
+ avgDailyOrders: daysDiff > 0 ? Math.round((totals.totalOrders / daysDiff) * 10) / 10 : 0
1493
+ },
1494
+ daily: (data || []).slice(0, 90),
1495
+ rowCount: data?.length || 0
1496
+ }
1497
+ };
1498
+ }
1499
+ case "discover": {
1500
+ const tables = ["products", "orders", "customers", "inventory", "locations"];
1501
+ const result = {};
1502
+ for (const table of tables) {
1503
+ const { count } = await supabase.from(table).select("*", { count: "exact", head: true });
1504
+ result[table] = count || 0;
1505
+ }
1506
+ return { success: true, data: result };
1507
+ }
1508
+ case "employee": {
1509
+ const { data, error } = await supabase
1510
+ .from("orders")
1511
+ .select("employee_id, total_amount, created_at")
1512
+ .not("employee_id", "is", null);
1513
+ if (error)
1514
+ return { success: false, error: error.message };
1515
+ const byEmployee = {};
1516
+ for (const order of data || []) {
1517
+ const eid = order.employee_id;
1518
+ if (!byEmployee[eid])
1519
+ byEmployee[eid] = { count: 0, total: 0 };
1520
+ byEmployee[eid].count++;
1521
+ byEmployee[eid].total += order.total_amount || 0;
1522
+ }
1523
+ return { success: true, data: byEmployee };
1524
+ }
1525
+ // ==== NEW INTELLIGENCE ACTIONS ====
1526
+ case "customers":
1527
+ case "customer_intelligence": {
1528
+ // Customer LTV, RFM segmentation, cohorts
1529
+ const { data, error } = await supabase.rpc("get_customer_intelligence", {
1530
+ p_store_id: storeId || null
1531
+ });
1532
+ if (error)
1533
+ return { success: false, error: error.message };
1534
+ return { success: true, data };
1535
+ }
1536
+ case "products":
1537
+ case "product_intelligence": {
1538
+ // Product velocity, ABC analysis, performance tiers
1539
+ const { data, error } = await supabase.rpc("get_product_intelligence", {
1540
+ p_store_id: storeId || null
1541
+ });
1542
+ if (error)
1543
+ return { success: false, error: error.message };
1544
+ return { success: true, data };
1545
+ }
1546
+ case "inventory_intelligence": {
1547
+ // Inventory turnover, dead stock, overstock risk
1548
+ const { data, error } = await supabase.rpc("get_inventory_intelligence", {
1549
+ p_store_id: storeId || null
1550
+ });
1551
+ if (error)
1552
+ return { success: false, error: error.message };
1553
+ return { success: true, data };
1554
+ }
1555
+ case "marketing":
1556
+ case "marketing_intelligence": {
1557
+ // UTM attribution, channel performance, conversion rates
1558
+ const { data, error } = await supabase.rpc("get_marketing_intelligence", {
1559
+ p_store_id: storeId || null,
1560
+ p_start_date: startDate,
1561
+ p_end_date: endDate
1562
+ });
1563
+ if (error)
1564
+ return { success: false, error: error.message };
1565
+ return { success: true, data };
1566
+ }
1567
+ case "fraud":
1568
+ case "fraud_detection": {
1569
+ // Risk scores, suspicious activity patterns
1570
+ let q = supabase
1571
+ .from("v_fraud_detection")
1572
+ .select("*")
1573
+ .order("risk_score", { ascending: false })
1574
+ .limit(100);
1575
+ if (storeId)
1576
+ q = q.eq("store_id", storeId);
1577
+ if (args.risk_level)
1578
+ q = q.eq("risk_level", args.risk_level);
1579
+ const { data, error } = await q;
1580
+ if (error)
1581
+ return { success: false, error: error.message };
1582
+ const summary = {
1583
+ totalOrders: data?.length || 0,
1584
+ highRisk: data?.filter(o => o.risk_level === "high").length || 0,
1585
+ mediumRisk: data?.filter(o => o.risk_level === "medium").length || 0,
1586
+ avgRiskScore: data?.length ? Math.round(data.reduce((s, o) => s + o.risk_score, 0) / data.length) : 0,
1587
+ orders: data
1588
+ };
1589
+ return { success: true, data: summary };
1590
+ }
1591
+ case "employee_performance": {
1592
+ // Detailed employee metrics from v_employee_performance
1593
+ let q = supabase
1594
+ .from("v_employee_performance")
1595
+ .select("*")
1596
+ .order("total_revenue", { ascending: false });
1597
+ if (storeId)
1598
+ q = q.eq("store_id", storeId);
1599
+ const { data, error } = await q;
1600
+ if (error)
1601
+ return { success: false, error: error.message };
1602
+ return { success: true, data };
1603
+ }
1604
+ case "behavior":
1605
+ case "behavioral_analytics": {
1606
+ // UX metrics, session quality, bounce rates
1607
+ let q = supabase
1608
+ .from("v_behavioral_analytics")
1609
+ .select("*")
1610
+ .gte("visit_date", startDate)
1611
+ .lte("visit_date", endDate);
1612
+ if (storeId)
1613
+ q = q.eq("store_id", storeId);
1614
+ const { data, error } = await q;
1615
+ if (error)
1616
+ return { success: false, error: error.message };
1617
+ // Aggregate
1618
+ const totals = (data || []).reduce((acc, row) => ({
1619
+ sessions: acc.sessions + (row.sessions || 0),
1620
+ pageViews: acc.pageViews + (row.page_views || 0),
1621
+ rageClicks: acc.rageClicks + (row.total_rage_clicks || 0),
1622
+ uxIssues: acc.uxIssues + (row.sessions_with_ux_issues || 0)
1623
+ }), { sessions: 0, pageViews: 0, rageClicks: 0, uxIssues: 0 });
1624
+ return {
1625
+ success: true,
1626
+ data: {
1627
+ summary: {
1628
+ ...totals,
1629
+ avgPagesPerSession: totals.sessions ? Math.round((totals.pageViews / totals.sessions) * 100) / 100 : 0,
1630
+ uxIssueRate: totals.sessions ? Math.round((totals.uxIssues / totals.sessions) * 10000) / 100 : 0
1631
+ },
1632
+ byDevice: data?.reduce((acc, row) => {
1633
+ const device = row.device_type || "unknown";
1634
+ if (!acc[device])
1635
+ acc[device] = { sessions: 0, pageViews: 0 };
1636
+ acc[device].sessions += row.sessions || 0;
1637
+ acc[device].pageViews += row.page_views || 0;
1638
+ return acc;
1639
+ }, {})
1640
+ }
1641
+ };
1642
+ }
1643
+ case "full":
1644
+ case "business_intelligence": {
1645
+ // Master intelligence - all metrics in one call
1646
+ const { data, error } = await supabase.rpc("get_business_intelligence", {
1647
+ p_store_id: storeId || null,
1648
+ p_start_date: startDate,
1649
+ p_end_date: endDate
1650
+ });
1651
+ if (error)
1652
+ return { success: false, error: error.message };
1653
+ return { success: true, data };
1654
+ }
1655
+ default:
1656
+ return { success: false, error: `Unknown action: ${action}. Use: summary, by_location, detailed, discover, employee, customers, products, inventory_intelligence, marketing, fraud, employee_performance, behavior, full` };
1657
+ }
1658
+ }
1659
+ catch (err) {
1660
+ return { success: false, error: `Analytics error: ${err}` };
1661
+ }
1662
+ },
1663
+ // ===========================================================================
1664
+ // 8. LOCATIONS - Find/list locations
1665
+ // ===========================================================================
1666
+ locations: async (supabase, args, storeId) => {
1667
+ try {
1668
+ let q = supabase.from("locations").select("id, name, address_line1, city, state, is_active");
1669
+ if (storeId)
1670
+ q = q.eq("store_id", storeId);
1671
+ if (args.name)
1672
+ q = q.ilike("name", `%${args.name}%`);
1673
+ if (args.is_active !== undefined)
1674
+ q = q.eq("is_active", args.is_active);
1675
+ const { data, error } = await q;
1676
+ if (error)
1677
+ return { success: false, error: error.message };
1678
+ return { success: true, data };
1679
+ }
1680
+ catch (err) {
1681
+ return { success: false, error: `Locations error: ${err}` };
1682
+ }
1683
+ },
1684
+ // ===========================================================================
1685
+ // 9. ORDERS - Manage orders
1686
+ // Actions: find, get, create
1687
+ // ===========================================================================
1688
+ orders: async (supabase, args, storeId) => {
1689
+ const action = args.action || "find";
1690
+ try {
1691
+ switch (action) {
1692
+ case "find": {
1693
+ let q = supabase
1694
+ .from("orders")
1695
+ .select("id, order_number, status, total_amount, customer_id, created_at")
1696
+ .order("created_at", { ascending: false })
1697
+ .limit(args.limit || 50);
1698
+ if (storeId)
1699
+ q = q.eq("store_id", storeId);
1700
+ if (args.status)
1701
+ q = q.eq("status", args.status);
1702
+ if (args.customer_id)
1703
+ q = q.eq("customer_id", args.customer_id);
1704
+ const { data, error } = await q;
1705
+ if (error)
1706
+ return { success: false, error: error.message };
1707
+ return { success: true, data };
1708
+ }
1709
+ case "get": {
1710
+ const orderId = args.order_id;
1711
+ if (!orderId)
1712
+ return { success: false, error: "order_id required" };
1713
+ const { data, error } = await supabase
1714
+ .from("orders")
1715
+ .select("*, order_items(*)")
1716
+ .eq("id", orderId)
1717
+ .single();
1718
+ if (error)
1719
+ return { success: false, error: error.message };
1720
+ return { success: true, data };
1721
+ }
1722
+ case "purchase_orders": {
1723
+ let q = supabase.from("purchase_orders").select("*");
1724
+ if (storeId)
1725
+ q = q.eq("store_id", storeId);
1726
+ const { data, error } = await q.limit(50);
1727
+ if (error)
1728
+ return { success: false, error: error.message };
1729
+ return { success: true, data };
1730
+ }
1731
+ default:
1732
+ return { success: false, error: `Unknown action: ${action}. Use: find, get, purchase_orders` };
1733
+ }
1734
+ }
1735
+ catch (err) {
1736
+ return { success: false, error: `Orders error: ${err}` };
1737
+ }
1738
+ },
1739
+ // ===========================================================================
1740
+ // 10. SUPPLIERS - Find suppliers
1741
+ // ===========================================================================
1742
+ suppliers: async (supabase, args, storeId) => {
1743
+ try {
1744
+ let q = supabase.from("suppliers").select("*");
1745
+ if (storeId)
1746
+ q = q.eq("store_id", storeId);
1747
+ if (args.name)
1748
+ q = q.ilike("name", `%${args.name}%`);
1749
+ const { data, error } = await q.limit(50);
1750
+ if (error)
1751
+ return { success: false, error: error.message };
1752
+ return { success: true, data };
1753
+ }
1754
+ catch (err) {
1755
+ return { success: false, error: `Suppliers error: ${err}` };
1756
+ }
1757
+ },
1758
+ // ===========================================================================
1759
+ // 11. EMAIL - Unified email tool with inbox support
1760
+ // Actions: send, send_template, list, get, templates,
1761
+ // inbox, inbox_get, inbox_reply, inbox_update, inbox_stats
1762
+ // ===========================================================================
1763
+ email: async (supabase, args, storeId) => {
1764
+ const action = args.action;
1765
+ if (!action) {
1766
+ return { success: false, error: "action required: send, send_template, list, get, templates, inbox, inbox_get, inbox_reply, inbox_update, inbox_stats" };
1767
+ }
1768
+ const SUPABASE_URL = process.env.SUPABASE_URL || "https://uaednwpxursknmwdeejn.supabase.co";
1769
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
1770
+ const invokeEdgeFunction = async (functionName, body) => {
1771
+ const res = await fetch(`${SUPABASE_URL}/functions/v1/${functionName}`, {
1772
+ method: "POST",
1773
+ headers: {
1774
+ "Authorization": `Bearer ${SUPABASE_ANON_KEY}`,
1775
+ "Content-Type": "application/json"
1776
+ },
1777
+ body: JSON.stringify(body)
1778
+ });
1779
+ const data = await res.json();
1780
+ if (!res.ok)
1781
+ throw new Error(data.error || data.message || `HTTP ${res.status}`);
1782
+ return data;
1783
+ };
1784
+ try {
1785
+ switch (action) {
1786
+ case "send": {
1787
+ const to = args.to;
1788
+ const subject = args.subject;
1789
+ const html = args.html;
1790
+ const text = args.text;
1791
+ if (!to || !subject || (!html && !text)) {
1792
+ return { success: false, error: "send requires: to, subject, and html or text" };
1793
+ }
1794
+ const result = await invokeEdgeFunction("send-email", { to, subject, html, text, storeId });
1795
+ return { success: true, data: result };
1796
+ }
1797
+ case "send_template": {
1798
+ const to = args.to;
1799
+ const template = args.template;
1800
+ const templateData = args.template_data;
1801
+ if (!to || !template) {
1802
+ return { success: false, error: "send_template requires: to, template" };
1803
+ }
1804
+ const result = await invokeEdgeFunction("send-email", { to, template, template_data: templateData, storeId });
1805
+ return { success: true, data: result };
1806
+ }
1807
+ case "list": {
1808
+ let query = supabase
1809
+ .from("email_sends")
1810
+ .select("id, to_email, subject, status, category, created_at")
1811
+ .order("created_at", { ascending: false })
1812
+ .limit(args.limit || 50);
1813
+ if (storeId)
1814
+ query = query.eq("store_id", storeId);
1815
+ const { data, error } = await query;
1816
+ if (error)
1817
+ return { success: false, error: error.message };
1818
+ return { success: true, data };
1819
+ }
1820
+ case "get": {
1821
+ const emailId = args.email_id;
1822
+ if (!emailId)
1823
+ return { success: false, error: "get requires email_id" };
1824
+ const { data, error } = await supabase.from("email_sends").select("*").eq("id", emailId).single();
1825
+ if (error)
1826
+ return { success: false, error: error.message };
1827
+ return { success: true, data };
1828
+ }
1829
+ case "templates": {
1830
+ const { data, error } = await supabase
1831
+ .from("email_templates")
1832
+ .select("id, name, slug, subject, description, category, is_active")
1833
+ .eq("is_active", true);
1834
+ if (error)
1835
+ return { success: false, error: error.message };
1836
+ return { success: true, data };
1837
+ }
1838
+ // =====================================================================
1839
+ // INBOX ACTIONS - AI-powered inbound email management
1840
+ // =====================================================================
1841
+ case "inbox": {
1842
+ // List threads with optional filters
1843
+ const mailbox = args.mailbox;
1844
+ const status = args.status;
1845
+ const priority = args.priority;
1846
+ const limit = args.limit || 25;
1847
+ let query = supabase
1848
+ .from("email_threads")
1849
+ .select(`
1850
+ id, subject, mailbox, status, priority, intent, ai_summary,
1851
+ message_count, unread_count, last_message_at, created_at,
1852
+ customer:customers(id, first_name, last_name, email),
1853
+ order:orders(id, order_number, status)
1854
+ `)
1855
+ .order("last_message_at", { ascending: false })
1856
+ .limit(limit);
1857
+ if (storeId)
1858
+ query = query.eq("store_id", storeId);
1859
+ if (mailbox)
1860
+ query = query.eq("mailbox", mailbox);
1861
+ if (status)
1862
+ query = query.eq("status", status);
1863
+ if (priority)
1864
+ query = query.eq("priority", priority);
1865
+ const { data, error } = await query;
1866
+ if (error)
1867
+ return { success: false, error: error.message };
1868
+ return { success: true, data };
1869
+ }
1870
+ case "inbox_get": {
1871
+ // Get full thread with all messages and context
1872
+ const threadId = args.thread_id;
1873
+ if (!threadId)
1874
+ return { success: false, error: "inbox_get requires thread_id" };
1875
+ // Fetch thread
1876
+ const { data: thread, error: threadError } = await supabase
1877
+ .from("email_threads")
1878
+ .select(`
1879
+ *,
1880
+ customer:customers(id, first_name, last_name, email, phone),
1881
+ order:orders(id, order_number, status, total, created_at)
1882
+ `)
1883
+ .eq("id", threadId)
1884
+ .single();
1885
+ if (threadError)
1886
+ return { success: false, error: threadError.message };
1887
+ // Fetch all messages in thread
1888
+ const { data: messages, error: msgError } = await supabase
1889
+ .from("email_inbox")
1890
+ .select("id, direction, from_email, from_name, to_email, subject, body_text, body_html, status, ai_draft, ai_intent, ai_confidence, has_attachments, created_at, read_at, replied_at")
1891
+ .eq("thread_id", threadId)
1892
+ .order("created_at", { ascending: true });
1893
+ if (msgError)
1894
+ return { success: false, error: msgError.message };
1895
+ // Mark unread messages as read
1896
+ const unreadIds = (messages || [])
1897
+ .filter((m) => m.direction === "inbound" && !m.read_at)
1898
+ .map((m) => m.id);
1899
+ if (unreadIds.length > 0) {
1900
+ await supabase
1901
+ .from("email_inbox")
1902
+ .update({ status: "read", read_at: new Date().toISOString() })
1903
+ .in("id", unreadIds);
1904
+ await supabase
1905
+ .from("email_threads")
1906
+ .update({ unread_count: 0 })
1907
+ .eq("id", threadId);
1908
+ }
1909
+ return { success: true, data: { thread, messages } };
1910
+ }
1911
+ case "inbox_reply": {
1912
+ // Reply to a thread
1913
+ const threadId = args.thread_id;
1914
+ const html = args.html;
1915
+ const text = args.text;
1916
+ if (!threadId)
1917
+ return { success: false, error: "inbox_reply requires thread_id" };
1918
+ if (!html && !text)
1919
+ return { success: false, error: "inbox_reply requires html or text body" };
1920
+ // Get thread + last inbound message for threading
1921
+ const { data: thread } = await supabase
1922
+ .from("email_threads")
1923
+ .select("id, subject, customer:customers(email)")
1924
+ .eq("id", threadId)
1925
+ .single();
1926
+ if (!thread)
1927
+ return { success: false, error: "Thread not found" };
1928
+ const { data: lastInbound } = await supabase
1929
+ .from("email_inbox")
1930
+ .select("from_email, message_id, references")
1931
+ .eq("thread_id", threadId)
1932
+ .eq("direction", "inbound")
1933
+ .order("created_at", { ascending: false })
1934
+ .limit(1)
1935
+ .single();
1936
+ if (!lastInbound)
1937
+ return { success: false, error: "No inbound message to reply to" };
1938
+ // Build threading references
1939
+ const refs = lastInbound.references || [];
1940
+ if (lastInbound.message_id && !refs.includes(lastInbound.message_id)) {
1941
+ refs.push(lastInbound.message_id);
1942
+ }
1943
+ const result = await invokeEdgeFunction("send-email", {
1944
+ to: lastInbound.from_email,
1945
+ subject: `Re: ${thread.subject || ""}`,
1946
+ html,
1947
+ text,
1948
+ storeId,
1949
+ thread_id: threadId,
1950
+ in_reply_to: lastInbound.message_id,
1951
+ references: refs,
1952
+ });
1953
+ return { success: true, data: result };
1954
+ }
1955
+ case "inbox_update": {
1956
+ // Update thread status, priority, intent, or summary
1957
+ const threadId = args.thread_id;
1958
+ if (!threadId)
1959
+ return { success: false, error: "inbox_update requires thread_id" };
1960
+ const updates = { updated_at: new Date().toISOString() };
1961
+ if (args.status)
1962
+ updates.status = args.status;
1963
+ if (args.priority)
1964
+ updates.priority = args.priority;
1965
+ if (args.intent)
1966
+ updates.intent = args.intent;
1967
+ if (args.ai_summary)
1968
+ updates.ai_summary = args.ai_summary;
1969
+ const { data, error } = await supabase
1970
+ .from("email_threads")
1971
+ .update(updates)
1972
+ .eq("id", threadId)
1973
+ .select("id, status, priority, intent, ai_summary")
1974
+ .single();
1975
+ if (error)
1976
+ return { success: false, error: error.message };
1977
+ return { success: true, data };
1978
+ }
1979
+ case "inbox_stats": {
1980
+ // Inbox statistics
1981
+ let query = supabase
1982
+ .from("email_threads")
1983
+ .select("mailbox, status, priority", { count: "exact" });
1984
+ if (storeId)
1985
+ query = query.eq("store_id", storeId);
1986
+ const { data: threads, error } = await query;
1987
+ if (error)
1988
+ return { success: false, error: error.message };
1989
+ // Aggregate counts
1990
+ const stats = {
1991
+ total: (threads || []).length,
1992
+ by_status: {},
1993
+ by_mailbox: {},
1994
+ by_priority: {},
1995
+ };
1996
+ for (const t of (threads || [])) {
1997
+ stats.by_status[t.status] = (stats.by_status[t.status] || 0) + 1;
1998
+ stats.by_mailbox[t.mailbox] = (stats.by_mailbox[t.mailbox] || 0) + 1;
1999
+ stats.by_priority[t.priority] = (stats.by_priority[t.priority] || 0) + 1;
2000
+ }
2001
+ return { success: true, data: stats };
2002
+ }
2003
+ // =====================================================================
2004
+ // DOMAIN MANAGEMENT - Multi-tenant email domain configuration
2005
+ // =====================================================================
2006
+ case "domains_list": {
2007
+ // List email domains for the store
2008
+ let query = supabase
2009
+ .from("store_email_domains")
2010
+ .select("id, domain, inbound_subdomain, status, receiving_enabled, sending_verified, dns_records, created_at");
2011
+ if (storeId)
2012
+ query = query.eq("store_id", storeId);
2013
+ const { data, error } = await query;
2014
+ if (error)
2015
+ return { success: false, error: error.message };
2016
+ return { success: true, data };
2017
+ }
2018
+ case "domains_add": {
2019
+ // Add a new email domain for the store
2020
+ const domain = args.domain;
2021
+ const inboundSubdomain = args.inbound_subdomain || "in";
2022
+ if (!domain)
2023
+ return { success: false, error: "domain required" };
2024
+ if (!storeId)
2025
+ return { success: false, error: "store_id required" };
2026
+ // Call Resend API to add domain
2027
+ const RESEND_API_KEY = process.env.RESEND_API_KEY;
2028
+ if (!RESEND_API_KEY)
2029
+ return { success: false, error: "RESEND_API_KEY not configured" };
2030
+ const fullDomain = `${inboundSubdomain}.${domain}`;
2031
+ const resendRes = await fetch("https://api.resend.com/domains", {
2032
+ method: "POST",
2033
+ headers: {
2034
+ "Authorization": `Bearer ${RESEND_API_KEY}`,
2035
+ "Content-Type": "application/json"
2036
+ },
2037
+ body: JSON.stringify({ name: fullDomain, region: "us-east-1" })
2038
+ });
2039
+ const resendData = await resendRes.json();
2040
+ if (!resendRes.ok) {
2041
+ return { success: false, error: `Resend API error: ${resendData.message || resendRes.status}` };
2042
+ }
2043
+ // Enable receiving
2044
+ await fetch(`https://api.resend.com/domains/${resendData.id}`, {
2045
+ method: "PATCH",
2046
+ headers: {
2047
+ "Authorization": `Bearer ${RESEND_API_KEY}`,
2048
+ "Content-Type": "application/json"
2049
+ },
2050
+ body: JSON.stringify({ capabilities: { receiving: "enabled" } })
2051
+ });
2052
+ // Format DNS records for display
2053
+ const dnsRecords = (resendData.records || []).map((r) => ({
2054
+ record: r.record,
2055
+ name: r.name,
2056
+ type: r.type,
2057
+ value: r.value,
2058
+ priority: r.priority,
2059
+ status: r.status
2060
+ }));
2061
+ // Add receiving MX record to the list
2062
+ dnsRecords.push({
2063
+ record: "Receiving",
2064
+ name: inboundSubdomain,
2065
+ type: "MX",
2066
+ value: "inbound-smtp.us-east-1.amazonaws.com",
2067
+ priority: 10,
2068
+ status: "pending"
2069
+ });
2070
+ // Insert into database
2071
+ const { data, error } = await supabase
2072
+ .from("store_email_domains")
2073
+ .insert({
2074
+ store_id: storeId,
2075
+ domain,
2076
+ inbound_subdomain: inboundSubdomain,
2077
+ resend_domain_id: resendData.id,
2078
+ status: "pending",
2079
+ receiving_enabled: true,
2080
+ dns_records: dnsRecords
2081
+ })
2082
+ .select()
2083
+ .single();
2084
+ if (error)
2085
+ return { success: false, error: error.message };
2086
+ return {
2087
+ success: true,
2088
+ data: {
2089
+ ...data,
2090
+ message: "Domain added. Please add the following DNS records to your domain registrar:",
2091
+ dns_records: dnsRecords
2092
+ }
2093
+ };
2094
+ }
2095
+ case "domains_verify": {
2096
+ // Trigger verification for a domain
2097
+ const domainId = args.domain_id;
2098
+ if (!domainId)
2099
+ return { success: false, error: "domain_id required" };
2100
+ // Get domain record
2101
+ const { data: domainRecord, error: fetchError } = await supabase
2102
+ .from("store_email_domains")
2103
+ .select("*")
2104
+ .eq("id", domainId)
2105
+ .single();
2106
+ if (fetchError || !domainRecord) {
2107
+ return { success: false, error: "Domain not found" };
2108
+ }
2109
+ // Call Resend verify API
2110
+ const RESEND_API_KEY = process.env.RESEND_API_KEY;
2111
+ if (!RESEND_API_KEY)
2112
+ return { success: false, error: "RESEND_API_KEY not configured" };
2113
+ await fetch(`https://api.resend.com/domains/${domainRecord.resend_domain_id}/verify`, {
2114
+ method: "POST",
2115
+ headers: { "Authorization": `Bearer ${RESEND_API_KEY}` }
2116
+ });
2117
+ // Fetch updated status
2118
+ const statusRes = await fetch(`https://api.resend.com/domains/${domainRecord.resend_domain_id}`, {
2119
+ headers: { "Authorization": `Bearer ${RESEND_API_KEY}` }
2120
+ });
2121
+ const statusData = await statusRes.json();
2122
+ // Update database
2123
+ const dnsRecords = (statusData.records || []).map((r) => ({
2124
+ record: r.record,
2125
+ name: r.name,
2126
+ type: r.type,
2127
+ value: r.value,
2128
+ priority: r.priority,
2129
+ status: r.status
2130
+ }));
2131
+ const receivingVerified = dnsRecords.some((r) => r.record === "Receiving" && r.status === "verified");
2132
+ const sendingVerified = dnsRecords.filter((r) => r.record !== "Receiving").every((r) => r.status === "verified");
2133
+ const { data, error } = await supabase
2134
+ .from("store_email_domains")
2135
+ .update({
2136
+ status: statusData.status === "verified" || receivingVerified ? "verified" : "pending",
2137
+ sending_verified: sendingVerified,
2138
+ dns_records: dnsRecords,
2139
+ verified_at: receivingVerified ? new Date().toISOString() : null
2140
+ })
2141
+ .eq("id", domainId)
2142
+ .select()
2143
+ .single();
2144
+ if (error)
2145
+ return { success: false, error: error.message };
2146
+ return { success: true, data };
2147
+ }
2148
+ case "addresses_list": {
2149
+ // List email addresses for the store
2150
+ let query = supabase
2151
+ .from("store_email_addresses")
2152
+ .select(`
2153
+ id, address, display_name, mailbox_type, ai_enabled, ai_auto_reply, is_active, created_at,
2154
+ domain:store_email_domains(id, domain, inbound_subdomain, status)
2155
+ `);
2156
+ if (storeId)
2157
+ query = query.eq("store_id", storeId);
2158
+ const { data, error } = await query;
2159
+ if (error)
2160
+ return { success: false, error: error.message };
2161
+ // Format with full email address
2162
+ const addresses = (data || []).map((a) => ({
2163
+ ...a,
2164
+ full_email: a.domain ? `${a.address}@${a.domain.inbound_subdomain}.${a.domain.domain}` : null
2165
+ }));
2166
+ return { success: true, data: addresses };
2167
+ }
2168
+ case "addresses_add": {
2169
+ // Add a new email address/mailbox
2170
+ const domainId = args.domain_id;
2171
+ const address = args.address;
2172
+ const displayName = args.display_name;
2173
+ const mailboxType = args.mailbox_type || "general";
2174
+ const aiEnabled = args.ai_enabled !== false;
2175
+ if (!domainId || !address) {
2176
+ return { success: false, error: "domain_id and address required" };
2177
+ }
2178
+ if (!storeId)
2179
+ return { success: false, error: "store_id required" };
2180
+ // Verify domain belongs to store
2181
+ const { data: domain } = await supabase
2182
+ .from("store_email_domains")
2183
+ .select("id, domain, inbound_subdomain")
2184
+ .eq("id", domainId)
2185
+ .eq("store_id", storeId)
2186
+ .single();
2187
+ if (!domain)
2188
+ return { success: false, error: "Domain not found or doesn't belong to store" };
2189
+ const { data, error } = await supabase
2190
+ .from("store_email_addresses")
2191
+ .insert({
2192
+ store_id: storeId,
2193
+ domain_id: domainId,
2194
+ address: address.toLowerCase(),
2195
+ display_name: displayName,
2196
+ mailbox_type: mailboxType,
2197
+ ai_enabled: aiEnabled
2198
+ })
2199
+ .select()
2200
+ .single();
2201
+ if (error)
2202
+ return { success: false, error: error.message };
2203
+ return {
2204
+ success: true,
2205
+ data: {
2206
+ ...data,
2207
+ full_email: `${address}@${domain.inbound_subdomain}.${domain.domain}`
2208
+ }
2209
+ };
2210
+ }
2211
+ default:
2212
+ return { success: false, error: `Unknown action: ${action}. Use: send, send_template, list, get, templates, inbox, inbox_get, inbox_reply, inbox_update, inbox_stats, domains_list, domains_add, domains_verify, addresses_list, addresses_add` };
2213
+ }
2214
+ }
2215
+ catch (err) {
2216
+ return { success: false, error: `Email error: ${err}` };
2217
+ }
2218
+ },
2219
+ // ===========================================================================
2220
+ // 12. DOCUMENTS - Document generation (COA, etc.)
2221
+ // ===========================================================================
2222
+ documents: async (supabase, args, storeId) => {
2223
+ try {
2224
+ const DOCUMENTS_API_URL = process.env.DOCUMENTS_API_URL || "http://localhost:3102/api/tools";
2225
+ const oauthToken = process.env.DOCUMENTS_OAUTH_TOKEN;
2226
+ const headers = { "Content-Type": "application/json" };
2227
+ if (oauthToken)
2228
+ headers["Authorization"] = `Bearer ${oauthToken}`;
2229
+ const response = await fetch(DOCUMENTS_API_URL, {
2230
+ method: "POST",
2231
+ headers,
2232
+ body: JSON.stringify({ tool: "documents", input: args, context: { storeId } })
2233
+ });
2234
+ const result = await response.json();
2235
+ if (!response.ok)
2236
+ return { success: false, error: result.error || `HTTP ${response.status}` };
2237
+ return { success: result.success, data: result.data, error: result.error };
2238
+ }
2239
+ catch (err) {
2240
+ return { success: false, error: `Documents error: ${err}` };
2241
+ }
2242
+ },
2243
+ // ===========================================================================
2244
+ // 13. ALERTS - System alerts (low stock, pending orders)
2245
+ // ===========================================================================
2246
+ alerts: async (supabase, args, storeId) => {
2247
+ try {
2248
+ const { data: lowStock } = await supabase
2249
+ .from("inventory")
2250
+ .select("product_id, quantity, products(name)")
2251
+ .lt("quantity", 10)
2252
+ .limit(20);
2253
+ const { data: pendingOrders } = await supabase
2254
+ .from("orders")
2255
+ .select("id, order_number")
2256
+ .eq("status", "pending")
2257
+ .limit(20);
2258
+ return {
2259
+ success: true,
2260
+ data: {
2261
+ lowStock: lowStock?.length || 0,
2262
+ pendingOrders: pendingOrders?.length || 0,
2263
+ alerts: [
2264
+ ...(lowStock || []).map(i => ({ type: "low_stock", product: i.products?.name, quantity: i.quantity })),
2265
+ ...(pendingOrders || []).map(o => ({ type: "pending_order", order_number: o.order_number }))
2266
+ ]
2267
+ }
2268
+ };
2269
+ }
2270
+ catch (err) {
2271
+ return { success: false, error: `Alerts error: ${err}` };
2272
+ }
2273
+ },
2274
+ // ===========================================================================
2275
+ // 14. AUDIT_TRAIL - View audit logs
2276
+ // ===========================================================================
2277
+ audit_trail: async (supabase, args, storeId) => {
2278
+ try {
2279
+ const limit = args.limit || 50;
2280
+ let q = supabase
2281
+ .from("audit_logs")
2282
+ .select("*")
2283
+ .order("created_at", { ascending: false })
2284
+ .limit(limit);
2285
+ if (storeId)
2286
+ q = q.eq("store_id", storeId);
2287
+ const { data, error } = await q;
2288
+ if (error)
2289
+ return { success: false, error: error.message };
2290
+ return { success: true, data };
2291
+ }
2292
+ catch (err) {
2293
+ return { success: false, error: `Audit trail error: ${err}` };
2294
+ }
2295
+ },
2296
+ };
2297
+ // Generate W3C-compliant span ID (16 hex chars)
2298
+ function generateSpanId() {
2299
+ const bytes = new Uint8Array(8);
2300
+ crypto.getRandomValues(bytes);
2301
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
2302
+ }
2303
+ // Generate W3C-compliant trace ID (32 hex chars)
2304
+ function generateTraceId() {
2305
+ const bytes = new Uint8Array(16);
2306
+ crypto.getRandomValues(bytes);
2307
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
2308
+ }
2309
+ async function logToolExecution(supabase, toolName, action, args, result, durationMs, storeId, context, startTime) {
2310
+ try {
2311
+ // Sanitize args - remove sensitive data (Anthropic best practice: comprehensive pattern matching)
2312
+ const SENSITIVE_PATTERNS = [
2313
+ /key/i,
2314
+ /secret/i,
2315
+ /token/i,
2316
+ /password/i,
2317
+ /auth/i,
2318
+ /credential/i,
2319
+ /bearer/i,
2320
+ /api.?key/i,
2321
+ /private/i
2322
+ ];
2323
+ const sanitizedArgs = {};
2324
+ for (const [key, value] of Object.entries(args)) {
2325
+ if (SENSITIVE_PATTERNS.some(pattern => pattern.test(key))) {
2326
+ sanitizedArgs[key] = "[REDACTED]";
2327
+ }
2328
+ else {
2329
+ sanitizedArgs[key] = value;
2330
+ }
2331
+ }
2332
+ // Generate W3C Trace Context IDs
2333
+ const spanId = context?.spanId || generateSpanId();
2334
+ const traceId = context?.traceId || context?.requestId || generateTraceId();
2335
+ const endTime = new Date();
2336
+ const actualStartTime = startTime || new Date(endTime.getTime() - durationMs);
2337
+ // Build the insert payload with BOTH denormalized columns and JSONB details
2338
+ const corePayload = {
2339
+ action: `tool.${toolName}${action ? `.${action}` : ""}`,
2340
+ severity: result.success ? "info" : "error",
2341
+ store_id: storeId || null,
2342
+ user_id: context?.userId || null,
2343
+ resource_type: "mcp_tool",
2344
+ resource_id: toolName,
2345
+ request_id: context?.requestId || traceId,
2346
+ parent_id: context?.parentId || null,
2347
+ duration_ms: durationMs,
2348
+ error_message: result.error || null,
2349
+ // Denormalized OTEL columns for fast queries/aggregations
2350
+ trace_id: traceId,
2351
+ span_id: spanId,
2352
+ trace_flags: context?.traceFlags ?? 1,
2353
+ span_kind: "INTERNAL",
2354
+ service_name: context?.serviceName || "agent-server",
2355
+ service_version: context?.serviceVersion || "3.0.0",
2356
+ status_code: result.success ? "OK" : "ERROR",
2357
+ start_time: actualStartTime.toISOString(),
2358
+ end_time: endTime.toISOString(),
2359
+ error_type: result.errorType || null,
2360
+ retryable: result.retryable ?? false,
2361
+ // AI telemetry columns (if executing in agent context)
2362
+ model: context?.model || null,
2363
+ input_tokens: context?.inputTokens || null,
2364
+ output_tokens: context?.outputTokens || null,
2365
+ total_cost: context?.totalCost || null,
2366
+ turn_number: context?.turnNumber || null,
2367
+ conversation_id: context?.conversationId || null,
2368
+ // JSONB details for flexible/extended attributes
2369
+ details: {
2370
+ // Source and agent info (for UI filtering)
2371
+ source: context?.source || "api",
2372
+ agent_id: context?.agentId || null,
2373
+ agent_name: context?.agentName || null,
2374
+ // Tool execution details (for UI display)
2375
+ tool_input: sanitizedArgs,
2376
+ tool_result: result.success ? (typeof result.data === 'string' ? result.data.slice(0, 1000) : result.data) : null,
2377
+ tool_error: result.error || null,
2378
+ timed_out: result.timedOut || false,
2379
+ // Cost isolation: marginal cost of the API turn that triggered this tool
2380
+ marginal_cost: context?.turnCost ?? null,
2381
+ cost_before: context?.costBefore ?? null,
2382
+ // Payload size tracking (bytes)
2383
+ input_bytes: JSON.stringify(sanitizedArgs).length,
2384
+ output_bytes: result.success
2385
+ ? (typeof result.data === 'string' ? result.data.length : JSON.stringify(result.data).length)
2386
+ : (result.error?.length ?? 0),
2387
+ // Error classification (surfaced for UI badges)
2388
+ error_type: result.errorType || null,
2389
+ retryable: result.retryable ?? false,
2390
+ // Legacy/backwards compatibility
2391
+ args: sanitizedArgs,
2392
+ result: result.success ? result.data : null,
2393
+ // OTEL context embedded (for tools that need full context)
2394
+ otel: {
2395
+ trace_id: traceId,
2396
+ span_id: spanId,
2397
+ parent_span_id: context?.parentSpanId || null,
2398
+ trace_flags: context?.traceFlags ?? 1,
2399
+ span_kind: "INTERNAL",
2400
+ service_name: context?.serviceName || "agent-server",
2401
+ service_version: context?.serviceVersion || "3.0.0",
2402
+ status_code: result.success ? "OK" : "ERROR",
2403
+ error_type: result.errorType || null,
2404
+ retryable: result.retryable ?? false
2405
+ },
2406
+ // OTEL resource attributes (gen_ai.* + tool.* semantic conventions)
2407
+ resource_attributes: {
2408
+ "tool.name": toolName,
2409
+ "tool.action": action || null,
2410
+ "tool.input_bytes": JSON.stringify(sanitizedArgs).length,
2411
+ "tool.output_bytes": result.success
2412
+ ? (typeof result.data === 'string' ? result.data.length : JSON.stringify(result.data).length)
2413
+ : 0,
2414
+ "ai.agent.id": context?.agentId || null,
2415
+ "ai.agent.name": context?.agentName || null,
2416
+ "ai.model": context?.model || null,
2417
+ "ai.turn_number": context?.turnNumber || null,
2418
+ "ai.conversation_id": context?.conversationId || null
2419
+ }
2420
+ }
2421
+ };
2422
+ // Insert using existing schema - OTEL data is embedded in details JSONB
2423
+ // This works without any migration and the UI can parse details.otel and details.ai
2424
+ const { data } = await supabase.from("audit_logs").insert(corePayload).select("id").single();
2425
+ return data?.id || null;
2426
+ }
2427
+ catch (err) {
2428
+ // Don't fail the tool call if logging fails
2429
+ console.error("[Telemetry] Failed to log:", err);
2430
+ return null;
2431
+ }
2432
+ }
2433
+ // ============================================================================
2434
+ // EXECUTOR
2435
+ // ============================================================================
2436
+ export async function executeTool(supabase, toolName, args, storeId, context) {
2437
+ const startDate = new Date();
2438
+ const startTime = startDate.getTime();
2439
+ const action = args.action;
2440
+ const handler = handlers[toolName];
2441
+ if (!handler) {
2442
+ const result = {
2443
+ success: false,
2444
+ error: `Tool "${toolName}" not found. Available tools: ${Object.keys(handlers).join(", ")}`,
2445
+ errorType: ToolErrorType.VALIDATION,
2446
+ retryable: false
2447
+ };
2448
+ // Log failed tool lookup with OTEL fields
2449
+ await logToolExecution(supabase, toolName, action, args, result, Date.now() - startTime, storeId, context, startDate);
2450
+ return result;
2451
+ }
2452
+ let result;
2453
+ try {
2454
+ result = await handler(supabase, args, storeId);
2455
+ // If handler returned an error without classification, classify it
2456
+ if (!result.success && !result.errorType) {
2457
+ const classification = classifyError({ message: result.error });
2458
+ result.errorType = classification.errorType;
2459
+ result.retryable = classification.retryable;
2460
+ }
2461
+ }
2462
+ catch (err) {
2463
+ // Classify the caught error
2464
+ const classification = classifyError(err);
2465
+ result = {
2466
+ success: false,
2467
+ error: `Tool execution error: ${err.message || err}`,
2468
+ errorType: classification.errorType,
2469
+ retryable: classification.retryable
2470
+ };
2471
+ }
2472
+ // Log the execution with full OTEL telemetry
2473
+ const durationMs = Date.now() - startTime;
2474
+ await logToolExecution(supabase, toolName, action, args, result, durationMs, storeId, context, startDate);
2475
+ return result;
2476
+ }
2477
+ export function getImplementedTools() {
2478
+ return Object.keys(handlers);
2479
+ }