← back to Ventura Claw

server/connectors/shopify.js

79 lines

// Shopify (DW prod store) — REAL.
// Reads SHOPIFY_ADMIN_TOKEN + SHOPIFY_STORE from env. Routed via secrets-manager.
// Docs: https://shopify.dev/docs/api/admin-rest

function token(c) {
  const t = c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN;
  if (!t) throw new Error("SHOPIFY_ADMIN_TOKEN not set");
  return t;
}
function store(c) {
  return c?.SHOPIFY_STORE || process.env.SHOPIFY_STORE || "designer-laboratory-sandbox.myshopify.com";
}
async function call(method, path, body, c) {
  const url = `https://${store(c)}/admin/api/2024-10${path}`;
  const r = await fetch(url, {
    method,
    headers: { "X-Shopify-Access-Token": token(c), "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(20_000)
  });
  const text = await r.text();
  if (!r.ok) throw new Error(`shopify ${method} ${path} ${r.status}: ${text.slice(0, 200)}`);
  return text ? JSON.parse(text) : {};
}

module.exports = {
  meta: { id: "shopify", name: "Shopify", category: "commerce", docsUrl: "https://shopify.dev/docs/api/admin-rest", auth: "api_key", realImpl: true },
  fields: [
    { key: "SHOPIFY_ADMIN_TOKEN", label: "Admin Access Token (shpat_...)", type: "password", required: true, hint: "Settings → Apps → Develop apps → reveal Admin API access token" },
    { key: "SHOPIFY_STORE",       label: "Store domain",                    type: "text",     required: false, hint: "e.g. mystore.myshopify.com (optional, defaults to env)" },
  ],
  configured(c) { return !!(c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN); },
  async health(c) {
    if (!this.configured(c)) return { ok: false, reason: "no_token" };
    try {
      const d = await call("GET", "/shop.json", null, c);
      return { ok: true, store: d.shop?.name, domain: d.shop?.myshopify_domain, plan: d.shop?.plan_name };
    } catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "shop.info"(_, c) { return call("GET", "/shop.json", null, c); },
    async "products.list"({ limit }, c) { return call("GET", `/products.json?limit=${Math.min(limit||10, 250)}`, null, c); },
    async "products.count"(_, c) { return call("GET", "/products/count.json", null, c); },
    async "orders.list"({ status, limit }, c) {
      const s = status || "any";
      return call("GET", `/orders.json?status=${s}&limit=${Math.min(limit||10, 250)}`, null, c);
    },
    async "orders.recent"(_, c) {
      return call("GET", `/orders.json?status=any&limit=20&order=created_at desc`, null, c);
    },
    async "order.fulfill"({ order_id, tracking_number, tracking_company }, c) {
      if (!order_id) throw new Error("order_id required");
      return call("POST", `/orders/${order_id}/fulfillments.json`, {
        fulfillment: { tracking_number, tracking_company, notify_customer: true }
      }, c);
    },
    async "order.refund"({ order_id, amount, reason }, c) {
      if (!order_id) throw new Error("order_id required");
      return call("POST", `/orders/${order_id}/refunds.json`, {
        refund: { note: reason || "automated refund", shipping: { full_refund: true }, transactions: amount ? [{ amount, kind: "refund" }] : undefined }
      }, c);
    },
    async "product.update"({ product_id, title, tags, body_html, status }, c) {
      if (!product_id) throw new Error("product_id required");
      const fields = { id: product_id };
      if (title !== undefined)     fields.title = title;
      if (tags !== undefined)      fields.tags = tags;
      if (body_html !== undefined) fields.body_html = body_html;
      if (status !== undefined)    fields.status = status;
      return call("PUT", `/products/${product_id}.json`, { product: fields }, c);
    },
    async "collections.list"(_, c) {
      const cust = await call("GET", "/custom_collections.json?limit=250", null, c);
      const smart = await call("GET", "/smart_collections.json?limit=250", null, c);
      return { custom: cust.custom_collections || [], smart: smart.smart_collections || [] };
    },
  }
};