whale-code 6.4.0 → 6.5.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.
- package/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { sanitizeFilterValue } from "../lib/utils.js";
|
|
2
|
+
import { handleEmail } from "./comms.js";
|
|
3
|
+
/** Strip internal infrastructure fields that stores should never see */
|
|
4
|
+
function stripInternal(row) {
|
|
5
|
+
if (!row)
|
|
6
|
+
return null;
|
|
7
|
+
const { platform_user_id, store_id, password_hash, ...clean } = row;
|
|
8
|
+
return clean;
|
|
9
|
+
}
|
|
10
|
+
function stripInternalArray(rows) {
|
|
11
|
+
return (rows || []).map(r => stripInternal(r));
|
|
12
|
+
}
|
|
2
13
|
export async function handleCustomers(sb, args, storeId) {
|
|
3
14
|
const sid = storeId;
|
|
4
15
|
switch (args.action) {
|
|
5
|
-
// ---- FIND: search customers via
|
|
16
|
+
// ---- FIND: search customers via v_segment_customers view ----
|
|
6
17
|
case "find": {
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
const sortField = args.sort_by || "created_at";
|
|
19
|
+
const sortAsc = args.sort_order === "asc";
|
|
20
|
+
let q = sb.from("v_segment_customers")
|
|
21
|
+
.select("id, first_name, last_name, email, phone, loyalty_points, loyalty_tier, total_spent, total_orders, lifetime_value, is_active, rfm_segment, is_vip_customer, is_at_risk, is_churned, reorder_due, ai_churn_risk, days_since_last_order, engagement_score, age_bracket, created_at")
|
|
9
22
|
.eq("store_id", sid)
|
|
10
|
-
.order(
|
|
23
|
+
.order(sortField, { ascending: sortAsc });
|
|
11
24
|
if (args.limit)
|
|
12
25
|
q = q.limit(args.limit);
|
|
13
26
|
if (args.query) {
|
|
14
27
|
const raw = sanitizeFilterValue(String(args.query).trim());
|
|
15
|
-
// Split multi-word queries so "Hannah Spivey" matches first_name=Hannah OR last_name=Spivey
|
|
16
28
|
const words = raw.split(/\s+/).filter(Boolean);
|
|
17
29
|
if (words.length > 1) {
|
|
18
|
-
// Multi-word: each word matches any field
|
|
19
30
|
const clauses = words.map(w => { const sw = sanitizeFilterValue(w); return `first_name.ilike.%${sw}%,last_name.ilike.%${sw}%`; }).join(",");
|
|
20
31
|
q = q.or(`${clauses},email.ilike.%${raw}%,phone.ilike.%${raw}%`);
|
|
21
32
|
}
|
|
@@ -30,79 +41,147 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
30
41
|
q = q.eq("is_active", false);
|
|
31
42
|
if (args.loyalty_tier)
|
|
32
43
|
q = q.eq("loyalty_tier", args.loyalty_tier);
|
|
44
|
+
if (args.rfm_segment)
|
|
45
|
+
q = q.eq("rfm_segment", args.rfm_segment);
|
|
46
|
+
if (args.is_vip !== undefined)
|
|
47
|
+
q = q.eq("is_vip_customer", args.is_vip);
|
|
48
|
+
if (args.is_at_risk !== undefined)
|
|
49
|
+
q = q.eq("is_at_risk", args.is_at_risk);
|
|
50
|
+
if (args.is_churned !== undefined)
|
|
51
|
+
q = q.eq("is_churned", args.is_churned);
|
|
52
|
+
if (args.reorder_due !== undefined)
|
|
53
|
+
q = q.eq("reorder_due", args.reorder_due);
|
|
54
|
+
if (args.age_bracket)
|
|
55
|
+
q = q.eq("age_bracket", args.age_bracket);
|
|
56
|
+
if (args.min_orders !== undefined)
|
|
57
|
+
q = q.gte("total_orders", args.min_orders);
|
|
58
|
+
if (args.max_orders !== undefined)
|
|
59
|
+
q = q.lte("total_orders", args.max_orders);
|
|
60
|
+
if (args.min_spent !== undefined)
|
|
61
|
+
q = q.gte("total_spent", args.min_spent);
|
|
62
|
+
if (args.max_spent !== undefined)
|
|
63
|
+
q = q.lte("total_spent", args.max_spent);
|
|
33
64
|
const { data, error } = await q;
|
|
34
65
|
return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
|
|
35
66
|
}
|
|
36
|
-
// ---- GET: full customer detail with orders, activity, notes ----
|
|
67
|
+
// ---- GET: full 360° customer detail with orders, activity, notes, loyalty, segments ----
|
|
37
68
|
case "get": {
|
|
38
69
|
const custId = args.customer_id;
|
|
39
|
-
const { data: customer, error: custErr } = await sb.from("
|
|
70
|
+
const { data: customer, error: custErr } = await sb.from("v_segment_customers")
|
|
40
71
|
.select("*").eq("id", custId).eq("store_id", sid).single();
|
|
41
72
|
if (custErr)
|
|
42
73
|
return { success: false, error: custErr.message };
|
|
43
|
-
const { data: orders } = await
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
const [{ data: orders }, { data: notes }, { data: activity }, { data: profile }, { data: loyaltyHistory }, { data: segmentRows }] = await Promise.all([
|
|
75
|
+
sb.from("orders")
|
|
76
|
+
.select("id, order_number, status, total_amount, payment_status, fulfillment_status, created_at")
|
|
77
|
+
.eq("customer_id", custId).eq("store_id", sid)
|
|
78
|
+
.order("created_at", { ascending: false })
|
|
79
|
+
.limit(args.orders_limit || 10),
|
|
80
|
+
sb.from("customer_notes")
|
|
81
|
+
.select("id, note, created_by, created_at")
|
|
82
|
+
.eq("customer_id", custId)
|
|
83
|
+
.order("created_at", { ascending: false }).limit(10),
|
|
84
|
+
sb.from("customer_activity")
|
|
85
|
+
.select("id, activity_type, description, created_at")
|
|
86
|
+
.eq("customer_id", custId)
|
|
87
|
+
.order("created_at", { ascending: false }).limit(10),
|
|
88
|
+
sb.from("store_customer_profiles")
|
|
89
|
+
.select("*").eq("relationship_id", custId).maybeSingle(),
|
|
90
|
+
sb.from("loyalty_transactions")
|
|
91
|
+
.select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
|
|
92
|
+
.eq("customer_id", custId)
|
|
93
|
+
.order("created_at", { ascending: false }).limit(20),
|
|
94
|
+
sb.from("customer_segment_memberships")
|
|
95
|
+
.select("added_at, segment:customer_segments(id, name, type)")
|
|
96
|
+
.eq("customer_id", custId),
|
|
97
|
+
]);
|
|
98
|
+
// Flatten profile fields into customer object (skip sensitive + computed-only fields)
|
|
60
99
|
const prof = profile;
|
|
61
|
-
const profileFields =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
const profileFields = {};
|
|
101
|
+
if (prof) {
|
|
102
|
+
const skip = new Set([
|
|
103
|
+
"id", "relationship_id", "created_at", "updated_at",
|
|
104
|
+
"password_hash", // never expose
|
|
105
|
+
// These are recomputed by refresh_metrics — read from the view instead
|
|
106
|
+
"total_spent", "total_orders", "lifetime_value", "average_order_value",
|
|
107
|
+
"first_order_at", "last_order_at",
|
|
108
|
+
]);
|
|
109
|
+
for (const [k, v] of Object.entries(prof)) {
|
|
110
|
+
if (!skip.has(k))
|
|
111
|
+
profileFields[k] = v ?? null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Flatten segment memberships
|
|
115
|
+
const segments = (segmentRows || []).map((r) => ({
|
|
116
|
+
segment_id: r.segment?.id, name: r.segment?.name, type: r.segment?.type, added_at: r.added_at,
|
|
117
|
+
}));
|
|
118
|
+
return { success: true, data: stripInternal({ ...customer, ...profileFields, orders, notes, activity, loyalty_history: loyaltyHistory || [], segments }) };
|
|
74
119
|
}
|
|
75
120
|
// ---- CREATE: new customer (platform_user + relationship + profile) ----
|
|
121
|
+
// Matching priority: email → phone → drivers_license → name+DOB → create new
|
|
76
122
|
case "create": {
|
|
77
123
|
const email = args.email;
|
|
78
124
|
const phone = args.phone;
|
|
79
125
|
const firstName = args.first_name;
|
|
80
126
|
const lastName = args.last_name;
|
|
127
|
+
const dob = args.date_of_birth;
|
|
128
|
+
const dl = args.drivers_license_number;
|
|
81
129
|
if (!firstName && !lastName)
|
|
82
130
|
return { success: false, error: "first_name or last_name is required" };
|
|
83
|
-
//
|
|
131
|
+
// Multi-tier matching to find existing platform_user (mirrors POS verify flow)
|
|
84
132
|
let platformUserId = null;
|
|
85
|
-
|
|
133
|
+
let matchedVia = null;
|
|
134
|
+
// Tier 1: email
|
|
135
|
+
if (!platformUserId && email) {
|
|
86
136
|
const { data: existing } = await sb.from("platform_users")
|
|
87
137
|
.select("id").eq("email", email).maybeSingle();
|
|
88
|
-
if (existing)
|
|
138
|
+
if (existing) {
|
|
89
139
|
platformUserId = existing.id;
|
|
140
|
+
matchedVia = "email";
|
|
141
|
+
}
|
|
90
142
|
}
|
|
143
|
+
// Tier 2: phone
|
|
91
144
|
if (!platformUserId && phone) {
|
|
92
145
|
const { data: existing } = await sb.from("platform_users")
|
|
93
146
|
.select("id").eq("phone", phone).maybeSingle();
|
|
94
|
-
if (existing)
|
|
147
|
+
if (existing) {
|
|
95
148
|
platformUserId = existing.id;
|
|
149
|
+
matchedVia = "phone";
|
|
150
|
+
}
|
|
96
151
|
}
|
|
97
|
-
//
|
|
152
|
+
// Tier 3: drivers license (look up via store_customer_profiles → relationship → platform_user)
|
|
153
|
+
if (!platformUserId && dl) {
|
|
154
|
+
const { data: profMatch } = await sb.from("store_customer_profiles")
|
|
155
|
+
.select("relationship_id, relationship:user_creation_relationships!relationship_id(user_id, store_id)")
|
|
156
|
+
.eq("drivers_license_number", dl).limit(1).maybeSingle();
|
|
157
|
+
if (profMatch) {
|
|
158
|
+
const rel = profMatch.relationship;
|
|
159
|
+
if (rel?.user_id) {
|
|
160
|
+
platformUserId = rel.user_id;
|
|
161
|
+
matchedVia = "drivers_license";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Tier 4: exact name + date of birth (only when all 3 are provided)
|
|
166
|
+
if (!platformUserId && firstName && lastName && dob) {
|
|
167
|
+
const { data: nameMatch } = await sb.from("platform_users")
|
|
168
|
+
.select("id").ilike("first_name", firstName).ilike("last_name", lastName)
|
|
169
|
+
.eq("date_of_birth", dob).limit(1).maybeSingle();
|
|
170
|
+
if (nameMatch) {
|
|
171
|
+
platformUserId = nameMatch.id;
|
|
172
|
+
matchedVia = "name+dob";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Create platform_user if no match found
|
|
176
|
+
const isNameOnly = !email && !phone;
|
|
98
177
|
if (!platformUserId) {
|
|
99
178
|
const puInsert = { first_name: firstName, last_name: lastName };
|
|
100
179
|
if (email)
|
|
101
180
|
puInsert.email = email;
|
|
102
181
|
if (phone)
|
|
103
182
|
puInsert.phone = phone;
|
|
104
|
-
if (
|
|
105
|
-
puInsert.date_of_birth =
|
|
183
|
+
if (dob)
|
|
184
|
+
puInsert.date_of_birth = dob;
|
|
106
185
|
const { data: newPu, error: puErr } = await sb.from("platform_users")
|
|
107
186
|
.insert(puInsert).select("id").single();
|
|
108
187
|
if (puErr)
|
|
@@ -115,7 +194,7 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
115
194
|
if (existingRel) {
|
|
116
195
|
const { data: existing } = await sb.from("v_store_customers")
|
|
117
196
|
.select("*").eq("id", existingRel.id).single();
|
|
118
|
-
return { success: true, data: existing, note:
|
|
197
|
+
return { success: true, data: stripInternal(existing), note: `Customer already exists (matched via ${matchedVia || "existing record"})` };
|
|
119
198
|
}
|
|
120
199
|
// Create relationship
|
|
121
200
|
const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
|
|
@@ -137,44 +216,63 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
137
216
|
profileInsert.state = args.state;
|
|
138
217
|
if (args.postal_code)
|
|
139
218
|
profileInsert.postal_code = args.postal_code;
|
|
140
|
-
if (
|
|
141
|
-
profileInsert.drivers_license_number =
|
|
219
|
+
if (dl)
|
|
220
|
+
profileInsert.drivers_license_number = dl;
|
|
142
221
|
if (args.medical_card_number)
|
|
143
222
|
profileInsert.medical_card_number = args.medical_card_number;
|
|
144
223
|
if (args.medical_card_expiry)
|
|
145
224
|
profileInsert.medical_card_expiry = args.medical_card_expiry;
|
|
225
|
+
if (args.gender)
|
|
226
|
+
profileInsert.gender = args.gender;
|
|
227
|
+
if (args.eye_color)
|
|
228
|
+
profileInsert.eye_color = args.eye_color;
|
|
229
|
+
if (args.hair_color)
|
|
230
|
+
profileInsert.hair_color = args.hair_color;
|
|
231
|
+
if (args.height_raw)
|
|
232
|
+
profileInsert.height_raw = args.height_raw;
|
|
233
|
+
if (args.weight)
|
|
234
|
+
profileInsert.weight = args.weight;
|
|
235
|
+
if (args.suffix)
|
|
236
|
+
profileInsert.suffix = args.suffix;
|
|
237
|
+
if (args.is_staff !== undefined)
|
|
238
|
+
profileInsert.is_staff = args.is_staff;
|
|
146
239
|
await sb.from("store_customer_profiles").insert(profileInsert);
|
|
147
240
|
const { data: created } = await sb.from("v_store_customers")
|
|
148
241
|
.select("*").eq("id", rel.id).single();
|
|
149
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
data: stripInternal(created),
|
|
245
|
+
...(matchedVia ? { note: `Linked to existing customer record (matched via ${matchedVia})` } : {}),
|
|
246
|
+
...(isNameOnly ? { warning: "Created with no email or phone — add contact info to prevent duplicates." } : {}),
|
|
247
|
+
};
|
|
150
248
|
}
|
|
151
|
-
// ---- UPDATE: modify customer
|
|
249
|
+
// ---- UPDATE: modify any customer fields ----
|
|
152
250
|
case "update": {
|
|
153
251
|
const custId = args.customer_id;
|
|
154
252
|
const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
|
|
155
253
|
.select("id, user_id").eq("id", custId).eq("store_id", sid).single();
|
|
156
254
|
if (relErr)
|
|
157
255
|
return { success: false, error: `Customer not found: ${relErr.message}` };
|
|
158
|
-
//
|
|
159
|
-
const
|
|
256
|
+
// Customer identity (name, email, phone, DOB)
|
|
257
|
+
const identityUpdates = {};
|
|
160
258
|
if (args.first_name !== undefined)
|
|
161
|
-
|
|
259
|
+
identityUpdates.first_name = args.first_name;
|
|
162
260
|
if (args.last_name !== undefined)
|
|
163
|
-
|
|
261
|
+
identityUpdates.last_name = args.last_name;
|
|
164
262
|
if (args.email !== undefined)
|
|
165
|
-
|
|
263
|
+
identityUpdates.email = args.email;
|
|
166
264
|
if (args.phone !== undefined)
|
|
167
|
-
|
|
265
|
+
identityUpdates.phone = args.phone;
|
|
168
266
|
if (args.date_of_birth !== undefined)
|
|
169
|
-
|
|
170
|
-
if (Object.keys(
|
|
171
|
-
|
|
267
|
+
identityUpdates.date_of_birth = args.date_of_birth;
|
|
268
|
+
if (Object.keys(identityUpdates).length > 0) {
|
|
269
|
+
identityUpdates.updated_at = new Date().toISOString();
|
|
172
270
|
const { error: puErr } = await sb.from("platform_users")
|
|
173
|
-
.update(
|
|
271
|
+
.update(identityUpdates).eq("id", rel.user_id);
|
|
174
272
|
if (puErr)
|
|
175
|
-
return { success: false, error: `Failed to update
|
|
273
|
+
return { success: false, error: `Failed to update customer: ${puErr.message}` };
|
|
176
274
|
}
|
|
177
|
-
//
|
|
275
|
+
// Consent & status
|
|
178
276
|
const relUpdates = {};
|
|
179
277
|
if (args.status !== undefined)
|
|
180
278
|
relUpdates.status = args.status;
|
|
@@ -188,7 +286,7 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
188
286
|
relUpdates.updated_at = new Date().toISOString();
|
|
189
287
|
await sb.from("user_creation_relationships").update(relUpdates).eq("id", custId);
|
|
190
288
|
}
|
|
191
|
-
//
|
|
289
|
+
// Profile fields
|
|
192
290
|
const profUpdates = {};
|
|
193
291
|
if (args.loyalty_points !== undefined)
|
|
194
292
|
profUpdates.loyalty_points = args.loyalty_points;
|
|
@@ -202,24 +300,65 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
202
300
|
profUpdates.state = args.state;
|
|
203
301
|
if (args.postal_code !== undefined)
|
|
204
302
|
profUpdates.postal_code = args.postal_code;
|
|
303
|
+
if (args.billing_address !== undefined)
|
|
304
|
+
profUpdates.billing_address = args.billing_address;
|
|
305
|
+
if (args.shipping_addresses !== undefined)
|
|
306
|
+
profUpdates.shipping_addresses = args.shipping_addresses;
|
|
307
|
+
if (args.default_shipping_address_index !== undefined)
|
|
308
|
+
profUpdates.default_shipping_address_index = args.default_shipping_address_index;
|
|
205
309
|
if (args.drivers_license_number !== undefined)
|
|
206
310
|
profUpdates.drivers_license_number = args.drivers_license_number;
|
|
207
|
-
if (args.id_verified !== undefined)
|
|
311
|
+
if (args.id_verified !== undefined) {
|
|
208
312
|
profUpdates.id_verified = args.id_verified;
|
|
313
|
+
if (args.id_verified === true)
|
|
314
|
+
profUpdates.id_verified_at = new Date().toISOString();
|
|
315
|
+
}
|
|
316
|
+
if (args.license_expiration_date !== undefined)
|
|
317
|
+
profUpdates.license_expiration_date = args.license_expiration_date;
|
|
318
|
+
if (args.license_issue_date !== undefined)
|
|
319
|
+
profUpdates.license_issue_date = args.license_issue_date;
|
|
209
320
|
if (args.medical_card_number !== undefined)
|
|
210
321
|
profUpdates.medical_card_number = args.medical_card_number;
|
|
211
322
|
if (args.medical_card_expiry !== undefined)
|
|
212
323
|
profUpdates.medical_card_expiry = args.medical_card_expiry;
|
|
324
|
+
if (args.gender !== undefined)
|
|
325
|
+
profUpdates.gender = args.gender;
|
|
326
|
+
if (args.eye_color !== undefined)
|
|
327
|
+
profUpdates.eye_color = args.eye_color;
|
|
328
|
+
if (args.height_raw !== undefined)
|
|
329
|
+
profUpdates.height_raw = args.height_raw;
|
|
330
|
+
if (args.weight !== undefined)
|
|
331
|
+
profUpdates.weight = args.weight;
|
|
332
|
+
if (args.hair_color !== undefined)
|
|
333
|
+
profUpdates.hair_color = args.hair_color;
|
|
334
|
+
if (args.suffix !== undefined)
|
|
335
|
+
profUpdates.suffix = args.suffix;
|
|
336
|
+
if (args.is_staff !== undefined)
|
|
337
|
+
profUpdates.is_staff = args.is_staff;
|
|
338
|
+
if (args.has_wallet_pass !== undefined)
|
|
339
|
+
profUpdates.has_wallet_pass = args.has_wallet_pass;
|
|
213
340
|
if (args.is_wholesale_approved !== undefined)
|
|
214
341
|
profUpdates.is_wholesale_approved = args.is_wholesale_approved;
|
|
215
342
|
if (args.wholesale_tier !== undefined)
|
|
216
343
|
profUpdates.wholesale_tier = args.wholesale_tier;
|
|
217
344
|
if (args.wholesale_business_name !== undefined)
|
|
218
345
|
profUpdates.wholesale_business_name = args.wholesale_business_name;
|
|
346
|
+
if (args.wholesale_business_type !== undefined)
|
|
347
|
+
profUpdates.wholesale_business_type = args.wholesale_business_type;
|
|
219
348
|
if (args.wholesale_license_number !== undefined)
|
|
220
349
|
profUpdates.wholesale_license_number = args.wholesale_license_number;
|
|
221
350
|
if (args.wholesale_tax_id !== undefined)
|
|
222
351
|
profUpdates.wholesale_tax_id = args.wholesale_tax_id;
|
|
352
|
+
if (args.wholesale_discount_percent !== undefined)
|
|
353
|
+
profUpdates.wholesale_discount_percent = args.wholesale_discount_percent;
|
|
354
|
+
if (args.wholesale_payment_terms !== undefined)
|
|
355
|
+
profUpdates.wholesale_payment_terms = args.wholesale_payment_terms;
|
|
356
|
+
if (args.wholesale_credit_limit !== undefined)
|
|
357
|
+
profUpdates.wholesale_credit_limit = args.wholesale_credit_limit;
|
|
358
|
+
if (args.wholesale_resale_certificate !== undefined)
|
|
359
|
+
profUpdates.wholesale_resale_certificate = args.wholesale_resale_certificate;
|
|
360
|
+
if (args.wholesale_notes !== undefined)
|
|
361
|
+
profUpdates.wholesale_notes = args.wholesale_notes;
|
|
223
362
|
if (Object.keys(profUpdates).length > 0) {
|
|
224
363
|
profUpdates.updated_at = new Date().toISOString();
|
|
225
364
|
const { error: profErr } = await sb.from("store_customer_profiles")
|
|
@@ -227,9 +366,9 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
227
366
|
if (profErr)
|
|
228
367
|
return { success: false, error: `Failed to update profile: ${profErr.message}` };
|
|
229
368
|
}
|
|
230
|
-
const { data: updated } = await sb.from("
|
|
369
|
+
const { data: updated } = await sb.from("v_segment_customers")
|
|
231
370
|
.select("*").eq("id", custId).eq("store_id", sid).single();
|
|
232
|
-
return { success: true, data: updated };
|
|
371
|
+
return { success: true, data: stripInternal(updated) };
|
|
233
372
|
}
|
|
234
373
|
// ---- FIND_DUPLICATES: identify potential duplicate customer accounts ----
|
|
235
374
|
case "find_duplicates": {
|
|
@@ -389,7 +528,7 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
389
528
|
}
|
|
390
529
|
}
|
|
391
530
|
const { data: merged } = await sb.from("v_store_customers").select("*").eq("id", primaryId).single();
|
|
392
|
-
return { success: true, data: { merged_customer: merged, reassign_results: reassignResults } };
|
|
531
|
+
return { success: true, data: { merged_customer: stripInternal(merged), reassign_results: reassignResults } };
|
|
393
532
|
}
|
|
394
533
|
// ---- ADD_NOTE ----
|
|
395
534
|
case "add_note": {
|
|
@@ -469,8 +608,154 @@ export async function handleCustomers(sb, args, storeId) {
|
|
|
469
608
|
.order("last_seen_at", { ascending: false });
|
|
470
609
|
return error ? { success: false, error: error.message } : { success: true, data };
|
|
471
610
|
}
|
|
611
|
+
// ---- INVITE: create auth account for customer and send branded invite email ----
|
|
612
|
+
case "invite": {
|
|
613
|
+
const custId = args.customer_id;
|
|
614
|
+
if (!custId)
|
|
615
|
+
return { success: false, error: "customer_id is required" };
|
|
616
|
+
// Verify customer belongs to this store
|
|
617
|
+
const { data: rel, error: relErr } = await sb.from("user_creation_relationships")
|
|
618
|
+
.select("id, user_id, store_id")
|
|
619
|
+
.eq("id", custId).eq("store_id", sid).single();
|
|
620
|
+
if (relErr || !rel)
|
|
621
|
+
return { success: false, error: "Customer not found for this store" };
|
|
622
|
+
// Get platform user + store name in parallel
|
|
623
|
+
const [{ data: pu }, { data: store }] = await Promise.all([
|
|
624
|
+
sb.from("platform_users").select("id, auth_id, email, first_name, last_name").eq("id", rel.user_id).single(),
|
|
625
|
+
sb.from("stores").select("store_name").eq("id", sid).single(),
|
|
626
|
+
]);
|
|
627
|
+
if (!pu)
|
|
628
|
+
return { success: false, error: "Platform user not found" };
|
|
629
|
+
if (!pu.email)
|
|
630
|
+
return { success: false, error: "Customer has no email address — add one first" };
|
|
631
|
+
const storeName = store?.store_name || "our platform";
|
|
632
|
+
const customerName = [pu.first_name, pu.last_name].filter(Boolean).join(" ") || "there";
|
|
633
|
+
const portalUrl = args.portal_url || args.redirect_to || null;
|
|
634
|
+
// If already has an auth account, just resend the email with a new magic link
|
|
635
|
+
let authId = pu.auth_id;
|
|
636
|
+
if (!authId) {
|
|
637
|
+
// Create auth account silently (no Supabase built-in email)
|
|
638
|
+
const { data: newUser, error: createErr } = await sb.auth.admin.createUser({
|
|
639
|
+
email: pu.email,
|
|
640
|
+
email_confirm: true, // skip verification — store is vouching for this email
|
|
641
|
+
user_metadata: {
|
|
642
|
+
first_name: pu.first_name,
|
|
643
|
+
last_name: pu.last_name,
|
|
644
|
+
invited_by_store: sid,
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
if (createErr)
|
|
648
|
+
return { success: false, error: `Account creation failed: ${createErr.message}` };
|
|
649
|
+
authId = newUser.user.id;
|
|
650
|
+
// Link auth account to platform user
|
|
651
|
+
await sb.from("platform_users").update({ auth_id: authId }).eq("id", pu.id);
|
|
652
|
+
}
|
|
653
|
+
// Generate a magic link for the customer to set up / log in
|
|
654
|
+
const { data: linkData, error: linkErr } = await sb.auth.admin.generateLink({
|
|
655
|
+
type: "magiclink",
|
|
656
|
+
email: pu.email,
|
|
657
|
+
options: {
|
|
658
|
+
...(portalUrl ? { redirectTo: portalUrl } : {}),
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
if (linkErr)
|
|
662
|
+
return { success: false, error: `Link generation failed: ${linkErr.message}` };
|
|
663
|
+
const magicLink = linkData?.properties?.action_link || "";
|
|
664
|
+
// Send branded invite email through our email system
|
|
665
|
+
const emailResult = await handleEmail(sb, {
|
|
666
|
+
action: "send_template",
|
|
667
|
+
to: pu.email,
|
|
668
|
+
template: "customer-portal-invite",
|
|
669
|
+
template_data: {
|
|
670
|
+
customer_name: customerName,
|
|
671
|
+
store_name: storeName,
|
|
672
|
+
magic_link: magicLink,
|
|
673
|
+
portal_url: portalUrl || magicLink,
|
|
674
|
+
},
|
|
675
|
+
}, sid);
|
|
676
|
+
return {
|
|
677
|
+
success: true,
|
|
678
|
+
data: {
|
|
679
|
+
email: pu.email,
|
|
680
|
+
name: customerName,
|
|
681
|
+
invited: true,
|
|
682
|
+
email_sent: emailResult.success,
|
|
683
|
+
...(emailResult.success ? {} : { email_error: emailResult.error }),
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
// ---- LOYALTY_HISTORY: transaction history for a customer ----
|
|
688
|
+
case "loyalty_history": {
|
|
689
|
+
const custId = args.customer_id;
|
|
690
|
+
if (!custId)
|
|
691
|
+
return { success: false, error: "customer_id is required" };
|
|
692
|
+
let q = sb.from("loyalty_transactions")
|
|
693
|
+
.select("id, points, transaction_type, reference_type, reference_id, description, balance_before, balance_after, expires_at, created_at")
|
|
694
|
+
.eq("customer_id", custId)
|
|
695
|
+
.order("created_at", { ascending: false });
|
|
696
|
+
if (args.transaction_type)
|
|
697
|
+
q = q.eq("transaction_type", args.transaction_type);
|
|
698
|
+
q = q.limit(args.limit || 50);
|
|
699
|
+
const { data, error } = await q;
|
|
700
|
+
return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
|
|
701
|
+
}
|
|
702
|
+
// ---- ADJUST_LOYALTY: award or deduct loyalty points via RPC ----
|
|
703
|
+
case "adjust_loyalty": {
|
|
704
|
+
const custId = args.customer_id;
|
|
705
|
+
const points = args.points;
|
|
706
|
+
const reason = args.reason;
|
|
707
|
+
if (!custId || points === undefined || !reason)
|
|
708
|
+
return { success: false, error: "customer_id, points, and reason are required" };
|
|
709
|
+
const { data, error } = await sb.rpc("adjust_customer_loyalty_points", {
|
|
710
|
+
p_customer_id: custId,
|
|
711
|
+
p_points_change: points,
|
|
712
|
+
p_reason: reason,
|
|
713
|
+
});
|
|
714
|
+
if (error)
|
|
715
|
+
return { success: false, error: error.message };
|
|
716
|
+
const { data: updated } = await sb.from("store_customer_profiles")
|
|
717
|
+
.select("loyalty_points, loyalty_tier, lifetime_points_earned")
|
|
718
|
+
.eq("relationship_id", custId).maybeSingle();
|
|
719
|
+
return { success: true, data: { rpc_result: data, ...updated } };
|
|
720
|
+
}
|
|
721
|
+
// ---- SEGMENTS: list all customer segments for the store ----
|
|
722
|
+
case "segments": {
|
|
723
|
+
let q = sb.from("customer_segments")
|
|
724
|
+
.select("id, name, ai_description, type, segment_rules, filter_criteria, customer_count, is_active, color, icon, targeting_tips, created_at")
|
|
725
|
+
.eq("store_id", sid)
|
|
726
|
+
.order("customer_count", { ascending: false });
|
|
727
|
+
if (args.limit)
|
|
728
|
+
q = q.limit(args.limit);
|
|
729
|
+
const { data, error } = await q;
|
|
730
|
+
return error ? { success: false, error: error.message } : { success: true, count: data?.length, data };
|
|
731
|
+
}
|
|
732
|
+
// ---- SEGMENT_MEMBERS: list customers in a specific segment ----
|
|
733
|
+
case "segment_members": {
|
|
734
|
+
const segId = args.segment_id;
|
|
735
|
+
if (!segId)
|
|
736
|
+
return { success: false, error: "segment_id is required" };
|
|
737
|
+
const { data, error } = await sb.from("customer_segment_memberships")
|
|
738
|
+
.select("added_at, customer:v_segment_customers!customer_id(*)")
|
|
739
|
+
.eq("segment_id", segId)
|
|
740
|
+
.order("added_at", { ascending: false })
|
|
741
|
+
.limit(args.limit || 50);
|
|
742
|
+
if (error)
|
|
743
|
+
return { success: false, error: error.message };
|
|
744
|
+
const members = (data || []).map((r) => ({ added_at: r.added_at, ...r.customer }))
|
|
745
|
+
.filter((m) => m.store_id === sid);
|
|
746
|
+
return { success: true, count: members.length, data: stripInternalArray(members) };
|
|
747
|
+
}
|
|
748
|
+
// ---- REFRESH_METRICS: recalculate all customer metrics for the store ----
|
|
749
|
+
case "refresh_metrics": {
|
|
750
|
+
const start = Date.now();
|
|
751
|
+
const { data, error } = await sb.rpc("refresh_customer_metrics", { p_store_id: sid });
|
|
752
|
+
const elapsed = Date.now() - start;
|
|
753
|
+
if (error)
|
|
754
|
+
return { success: false, error: error.message };
|
|
755
|
+
return { success: true, data: { result: data, elapsed_ms: elapsed, message: `Customer metrics refreshed in ${elapsed}ms` } };
|
|
756
|
+
}
|
|
472
757
|
default:
|
|
473
|
-
return { success: false, error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, find_duplicates, merge, add_note, notes, activity, orders, link_channel_identity, get_channel_identities` };
|
|
758
|
+
return { success: false, error: `Unknown customers action: ${args.action}. Valid: find, get, create, update, invite, find_duplicates, merge, add_note, notes, activity, orders, loyalty_history, adjust_loyalty, segments, segment_members, refresh_metrics, link_channel_identity, get_channel_identities` };
|
|
474
759
|
}
|
|
475
760
|
}
|
|
476
761
|
export async function handleOrders(sb, args, storeId) {
|
|
@@ -45,8 +45,8 @@ function productToText(product) {
|
|
|
45
45
|
parts.push(String(product.short_description));
|
|
46
46
|
if (product.description)
|
|
47
47
|
parts.push(String(product.description));
|
|
48
|
-
if (product.
|
|
49
|
-
const fv = product.
|
|
48
|
+
if (product.custom_fields && typeof product.custom_fields === "object") {
|
|
49
|
+
const fv = product.custom_fields;
|
|
50
50
|
for (const [key, val] of Object.entries(fv)) {
|
|
51
51
|
if (val !== null && val !== undefined && val !== "") {
|
|
52
52
|
parts.push(`${key}: ${String(val)}`);
|
|
@@ -117,7 +117,7 @@ export async function handleEmbeddings(sb, args, storeId) {
|
|
|
117
117
|
case "index_products": {
|
|
118
118
|
const { data: products, error: fetchErr } = await sb
|
|
119
119
|
.from("products")
|
|
120
|
-
.select("id, name, description, short_description,
|
|
120
|
+
.select("id, name, description, short_description, custom_fields")
|
|
121
121
|
.eq("store_id", sid);
|
|
122
122
|
if (fetchErr)
|
|
123
123
|
return { success: false, error: fetchErr.message };
|