
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
};
});