whale-code 6.4.0 → 6.5.1
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 +51 -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 +65 -8
- 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 +7 -6
- 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 +85 -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 +46 -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 +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- 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 +25 -2
- package/dist/shared/agent-core.js +66 -5
- 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 +15 -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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// server/handlers/remove-bg.ts — Remove.bg background removal
|
|
2
|
+
// Removes background from images, uploads result to Supabase storage + store_media
|
|
3
|
+
async function getRemoveBgKey(sb, storeId) {
|
|
4
|
+
try {
|
|
5
|
+
const { data } = await sb.rpc("decrypt_secret", { p_name: "REMOVE_BG_API_KEY", p_store_id: storeId });
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function uploadResult(sb, storeId, buffer, sourceUrl) {
|
|
13
|
+
const id = crypto.randomUUID();
|
|
14
|
+
const fileName = `${id}.png`;
|
|
15
|
+
const storagePath = `remove-bg/${storeId.toUpperCase()}/${fileName}`;
|
|
16
|
+
const { error: uploadErr } = await sb.storage
|
|
17
|
+
.from("product-images")
|
|
18
|
+
.upload(storagePath, buffer, { contentType: "image/png", upsert: true });
|
|
19
|
+
if (uploadErr)
|
|
20
|
+
throw new Error(`Storage upload failed: ${uploadErr.message}`);
|
|
21
|
+
const { data: urlData } = sb.storage.from("product-images").getPublicUrl(storagePath);
|
|
22
|
+
const fileUrl = urlData.publicUrl;
|
|
23
|
+
const { data: mediaRow, error: mediaErr } = await sb
|
|
24
|
+
.from("store_media")
|
|
25
|
+
.insert({
|
|
26
|
+
store_id: storeId,
|
|
27
|
+
file_name: fileName,
|
|
28
|
+
file_path: storagePath,
|
|
29
|
+
file_url: fileUrl,
|
|
30
|
+
file_size: buffer.length,
|
|
31
|
+
file_type: "image/png",
|
|
32
|
+
category: "ai_generated",
|
|
33
|
+
ai_tags: ["background-removed", "remove.bg"],
|
|
34
|
+
ai_description: `Background removed from: ${sourceUrl.substring(0, 200)}`,
|
|
35
|
+
source: "remove-bg",
|
|
36
|
+
folder: "remove-bg",
|
|
37
|
+
})
|
|
38
|
+
.select("id")
|
|
39
|
+
.single();
|
|
40
|
+
if (mediaErr)
|
|
41
|
+
console.error("[remove-bg] store_media insert error:", mediaErr.message);
|
|
42
|
+
return { file_url: fileUrl, media_id: mediaRow?.id || id, file_name: fileName };
|
|
43
|
+
}
|
|
44
|
+
export async function handleRemoveBg(sb, args, storeId) {
|
|
45
|
+
if (!storeId)
|
|
46
|
+
return { success: false, error: "store_id is required" };
|
|
47
|
+
const apiKey = await getRemoveBgKey(sb, storeId);
|
|
48
|
+
if (!apiKey)
|
|
49
|
+
return { success: false, error: "Remove.bg API key not configured. Add REMOVE_BG_API_KEY to platform_secrets." };
|
|
50
|
+
const imageUrl = args.image_url;
|
|
51
|
+
const imageBase64 = args.image_base64;
|
|
52
|
+
const size = args.size || "auto";
|
|
53
|
+
const type = args.type || "auto";
|
|
54
|
+
if (!imageUrl && !imageBase64) {
|
|
55
|
+
return { success: false, error: "Provide image_url (public URL), image_base64, or image_path (local file — handled by CLI)" };
|
|
56
|
+
}
|
|
57
|
+
// Reject local file paths passed as image_url
|
|
58
|
+
if (imageUrl && (imageUrl.startsWith("/") || /^[A-Z]:\\/i.test(imageUrl))) {
|
|
59
|
+
return { success: false, error: `Local paths cannot be used as image_url. Use image_path instead: remove_bg(image_path="${imageUrl}")` };
|
|
60
|
+
}
|
|
61
|
+
// Build form data for remove.bg API
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
if (imageUrl) {
|
|
64
|
+
form.append("image_url", imageUrl);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Strip data URI prefix if present
|
|
68
|
+
const b64 = imageBase64.replace(/^data:image\/[a-z]+;base64,/, "");
|
|
69
|
+
const buf = Buffer.from(b64, "base64");
|
|
70
|
+
form.append("image_file", new Blob([buf]), "image.png");
|
|
71
|
+
}
|
|
72
|
+
form.append("size", size);
|
|
73
|
+
form.append("type", type);
|
|
74
|
+
const resp = await fetch("https://api.remove.bg/v1.0/removebg", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "X-Api-Key": apiKey },
|
|
77
|
+
body: form,
|
|
78
|
+
});
|
|
79
|
+
if (!resp.ok) {
|
|
80
|
+
const errText = await resp.text().catch(() => resp.statusText);
|
|
81
|
+
return { success: false, error: `remove.bg API error ${resp.status}: ${errText}` };
|
|
82
|
+
}
|
|
83
|
+
const resultBuffer = Buffer.from(await resp.arrayBuffer());
|
|
84
|
+
const sourceRef = imageUrl || "base64-input";
|
|
85
|
+
const { file_url, media_id, file_name } = await uploadResult(sb, storeId, resultBuffer, sourceRef);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
data: {
|
|
89
|
+
file_url,
|
|
90
|
+
media_id,
|
|
91
|
+
file_name,
|
|
92
|
+
file_size: resultBuffer.length,
|
|
93
|
+
source: sourceRef,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// handlers/storefront.ts — Consumer-facing commerce tool for AI shopping agents.
|
|
2
|
+
// Read-heavy, limited writes (cart + checkout). Only exposes published/active products.
|
|
3
|
+
import { sanitizeFilterValue } from "../lib/utils.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// HELPERS
|
|
6
|
+
// ============================================================================
|
|
7
|
+
/** Resolve the primary display price from pricing_data JSON.
|
|
8
|
+
* pricing_data is a Record<tierName, { default_price?, price?, ... }>.
|
|
9
|
+
* Returns the first tier's default_price or price, or 0. */
|
|
10
|
+
function resolvePrimaryPrice(pricingData) {
|
|
11
|
+
if (!pricingData || typeof pricingData !== "object")
|
|
12
|
+
return 0;
|
|
13
|
+
const tiers = Object.values(pricingData);
|
|
14
|
+
if (tiers.length === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
return tiers[0].default_price ?? tiers[0].price ?? 0;
|
|
17
|
+
}
|
|
18
|
+
/** Format pricing tiers for consumer display (name + price, no cost data). */
|
|
19
|
+
function formatPricingTiers(pricingData) {
|
|
20
|
+
if (!pricingData || typeof pricingData !== "object")
|
|
21
|
+
return null;
|
|
22
|
+
const entries = Object.entries(pricingData);
|
|
23
|
+
if (entries.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
return entries.map(([tier, data]) => ({
|
|
26
|
+
tier,
|
|
27
|
+
price: data.default_price ?? data.price ?? 0,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
/** Strip internal/admin fields from a product row for public display. */
|
|
31
|
+
function filterPublicFields(product) {
|
|
32
|
+
const { cost_price, wholesale_price, is_wholesale, wholesale_only, minimum_wholesale_quantity, manage_stock, tax_status, tax_class, catalog_id, pricing_schema_id, ...publicFields } = product;
|
|
33
|
+
return publicFields;
|
|
34
|
+
}
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// MAIN HANDLER
|
|
37
|
+
// ============================================================================
|
|
38
|
+
export async function handleStorefront(sb, args, storeId) {
|
|
39
|
+
const sid = storeId;
|
|
40
|
+
switch (args.action) {
|
|
41
|
+
// ======================== BROWSE ========================
|
|
42
|
+
case "browse": {
|
|
43
|
+
const page = args.page || 1;
|
|
44
|
+
const perPage = Math.min(args.per_page || 20, 50);
|
|
45
|
+
const offset = (page - 1) * perPage;
|
|
46
|
+
let q = sb.from("products")
|
|
47
|
+
.select("id, name, sku, slug, short_description, featured_image, pricing_data, status, primary_category_id, featured", { count: "exact" })
|
|
48
|
+
.eq("store_id", sid)
|
|
49
|
+
.in("status", ["published", "active"])
|
|
50
|
+
.order("created_at", { ascending: false })
|
|
51
|
+
.range(offset, offset + perPage - 1);
|
|
52
|
+
if (args.category_id) {
|
|
53
|
+
const catId = args.category_id;
|
|
54
|
+
// Include sub-categories
|
|
55
|
+
const { data: children } = await sb.from("categories").select("id").eq("parent_id", catId).eq("store_id", sid);
|
|
56
|
+
const catIds = [catId, ...(children || []).map((c) => c.id)];
|
|
57
|
+
q = q.in("primary_category_id", catIds);
|
|
58
|
+
}
|
|
59
|
+
if (args.featured === true)
|
|
60
|
+
q = q.eq("featured", true);
|
|
61
|
+
const sortBy = args.sort;
|
|
62
|
+
if (sortBy === "price_asc" || sortBy === "price_desc") {
|
|
63
|
+
// price sort not directly supported on JSON column — sort in JS below
|
|
64
|
+
}
|
|
65
|
+
else if (sortBy === "name") {
|
|
66
|
+
q = q.order("name", { ascending: true });
|
|
67
|
+
}
|
|
68
|
+
const { data, error, count } = await q;
|
|
69
|
+
if (error)
|
|
70
|
+
return { success: false, error: error.message };
|
|
71
|
+
let products = (data || []).map((p) => ({
|
|
72
|
+
...filterPublicFields(p),
|
|
73
|
+
price: resolvePrimaryPrice(p.pricing_data),
|
|
74
|
+
pricing_tiers: formatPricingTiers(p.pricing_data),
|
|
75
|
+
}));
|
|
76
|
+
// Client-side price sort when requested
|
|
77
|
+
if (sortBy === "price_asc")
|
|
78
|
+
products.sort((a, b) => a.price - b.price);
|
|
79
|
+
if (sortBy === "price_desc")
|
|
80
|
+
products.sort((a, b) => b.price - a.price);
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
data: {
|
|
84
|
+
products,
|
|
85
|
+
page,
|
|
86
|
+
per_page: perPage,
|
|
87
|
+
total: count || 0,
|
|
88
|
+
total_pages: Math.ceil((count || 0) / perPage),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ======================== PRODUCT DETAIL ========================
|
|
93
|
+
case "product_detail": {
|
|
94
|
+
const pid = args.product_id;
|
|
95
|
+
if (!pid)
|
|
96
|
+
return { success: false, error: "product_id is required" };
|
|
97
|
+
const { data: product, error: pErr } = await sb.from("products")
|
|
98
|
+
.select("*, category:categories!primary_category_id(id, name, slug)")
|
|
99
|
+
.eq("id", pid)
|
|
100
|
+
.eq("store_id", sid)
|
|
101
|
+
.in("status", ["published", "active"])
|
|
102
|
+
.single();
|
|
103
|
+
if (pErr || !product)
|
|
104
|
+
return { success: false, error: "Product not found or not available" };
|
|
105
|
+
// Inventory availability per location
|
|
106
|
+
const { data: inventory } = await sb.from("inventory")
|
|
107
|
+
.select("quantity, location:locations!location_id(id, name)")
|
|
108
|
+
.eq("product_id", pid)
|
|
109
|
+
.eq("store_id", sid)
|
|
110
|
+
.gt("quantity", 0);
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
data: {
|
|
114
|
+
...filterPublicFields(product),
|
|
115
|
+
price: resolvePrimaryPrice(product.pricing_data),
|
|
116
|
+
pricing_tiers: formatPricingTiers(product.pricing_data),
|
|
117
|
+
availability: (inventory || []).map((inv) => ({
|
|
118
|
+
location_id: inv.location?.id,
|
|
119
|
+
location_name: inv.location?.name,
|
|
120
|
+
in_stock: (inv.quantity ?? 0) > 0,
|
|
121
|
+
quantity: inv.quantity,
|
|
122
|
+
})),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ======================== SEARCH ========================
|
|
127
|
+
case "search": {
|
|
128
|
+
const query = args.query;
|
|
129
|
+
if (!query)
|
|
130
|
+
return { success: false, error: "query is required" };
|
|
131
|
+
const sq = sanitizeFilterValue(query);
|
|
132
|
+
const limit = Math.min(args.limit || 20, 50);
|
|
133
|
+
const { data, error } = await sb.from("products")
|
|
134
|
+
.select("id, name, sku, slug, short_description, featured_image, pricing_data, status, primary_category_id")
|
|
135
|
+
.eq("store_id", sid)
|
|
136
|
+
.in("status", ["published", "active"])
|
|
137
|
+
.or(`name.ilike.%${sq}%,description.ilike.%${sq}%,sku.ilike.%${sq}%`)
|
|
138
|
+
.order("name")
|
|
139
|
+
.limit(limit);
|
|
140
|
+
if (error)
|
|
141
|
+
return { success: false, error: error.message };
|
|
142
|
+
const products = (data || []).map((p) => ({
|
|
143
|
+
...filterPublicFields(p),
|
|
144
|
+
price: resolvePrimaryPrice(p.pricing_data),
|
|
145
|
+
}));
|
|
146
|
+
return { success: true, data: { products, count: products.length, query } };
|
|
147
|
+
}
|
|
148
|
+
// ======================== CATEGORIES ========================
|
|
149
|
+
case "categories": {
|
|
150
|
+
const { data, error } = await sb.from("categories")
|
|
151
|
+
.select("id, name, slug, description, icon, parent_id, display_order, product_count")
|
|
152
|
+
.eq("store_id", sid)
|
|
153
|
+
.eq("is_active", true)
|
|
154
|
+
.order("display_order", { ascending: true });
|
|
155
|
+
if (error)
|
|
156
|
+
return { success: false, error: error.message };
|
|
157
|
+
return { success: true, data: { categories: data || [] } };
|
|
158
|
+
}
|
|
159
|
+
// ======================== CART CREATE ========================
|
|
160
|
+
case "cart_create": {
|
|
161
|
+
// Resolve default location if not provided
|
|
162
|
+
let locationId = args.location_id;
|
|
163
|
+
if (!locationId) {
|
|
164
|
+
const { data: loc } = await sb.from("locations")
|
|
165
|
+
.select("id")
|
|
166
|
+
.eq("store_id", sid)
|
|
167
|
+
.eq("is_active", true)
|
|
168
|
+
.limit(1)
|
|
169
|
+
.single();
|
|
170
|
+
if (loc)
|
|
171
|
+
locationId = loc.id;
|
|
172
|
+
}
|
|
173
|
+
const expiresAt = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString();
|
|
174
|
+
const record = {
|
|
175
|
+
store_id: sid,
|
|
176
|
+
status: "active",
|
|
177
|
+
expires_at: expiresAt,
|
|
178
|
+
};
|
|
179
|
+
if (locationId)
|
|
180
|
+
record.location_id = locationId;
|
|
181
|
+
if (args.customer_id)
|
|
182
|
+
record.customer_id = args.customer_id;
|
|
183
|
+
if (args.customer_email)
|
|
184
|
+
record.customer_email = args.customer_email;
|
|
185
|
+
const { data, error } = await sb.from("carts").insert(record).select().single();
|
|
186
|
+
if (error)
|
|
187
|
+
return { success: false, error: error.message };
|
|
188
|
+
return { success: true, data: { cart: data, items: [] } };
|
|
189
|
+
}
|
|
190
|
+
// ======================== CART ADD ========================
|
|
191
|
+
case "cart_add": {
|
|
192
|
+
const cartId = args.cart_id;
|
|
193
|
+
const productId = args.product_id;
|
|
194
|
+
const quantity = args.quantity || 1;
|
|
195
|
+
if (!cartId)
|
|
196
|
+
return { success: false, error: "cart_id is required" };
|
|
197
|
+
if (!productId)
|
|
198
|
+
return { success: false, error: "product_id is required" };
|
|
199
|
+
if (quantity < 1)
|
|
200
|
+
return { success: false, error: "quantity must be at least 1" };
|
|
201
|
+
// Validate cart is active
|
|
202
|
+
const { data: cart, error: cartErr } = await sb.from("carts")
|
|
203
|
+
.select("id, status")
|
|
204
|
+
.eq("id", cartId)
|
|
205
|
+
.eq("store_id", sid)
|
|
206
|
+
.single();
|
|
207
|
+
if (cartErr || !cart)
|
|
208
|
+
return { success: false, error: "Cart not found" };
|
|
209
|
+
if (cart.status !== "active")
|
|
210
|
+
return { success: false, error: "Cart is not active" };
|
|
211
|
+
// Validate product is published
|
|
212
|
+
const { data: product, error: prodErr } = await sb.from("products")
|
|
213
|
+
.select("id, name, sku, status, pricing_data")
|
|
214
|
+
.eq("id", productId)
|
|
215
|
+
.eq("store_id", sid)
|
|
216
|
+
.single();
|
|
217
|
+
if (prodErr || !product)
|
|
218
|
+
return { success: false, error: "Product not found" };
|
|
219
|
+
if (product.status !== "published" && product.status !== "active") {
|
|
220
|
+
return { success: false, error: "Product is not available for purchase" };
|
|
221
|
+
}
|
|
222
|
+
// Resolve price from pricing_data
|
|
223
|
+
let unitPrice = args.unit_price;
|
|
224
|
+
if (unitPrice === undefined) {
|
|
225
|
+
if (product.pricing_data) {
|
|
226
|
+
const pd = product.pricing_data;
|
|
227
|
+
if (args.tier && pd[args.tier]) {
|
|
228
|
+
unitPrice = pd[args.tier].default_price ?? pd[args.tier].price ?? 0;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
unitPrice = resolvePrimaryPrice(pd);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
unitPrice = 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const itemRecord = {
|
|
239
|
+
cart_id: cartId,
|
|
240
|
+
product_id: productId,
|
|
241
|
+
product_name: product.name,
|
|
242
|
+
sku: product.sku,
|
|
243
|
+
quantity,
|
|
244
|
+
unit_price: unitPrice,
|
|
245
|
+
};
|
|
246
|
+
if (args.tier)
|
|
247
|
+
itemRecord.tier_label = args.tier;
|
|
248
|
+
const { data: item, error: insertErr } = await sb.from("cart_items")
|
|
249
|
+
.insert(itemRecord).select().single();
|
|
250
|
+
if (insertErr)
|
|
251
|
+
return { success: false, error: insertErr.message };
|
|
252
|
+
return { success: true, data: { item, line_total: Math.round(quantity * (unitPrice || 0) * 100) / 100 } };
|
|
253
|
+
}
|
|
254
|
+
// ======================== CART VIEW ========================
|
|
255
|
+
case "cart_view": {
|
|
256
|
+
const cartId = args.cart_id;
|
|
257
|
+
if (!cartId)
|
|
258
|
+
return { success: false, error: "cart_id is required" };
|
|
259
|
+
const { data: cart, error: cartErr } = await sb.from("carts")
|
|
260
|
+
.select("*")
|
|
261
|
+
.eq("id", cartId)
|
|
262
|
+
.eq("store_id", sid)
|
|
263
|
+
.single();
|
|
264
|
+
if (cartErr || !cart)
|
|
265
|
+
return { success: false, error: "Cart not found" };
|
|
266
|
+
const { data: items } = await sb.from("cart_items")
|
|
267
|
+
.select("*")
|
|
268
|
+
.eq("cart_id", cartId)
|
|
269
|
+
.order("created_at", { ascending: true });
|
|
270
|
+
const cartItems = items || [];
|
|
271
|
+
const subtotal = cartItems.reduce((sum, i) => sum + (i.quantity ?? 0) * (i.unit_price ?? 0), 0);
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
data: {
|
|
275
|
+
cart: { id: cart.id, status: cart.status, expires_at: cart.expires_at },
|
|
276
|
+
items: cartItems.map((i) => ({
|
|
277
|
+
id: i.id,
|
|
278
|
+
product_id: i.product_id,
|
|
279
|
+
product_name: i.product_name,
|
|
280
|
+
sku: i.sku,
|
|
281
|
+
quantity: i.quantity,
|
|
282
|
+
unit_price: i.unit_price,
|
|
283
|
+
line_total: Math.round((i.quantity ?? 0) * (i.unit_price ?? 0) * 100) / 100,
|
|
284
|
+
tier_label: i.tier_label,
|
|
285
|
+
})),
|
|
286
|
+
subtotal: Math.round(subtotal * 100) / 100,
|
|
287
|
+
item_count: cartItems.length,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ======================== CART UPDATE ========================
|
|
292
|
+
case "cart_update": {
|
|
293
|
+
const cartId = args.cart_id;
|
|
294
|
+
const itemId = args.item_id;
|
|
295
|
+
const quantity = args.quantity;
|
|
296
|
+
if (!cartId)
|
|
297
|
+
return { success: false, error: "cart_id is required" };
|
|
298
|
+
if (!itemId)
|
|
299
|
+
return { success: false, error: "item_id is required" };
|
|
300
|
+
if (!quantity || quantity < 1)
|
|
301
|
+
return { success: false, error: "quantity must be a positive integer" };
|
|
302
|
+
// Validate cart is active
|
|
303
|
+
const { data: cart } = await sb.from("carts")
|
|
304
|
+
.select("id, status")
|
|
305
|
+
.eq("id", cartId)
|
|
306
|
+
.eq("store_id", sid)
|
|
307
|
+
.single();
|
|
308
|
+
if (!cart)
|
|
309
|
+
return { success: false, error: "Cart not found" };
|
|
310
|
+
if (cart.status !== "active")
|
|
311
|
+
return { success: false, error: "Cart is not active" };
|
|
312
|
+
const { data, error } = await sb.from("cart_items")
|
|
313
|
+
.update({ quantity, updated_at: new Date().toISOString() })
|
|
314
|
+
.eq("id", itemId)
|
|
315
|
+
.eq("cart_id", cartId)
|
|
316
|
+
.select()
|
|
317
|
+
.single();
|
|
318
|
+
if (error || !data)
|
|
319
|
+
return { success: false, error: error?.message || "Cart item not found" };
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
data: {
|
|
323
|
+
item: data,
|
|
324
|
+
line_total: Math.round(data.quantity * (data.unit_price ?? 0) * 100) / 100,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// ======================== CART REMOVE ========================
|
|
329
|
+
case "cart_remove": {
|
|
330
|
+
const cartId = args.cart_id;
|
|
331
|
+
const itemId = args.item_id;
|
|
332
|
+
if (!cartId)
|
|
333
|
+
return { success: false, error: "cart_id is required" };
|
|
334
|
+
if (!itemId)
|
|
335
|
+
return { success: false, error: "item_id is required" };
|
|
336
|
+
// Validate cart is active
|
|
337
|
+
const { data: cart } = await sb.from("carts")
|
|
338
|
+
.select("id, status")
|
|
339
|
+
.eq("id", cartId)
|
|
340
|
+
.eq("store_id", sid)
|
|
341
|
+
.single();
|
|
342
|
+
if (!cart)
|
|
343
|
+
return { success: false, error: "Cart not found" };
|
|
344
|
+
if (cart.status !== "active")
|
|
345
|
+
return { success: false, error: "Cart is not active" };
|
|
346
|
+
const { error } = await sb.from("cart_items")
|
|
347
|
+
.delete()
|
|
348
|
+
.eq("id", itemId)
|
|
349
|
+
.eq("cart_id", cartId);
|
|
350
|
+
if (error)
|
|
351
|
+
return { success: false, error: error.message };
|
|
352
|
+
return { success: true, data: { removed: true, item_id: itemId } };
|
|
353
|
+
}
|
|
354
|
+
// ======================== CHECKOUT ========================
|
|
355
|
+
case "checkout": {
|
|
356
|
+
const cartId = args.cart_id;
|
|
357
|
+
if (!cartId)
|
|
358
|
+
return { success: false, error: "cart_id is required" };
|
|
359
|
+
// 1. Validate cart
|
|
360
|
+
const { data: cart, error: cartErr } = await sb.from("carts")
|
|
361
|
+
.select("*")
|
|
362
|
+
.eq("id", cartId)
|
|
363
|
+
.eq("store_id", sid)
|
|
364
|
+
.single();
|
|
365
|
+
if (cartErr || !cart)
|
|
366
|
+
return { success: false, error: "Cart not found" };
|
|
367
|
+
if (cart.status !== "active")
|
|
368
|
+
return { success: false, error: "Cart is not active" };
|
|
369
|
+
// 2. Fetch cart items
|
|
370
|
+
const { data: cartItems, error: itemsErr } = await sb.from("cart_items")
|
|
371
|
+
.select("*")
|
|
372
|
+
.eq("cart_id", cartId)
|
|
373
|
+
.order("created_at", { ascending: true });
|
|
374
|
+
if (itemsErr)
|
|
375
|
+
return { success: false, error: itemsErr.message };
|
|
376
|
+
if (!cartItems || cartItems.length === 0)
|
|
377
|
+
return { success: false, error: "Cart has no items" };
|
|
378
|
+
// 3. Compute totals
|
|
379
|
+
const subtotal = cartItems.reduce((sum, i) => sum + (i.quantity ?? 0) * (i.unit_price ?? 0), 0);
|
|
380
|
+
const taxAmount = cart.tax_amount ?? 0;
|
|
381
|
+
const total = Math.round((subtotal + taxAmount) * 100) / 100;
|
|
382
|
+
const roundedSubtotal = Math.round(subtotal * 100) / 100;
|
|
383
|
+
// 4. Generate sequential order number
|
|
384
|
+
const { data: maxOrder } = await sb.from("orders")
|
|
385
|
+
.select("order_number")
|
|
386
|
+
.eq("store_id", sid)
|
|
387
|
+
.order("created_at", { ascending: false })
|
|
388
|
+
.limit(1)
|
|
389
|
+
.single();
|
|
390
|
+
let nextNum = 10001;
|
|
391
|
+
if (maxOrder?.order_number) {
|
|
392
|
+
const parsed = parseInt(maxOrder.order_number.replace("#", ""), 10);
|
|
393
|
+
if (!isNaN(parsed))
|
|
394
|
+
nextNum = parsed + 1;
|
|
395
|
+
}
|
|
396
|
+
const orderNumber = `#${String(nextNum).padStart(5, "0")}`;
|
|
397
|
+
// 5. Insert order
|
|
398
|
+
const orderRecord = {
|
|
399
|
+
store_id: sid,
|
|
400
|
+
location_id: cart.location_id ?? null,
|
|
401
|
+
customer_id: cart.customer_id ?? null,
|
|
402
|
+
order_number: orderNumber,
|
|
403
|
+
channel: "online",
|
|
404
|
+
status: "pending",
|
|
405
|
+
subtotal: roundedSubtotal,
|
|
406
|
+
tax_amount: Math.round(taxAmount * 100) / 100,
|
|
407
|
+
total_amount: total,
|
|
408
|
+
payment_method: "pending",
|
|
409
|
+
payment_status: "pending",
|
|
410
|
+
};
|
|
411
|
+
const { data: order, error: orderErr } = await sb.from("orders")
|
|
412
|
+
.insert(orderRecord).select().single();
|
|
413
|
+
if (orderErr)
|
|
414
|
+
return { success: false, error: orderErr.message };
|
|
415
|
+
// 6. Bulk insert order_items
|
|
416
|
+
const orderItems = cartItems.map((ci) => ({
|
|
417
|
+
order_id: order.id,
|
|
418
|
+
product_id: ci.product_id,
|
|
419
|
+
product_name: ci.product_name,
|
|
420
|
+
quantity: ci.quantity,
|
|
421
|
+
unit_price: ci.unit_price,
|
|
422
|
+
line_total: Math.round((ci.quantity ?? 0) * (ci.unit_price ?? 0) * 100) / 100,
|
|
423
|
+
variant_id: ci.variant_id ?? null,
|
|
424
|
+
variant_name: ci.variant_name ?? null,
|
|
425
|
+
tier_label: ci.tier_label ?? null,
|
|
426
|
+
}));
|
|
427
|
+
await sb.from("order_items").insert(orderItems);
|
|
428
|
+
// 7. Deduct inventory per item (best-effort)
|
|
429
|
+
const locationId = cart.location_id;
|
|
430
|
+
if (locationId) {
|
|
431
|
+
for (const ci of cartItems) {
|
|
432
|
+
try {
|
|
433
|
+
await sb.rpc("atomic_inventory_adjust", {
|
|
434
|
+
p_store_id: sid,
|
|
435
|
+
p_product_id: ci.product_id,
|
|
436
|
+
p_location_id: locationId,
|
|
437
|
+
p_adjustment: -(ci.quantity ?? 0),
|
|
438
|
+
p_reason: "sale",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Inventory deduction non-critical — order still stands
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// 8. Mark cart as completed
|
|
447
|
+
await sb.from("carts")
|
|
448
|
+
.update({ status: "completed", updated_at: new Date().toISOString() })
|
|
449
|
+
.eq("id", cartId)
|
|
450
|
+
.eq("store_id", sid);
|
|
451
|
+
// 9. Return order summary
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
data: {
|
|
455
|
+
order_id: order.id,
|
|
456
|
+
order_number: orderNumber,
|
|
457
|
+
status: "pending",
|
|
458
|
+
subtotal: roundedSubtotal,
|
|
459
|
+
tax_amount: Math.round(taxAmount * 100) / 100,
|
|
460
|
+
total,
|
|
461
|
+
items: orderItems.map((oi) => ({
|
|
462
|
+
product_id: oi.product_id,
|
|
463
|
+
product_name: oi.product_name,
|
|
464
|
+
quantity: oi.quantity,
|
|
465
|
+
unit_price: oi.unit_price,
|
|
466
|
+
line_total: oi.line_total,
|
|
467
|
+
})),
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
default:
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
error: `Unknown storefront action: ${args.action}. Valid: browse, product_detail, search, categories, cart_create, cart_add, cart_view, cart_update, cart_remove, checkout`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -140,14 +140,32 @@ export async function handlePurchaseOrders(sb, args, storeId) {
|
|
|
140
140
|
if (poErr || !po)
|
|
141
141
|
return { success: false, error: poErr?.message || "PO not found" };
|
|
142
142
|
// If caller provides specific items use those, otherwise receive all remaining
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const poItems = po.items || [];
|
|
144
|
+
let receiveItems;
|
|
145
|
+
if (args.items) {
|
|
146
|
+
// Normalize caller-provided items: accept item_id, po_item_id, or product_id
|
|
147
|
+
const callerItems = args.items;
|
|
148
|
+
receiveItems = callerItems.map((ci) => {
|
|
149
|
+
let itemId = ci.item_id || ci.po_item_id || ci.line_item_id;
|
|
150
|
+
// If caller used product_id instead, resolve to PO line item ID
|
|
151
|
+
if (!itemId && ci.product_id) {
|
|
152
|
+
const match = poItems.find((pi) => pi.id === ci.product_id || pi.product_id === ci.product_id);
|
|
153
|
+
if (match)
|
|
154
|
+
itemId = match.id;
|
|
155
|
+
}
|
|
156
|
+
return { item_id: itemId || null, quantity: Number(ci.quantity) || 0 };
|
|
157
|
+
}).filter((i) => i.item_id && i.quantity > 0);
|
|
158
|
+
if (!receiveItems.length)
|
|
159
|
+
return { success: false, error: "No valid items to receive — provide item_id or product_id matching a PO line item" };
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
receiveItems = poItems
|
|
146
163
|
.map((item) => ({
|
|
147
164
|
item_id: item.id,
|
|
148
165
|
quantity: Math.max((item.quantity || 0) - (item.received_quantity || 0), 0),
|
|
149
166
|
}))
|
|
150
167
|
.filter((item) => item.quantity > 0);
|
|
168
|
+
}
|
|
151
169
|
if (!receiveItems.length)
|
|
152
170
|
return { success: false, error: "No items to receive (all fully received)" };
|
|
153
171
|
const locationId = args.location_id || po.location_id;
|