NetSuite Shopify Integration – Order Sync (Draft Order & PO# Metafield)

We’ve updated the Shopify to NetSuite Order Sync, enabling automatic creation of ‘Pending Fulfillment’ Sales Orders in NetSuite for every new order placed in Shopify. These synced orders include all essential details such as product name, quantity, customer information, payment status, and shipping address.

Note: Any changes made to Shopify orders after fulfillment will not be reflected in NetSuite. Only updates made prior to fulfillment will sync.

Draft Order Logic

The integration now supports draft order handling with the following logic:

  • If Draft Order = true and the customer is marked as a credit customer, the order will sync to NetSuite as a Sales Order.
  • Customer Deposits will not be created for these orders.
  • Orders with payment statuses such as authorized, partially_paid, paid, or pending will sync accordingly.

PO# Field Mapping from Shopify Metafield

  • The Order Reference metafield is retrieved using Shopify’s GraphQL API (as the REST Admin API does not support this).
  • This value is mapped to the PO# field in the NetSuite Sales Order during creation.

Expected Outcome:

  • Orders appear in NetSuite as ‘Pending Fulfillment’ Sales Orders within seconds.
  • All entered details match exactly.
  • Real-time sync is triggered upon order creation.
  • Scheduled sync ensures updates are reflected post-creation.

Payment Sync

To ensure accurate logging of customer payments, we’ve disabled the native Payment Sync from Shopify to NetSuite. Instead, the integration now creates Customer Deposit records in NetSuite for orders with payments processed through Shopify. Each deposit is linked to the corresponding Shopify Order ID and customer.

Customer Deposit Flow

  • Applies to all customers, including new and cash customers.
  • Orders marked as Draft are excluded.
  • Conditional filters differentiate between:
  • Draft Order = False
  • Financial Status = Paid
  • Cash Customer
  • Celigo’s native Customer Deposit flow is disabled and replaced with custom script logic.
  • All field mappings and order statuses in NetSuite are validated post-sync.

Code For Integration

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
/**
 * Crest Group Ltd-NZ-NS
 *
 * Epic: CGLNN-360
 *
 * **********************************************************************************************
 * CGLNN-350: Dev | Change Request | NetSuite Shopify Integration - Order Sync (Draft Order & PO# Metafield)
 *
 * Author: Jobin & Jismi
 *
 * Date Created: 15-Jul-2025
 *
 *COPYRIGHT © 2024 Jobin & Jismi IT Services LLP. All rights reserved. This script is a proprietary product of Jobin & Jismi IT Services LLP and is protected by copyright law and international treaties.
 Unauthorized reproduction or distribution of this script, or any portion of it, may result in severe civil and criminal penalties and will be prosecuted to the maximum extent possible under law.
 *
 * Description: This script creates a Customer Deposit from a Sales Order based on Shopify order data.
 * It retrieves the Shopify order using the provided order ID, validates the Sales Order, sets the PO# field
 * with the order metafield value, and creates a Customer Deposit with the appropriate amount and Sales Order reference.
 *
 * REVISION HISTORY
 *
 * @version 1.0 CGLNN-350: 15-Jul-2025: Created the initial build by JJ0223
 *
***********************************************************************************************/
define(['N/record', 'N/https', 'N/runtime', 'N/search'],


    /** @type {import('N/record')} */
    /** @type {import('N/https')} */
    /** @type {import('N/runtime')} */
    /** @type {import('N/search')} */


    (record, https, runtime, search) => {
        "use strict";
        const SHOPIFY_API_URL = runtime.getCurrentScript().getParameter({ name: 'custscript_jj_shopify_api_url' });
        const SHOPIFY_ACCESS_TOKEN = runtime.getCurrentScript().getParameter({ name: 'custscript_jj_shopify_access_token' });
        const PAYMENT_METHOD = 9; // Default Payment Method Set Customer Deposit
        const DRAFT_ORDER_NAMESPACE = 'app--79060795393--draft-order'; // Shopify Draft Order Meta Field Name Space Key


        /**
         * Executes after a Sales Order is submitted, creating a Customer Deposit based on Shopify order data
         * and updating the Sales Order with the PO# from order metafields.
         * @param {Object} context - The script context
         * @param {import('N/record').Record} context.newRecord - The new Sales Order record
         * @param {import('N/record').Record} context.oldRecord - The old Sales Order record (null for CREATE)
         * @param {string} context.type - The trigger type (e.g., context.UserEventType.CREATE)
         */
        function afterSubmit(context) {
            if (context.type !== context.UserEventType.CREATE) return;
            try {
                let salesOrder = context.newRecord;
                let shopifyOrderId = salesOrder.getValue('custbody_celigo_etail_order_id');
                let customerId = salesOrder.getValue('entity');
                let salesOrderId = salesOrder.id;
                if (!shopifyOrderId) {
                    log.error({
                        title: 'Missing Shopify Order ID',
                        details: 'Cannot proceed without Shopify Order ID.'
                    });
                    return;
                }
                // Get tranid from context.newRecord
                let tranId = salesOrder.getValue('tranid');
                let salesOrderRef = 'Sales Order #' + tranId;
                let shopifyConfig = getShopifyConfig();
                let shopifyOrderData = getShopifyOrderDetails(shopifyConfig, shopifyOrderId);
                if (!shopifyOrderData || !shopifyOrderData.order) {
                    log.error({
                        title: 'No Order Found',
                        details: `Order not found for ID: ${shopifyOrderId}`
                    });
                    return;
                }
                // Get order total amount, financial status, and metafields
                let totalAmount = 0;
                let orderType = 'order';
                let displayFinancialStatus = null;
                let draftOrderMetafield = false; // Default to false if metafield is missing
                let poNumber = null;
                if (shopifyOrderData.order && shopifyOrderData.order.totalPriceSet) {
                    totalAmount = parseFloat(shopifyOrderData.order.totalPriceSet.shopMoney.amount) || 0;
                    displayFinancialStatus = shopifyOrderData.order.displayFinancialStatus;
                    // Check customer metafields for draft_order
                    if (shopifyOrderData.order.customer && shopifyOrderData.order.customer.metafields.edges) {
                        const metafield = shopifyOrderData.order.customer.metafields.edges.find(
                            edge => edge.node.namespace === DRAFT_ORDER_NAMESPACE &&
                                edge.node.key === 'use_draft_order'
                        );
                        if (metafield) {
                            draftOrderMetafield = metafield.node.value === 'true';
                        }
                    }
                    // Check order metafields for PO#
                    if (shopifyOrderData.order.metafields.edges) {
                        const poMetafield = shopifyOrderData.order.metafields.edges.find(
                            edge => edge.node.key === 'order_ref' && edge.node.namespace === 'custom'
                        );
                        if (poMetafield) {
                            poNumber = poMetafield.node.value;
                        }
                    }
                }
                // Update Sales Order with PO# if found using submitFields
                if (poNumber) {
                    record.submitFields({
                        type: record.Type.SALES_ORDER,
                        id: salesOrderId,
                        values: {
                            otherrefnum: poNumber
                        },
                        options: {
                            enablesourcing: false,
                            ignoreMandatoryFields: true
                        }
                    });
                    log.audit({
                        title: 'Sales Order Updated',
                        details: `Set PO# to ${poNumber} for Sales Order ID: ${salesOrderId}`
                    });
                }
                if (shouldCreateCustomerDeposit(totalAmount, orderType, shopifyOrderData, displayFinancialStatus, draftOrderMetafield)) {
                    createCustomerDeposit(salesOrderId, customerId, totalAmount, salesOrderRef, salesOrder);
                } else {
                    log.audit({
                        title: 'Customer Deposit Skipped',
                        details: `No valid total amount, conditions not met, or draft_order metafield is true. Order Type: ${orderType}, Draft Order Metafield: ${draftOrderMetafield}`
                    });
                }
            } catch (e) {
                log.error({
                    title: 'Error in afterSubmit',
                    details: e.toString()
                });
            }
        }
        /**
         * Retrieves Shopify API configuration details.
         * @returns {Object} Configuration object with API URL and access token
         */
        function getShopifyConfig() {
            try {
                // TODO: Replace hardcoded token with secure storage (e.g., custom record or script parameter)
                return {
                    apiUrl: SHOPIFY_API_URL,
                    accessToken: SHOPIFY_ACCESS_TOKEN
                };
            } catch (e) {
                log.error({
                    title: 'Error in getShopifyConfig',
                    details: e.toString()
                });
                return null;
            }
        }
        /**
         * Retrieves Shopify order details using GraphQL API.
         * @param {Object} config - Shopify API configuration (apiUrl, accessToken)
         * @param {string} orderId - Shopify order ID
         * @returns {Object|null} Shopify order data, or null if failed
         */
        function getShopifyOrderDetails(config, orderId) {
            try {
                let orderQuery = `
                query getOrderDetails($id: ID!) {
                    order(id: $id) {
                        id
                        name
                        createdAt
                        displayFinancialStatus
                        totalPriceSet {
                            shopMoney {
                                amount
                                currencyCode
                            }
                        }
                        metafields(first: 250) {
                            edges {
                                node {
                                    id
                                    namespace
                                    key
                                    value
                                    type
                                }
                            }
                        }
                        customer {
                            metafields(first: 250, namespace: "${DRAFT_ORDER_NAMESPACE}") {
                                edges {
                                    node {
                                        key
                                        value
                                        namespace
                                    }
                                }
                            }
                        }
                    }
                }
            `;


                let variables = { id: `gid://shopify/Order/${orderId}` };
                let response = https.post({
                    url: config.apiUrl,
                    headers: {
                        'X-Shopify-Access-Token': config.accessToken,
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ query: orderQuery, variables })
                });
                log.debug({
                    title: 'Shopify Response Status',
                    details: response.code
                });
                log.debug({
                    title: 'Shopify Response Body',
                    details: response.body
                });
                if (response.headers['Content-Type'].indexOf('application/json') === -1) {
                    log.error({
                        title: 'Non-JSON Response',
                        details: `Received: ${response.body}`
                    });
                    return null;
                }
                let respBody = JSON.parse(response.body);
                if (respBody.errors) {
                    log.error({
                        title: 'Shopify API Error',
                        details: JSON.stringify(respBody.errors)
                    });
                    return null;
                }
                return respBody.data;
            } catch (e) {
                log.error({
                    title: 'Error in getShopifyOrderDetails',
                    details: e.toString()
                });
                return null;
            }
        }


        /**
         * Determines if a Customer Deposit should be created based on order data.
         * @param {number} totalAmount - The total amount of the order
         * @param {string} orderType - The type of order ('order')
         * @param {Object} shopifyOrderData - Shopify order data
         * @param {string|null} displayFinancialStatus - The financial status of the order
         * @param {boolean} draftOrderMetafield - The value of the customer's draft_order metafield
         * @returns {boolean} True if a Customer Deposit should be created, false otherwise
         */
        function shouldCreateCustomerDeposit(totalAmount, orderType, shopifyOrderData, displayFinancialStatus, draftOrderMetafield) {
            try {
                // For regular orders, check if total amount is positive, financial status is PAID, and draft_order metafield is false
                if (orderType === 'order' && totalAmount > 0 && displayFinancialStatus === 'PAID' && !draftOrderMetafield) {
                    return true;
                }
                return false;
            } catch (e) {
                log.error({
                    title: 'Error in shouldCreateCustomerDeposit',
                    details: e.toString()
                });
                return false;
            }
        }
        /**
         * Creates a Customer Deposit record linked to the Sales Order.
         * @param {number} salesOrderId - The internal ID of the Sales Order
         * @param {number} customerId - The internal ID of the customer
         * @param {number} amount - The deposit amount
         * @param {string} salesOrderRef - The Sales Order reference (e.g., Sales Order #SO0492)
         * @param {Object} salesOrder - The Sales Order record from context.newRecord
         */
        function createCustomerDeposit(salesOrderId, customerId, amount, salesOrderRef, salesOrder) {
            try {
                // Define custom fields to retrieve from Sales Order
                const customFieldIds = [
                    'custbody_celigo_etail_order_id',
                    'custbody_celigo_etail_channel',
                    'custbody_celigo_shopify_order_no',
                    'custbody_celigo_shopify_store_id',
                    'custbody_celigo_shopify_store'
                ];


                // Get custom field values directly from salesOrder (context.newRecord)
                let customFields = {};
                customFieldIds.forEach(fieldId => {
                    let fieldValue = salesOrder.getValue(fieldId);
                    if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
                        customFields[fieldId] = fieldValue;
                    }
                });


                // Create Customer Deposit
                let depositRec = record.create({
                    type: record.Type.CUSTOMER_DEPOSIT,
                    isDynamic: true
                });
                // Set standard fields
                depositRec.setValue({
                    fieldId: 'customer',
                    value: parseInt(customerId, 10),
                    ignoreFieldChange: true
                });
                depositRec.setValue({
                    fieldId: 'salesorder',
                    value: parseInt(salesOrderId, 10),
                    ignoreFieldChange: true
                });
                depositRec.setValue({
                    fieldId: 'amount',
                    value: amount
                });
                depositRec.setValue({
                    fieldId: 'payment',
                    value: amount
                });
                depositRec.setValue({
                    fieldId: 'paymentoption',
                    value: PAYMENT_METHOD
                });
                // Set custom fields
                for (let fieldId of customFieldIds) {
                    let fieldValue = customFields[fieldId];
                    if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
                        depositRec.setValue({
                            fieldId: fieldId,
                            value: fieldValue
                        });
                    }
                }
                let depositId = depositRec.save();
                log.audit({
                    title: 'Customer Deposit Created',
                    details: `Deposit ID: ${depositId}, Amount: ${amount}, Sales Order Ref: ${salesOrderRef}`
                });
            } catch (e) {
                log.error({
                    title: 'Error in createCustomerDeposit',
                    details: e.toString()
                });
            }
        }
        return {
            afterSubmit
        };
    });

Leave a comment

Your email address will not be published. Required fields are marked *