How to Apply Weight-Based Shipping on Custom Line Items in Draft Orders (Across Multiple Markets)?

How to Apply Weight-Based Shipping on Custom Line Items in Draft Orders (Across Multiple Markets)?

Abrar1
Shopify Partner
3 0 0

Hi everyone,

 

We sell a custom headboard that includes around 6 configurable options, which can’t be handled through Shopify’s default variant system. To work around this, we’ve built a custom form on the product page where customers select their options, we calculate the final price on the front-end, and then use the Draft Order API to create the order programmatically.

 

For our pre-configured headboards, we use weight-based shipping with proper shipping profiles set up — and we’d like to apply the same logic to these custom-configured headboards created via draft orders.

 

Currently, we create a custom line item in the draft order using the calculated price from the front end. The issue is that custom items don’t exist in the Shopify product catalog, so they don’t follow any shipping profile rules, even if we know their weight.

 

Alternatively, if we try to use an existing Shopify product in draft order and override the weight dynamically based on the selected options, Shopify’s Draft Order API doesn’t allow us to override the product weight.

 

To make things more complex, we also sell in two different countries/markets, and we want to apply market-specific shipping rates based on the destination.

 

My Question:

Is there a way to assign a weight to a custom line item in a draft order so that Shopify can calculate weight-based shipping?

 

Is there any workaround that allows us to dynamically adjust the shipping based on weight and destination when using the Draft Order API?

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const fetch = require("cross-fetch");

const app = express();
const port = process.env.PORT || 3000;

// Allowed origins for CORS
const allowedOrigins = [
  "https://cofaneti.com",
  "https://admin.shopify.com",
  "https://53bd76-5a.myshopify.com",
];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("CORS not allowed"));
      }
    },
    credentials: true,
  })
);

// Set headers for all responses
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "https://cofaneti.com");
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
  res.header("Access-Control-Allow-Credentials", "true");
  if (req.method === "OPTIONS") {
    return res.status(200).end();
  }
  next();
});

app.use(bodyParser.json());

// Access token for Shopify
const shopifyAccessToken = process.env.SHOPIFY_ACCESS_TOKEN;

/**
 * Shipping cost table based on length & height:
 *
 *  - Length: 70-110,  Height: 70-120 => 30€,  Height: 121-150 => 40€
 *  - Length: 111-140, Height: 70-120 => 35€,  Height: 121-150 => 45€
 *  - Length: 141-170, Height: 70-120 => 35€,  Height: 121-150 => 45€
 *  - Length: 171-180, Height: 70-120 => 37€,  Height: 121-150 => 47€
 *  - Length: 181-200, Height: 70-120 => 40€,  Height: 121-150 => 50€
 *  - Length: 200-220, Height: 70-110 => 50€,  Height: 121-150 => 58€
 *
 * If no condition matches, returns 0.
 */
const calculateShippingCost = (length, height) => {
  console.log(
    ⁠ Calculating shipping for Length: ${length}cm, Height: ${height}cm ⁠
  );

  // 70-110 length
  if (length >= 70 && length <= 110) {
    if (height >= 70 && height <= 120) return 30;
    if (height >= 121 && height <= 150) return 40;
  }

  // 111-140 length
  if (length >= 111 && length <= 140) {
    if (height >= 70 && height <= 120) return 35;
    if (height >= 121 && height <= 150) return 45;
  }

  // 141-170 length
  if (length >= 141 && length <= 170) {
    if (height >= 70 && height <= 120) return 35;
    if (height >= 121 && height <= 150) return 45;
  }

  // 171-180 length
  if (length >= 171 && length <= 180) {
    if (height >= 70 && height <= 120) return 37;
    if (height >= 121 && height <= 150) return 47;
  }

  // 181-200 length
  if (length >= 181 && length <= 200) {
    if (height >= 70 && height <= 120) return 40;
    if (height >= 121 && height <= 150) return 50;
  }

  // 200-220 length
  if (length >= 200 && length <= 220) {
    if (height >= 70 && height <= 110) return 50;
    if (height >= 121 && height <= 150) return 58;
  }

  return 0; // No match => 0
};

// Simple test route
app.get("/", (req, res) => {
  res.send("Hello, from the draft order API");
});

app.post("/create-draft-order", async (req, res) => {
  console.log("Inside creating draft order on server");

  try {
    const cart = req.body;
    let totalShippingCost = 0; // Sum up shipping for all items

    // Build line_items from the cart
    const lineItems = cart.items.map((item) => {
      // If it's a custom product with "Price" property
      if (item.properties && item.properties.Price) {
        let length = null;
        let height = null;
        const lineItemProperties = [];

        for (const [key, value] of Object.entries(item.properties)) {
          // Keep all properties except "Price"
          if (key !== "Price") {
            lineItemProperties.push({ name: key, value: String(value) });
          }

          // Extract length & height if we see "Size" in format "XXcm x YYcm"
          if (key === "Size") {
            const sizeMatch = value.match(/(\d+)cm x (\d+)cm/);
            if (sizeMatch) {
              length = parseInt(sizeMatch[1], 10);
              height = parseInt(sizeMatch[2], 10);
            }
          }
        }

        // Calculate shipping cost for this custom item
        if (length && height) {
          const itemShippingCost = calculateShippingCost(length, height);
          totalShippingCost += itemShippingCost * item.quantity;
        }

        // Return a custom line item
        return {
          title: "Custom Product",
          price: parseFloat(item.properties.Price),
          quantity: item.quantity,
          requires_shipping: true,
          properties: lineItemProperties,
        };
      } else {
        // Normal variant-based item (no extra dimension-based shipping logic)
        return {
          variant_id: item.variant_id,
          quantity: item.quantity,
        };
      }
    });

    // Create the draft order payload
    const draftOrderData = {
      draft_order: {
        line_items: lineItems,
        // Add a shipping_line for the entire draft order
        shipping_line: {
          title: "Shipping",
          price: totalShippingCost.toFixed(2), // e.g. "40.00"
          code: "custom_shipping",
        },
        use_customer_default_address: true,
      },
    };

    // Send request to Shopify
    const response = await fetch(
      ⁠ https://${process.env.SHOP_DOMAIN}/admin/api/2024-01/draft_orders.json ⁠,
      {
        method: "POST",
        headers: {
          "X-Shopify-Access-Token": shopifyAccessToken,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(draftOrderData),
      }
    );

    const responseData = await response.json();

    if (responseData.draft_order && responseData.draft_order.invoice_url) {
      console.log(
        "Draft order created. Invoice URL:",
        responseData.draft_order.invoice_url
      );
      res.json({ invoice_url: responseData.draft_order.invoice_url });
    } else {
      console.error(
        "Draft order response is missing invoice_url:",
        responseData
      );
      res
        .status(500)
        .json({ error: "Draft order creation failed. Missing invoice_url." });
    }
  } catch (error) {
    console.error("Error:", error);
    res
      .status(500)
      .json({ error: "An error occurred while creating the draft order" });
  }
});

app.listen(port, () => {
  console.log(⁠ Server is running on port ${port} ⁠);
});



Replies 0 (0)