Skip to content

techdivision/netsuite-connector-doc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

NetSuite Connector (Adobe App Builder)

An Adobe App Builder extension that syncs customers and orders between Adobe Commerce and NetSuite using Adobe I/O Runtime actions.

Built on the Adobe Commerce Integration Starter Kit and extends it with NetSuite-specific transformers, senders, and a scheduled delta sync for customers.

Features

Feature Status Description
Commerce → NetSuite customers Upserts Commerce customers to NetSuite via SuiteTalk REST
Commerce → NetSuite orders Upserts Commerce sales orders to NetSuite via SuiteTalk REST
NetSuite → Commerce customers Delta sync of NetSuite customers back to Commerce (cron-based)
OAuth 1.0a (NetSuite) Token-Based Authentication for all NetSuite API calls
OAuth1 / IMS (Commerce) PaaS (OAuth1) and SaaS (IMS) Commerce authentication
Telemetry OpenTelemetry instrumentation (opt-in via ENABLE_TELEMETRY)

Quick Start

1. Clone and setup

git clone <repository-url>
cd netsuite-connector
npm install

2. Configure workspace

aio login
aio console org select
aio console project select
aio console workspace select
aio app use  # Choose 'm' (merge)

3. Fill .env

cp env.dist .env
chmod +x scripts/fetch-secrets.sh
scripts/fetch-secrets.sh <workspace> # e.g. Stage
# Edit .env with your Commerce and NetSuite credentials

4. Add Commerce custom attribute

Create a new attribute with Attribute Code = netsuite_internal_id in Adobe Commerce AdminStores > AttributesCustomer.

5. Configure Adobe I/O Events in Adobe Commerce instance

If your Commerce instance Adobe I/O Events for Adobe Commerce module version is 1.6.0 or greater and the onboarding script completed successfully, the following steps are not required. The onboarding script will configure the Adobe Commerce instance automatically. Follow the steps in the next section to validate that the configuration is correct or skip to the next section.

To configure the provider in Commerce, do the following:

  • In the Adobe Commerce Admin, navigate to Stores > Settings > Configuration > Adobe Services > Adobe I/O Events > General configuration. The following screen displays. Alt text
  • Select OAuth (Recommended) from the Adobe I/O Authorization Type menu.
  • Copy the contents of the <workspace-name>.json (Workspace configuration JSON you downloaded in the previous step Create app builder project) into the Adobe I/O Workspace Configuration field.
  • Copy the commerce provider instance ID you saved in the previous step Execute the onboarding into the Adobe Commerce Instance ID field.
  • Copy the commerce provider ID you saved in the previous step Execute the onboarding into the Adobe I/O Event Provider ID field.
  • Click Save Config.
  • Enable Commerce Eventing by setting the Enabled menu to Yes. (Note: You must enable cron so that Commerce can send events to the endpoint.)
  • Enter the merchant's company name in the Merchant ID field. You must use alphanumeric and underscores only.
  • In the Environment ID field, enter a temporary name for your workspaces while in development mode. When you are ready for production, change this value to a permanent value, such as Production.
  • (Optional) By default, if an error occurs when Adobe Commerce attempts to send an event to Adobe I/O, Commerce retries a maximum of seven times. To change this value, uncheck the Use system value checkbox and set a new value in the Maximum retries to send events field.
  • (Optional) By default, Adobe Commerce runs a cron job (clean_event_data) every 24 hours that delete event data three days old. To change the number of days to retain event data, uncheck the Use system value checkbox and set a new value in the Event retention time (in days) field.
  • Click Save Config.

6. Deploy

aio app deploy

7. Onboard events

npm run onboard
npm run commerce-event-subscribe

For more detailed general information about the setup, see the README.md.

Architecture

flowchart TB
    subgraph Commerce["Adobe Commerce"]
        CommerceAPI["Commerce REST API"]
        CommerceEvents["Commerce Events"]
    end

    subgraph AppBuilder["Adobe App Builder"]
        subgraph C2N_Customer["Commerce → NetSuite: Customer"]
            CustConsumer["customer/commerce/consumer"]
            CustSync["customer/commerce/sync\n(pre → transform → send → post)"]
        end

        subgraph C2N_Order["Commerce → NetSuite: Order"]
            OrdConsumer["order/commerce/consumer"]
            OrdSync["order/commerce/sync\n(validate → pre → transform → send → post)"]
        end

        subgraph N2C_Customer["NetSuite → Commerce: Customer (cron)"]
            ExtSync["customer/external/sync\n(fetch → transform → send → state update)"]
        end
    end

    subgraph NetSuite["NetSuite"]
        SuiteTalk["SuiteTalk REST API\n(record/v1/customer, record/v1/salesOrder)"]
        SuiteQL["SuiteQL API\n(customer delta query)"]
    end

    CommerceEvents --> CustConsumer --> CustSync
    CommerceEvents --> OrdConsumer --> OrdSync
    CustSync -->|"PUT eid:<id>"| SuiteTalk
    OrdSync -->|"PUT eid:<increment_id>"| SuiteTalk
    SuiteTalk -->|"netsuite_internal_id"| CommerceAPI
    SuiteQL -->|"lastmodifieddate delta"| ExtSync
    ExtSync -->|"upsert customers"| CommerceAPI
Loading

Project Structure

netsuite-connector/
├── app.config.yaml                # App Builder configuration (actions, triggers, hooks)
├── env.dist                       # Environment variable template
├── package.json
│
├── actions/                       # Runtime Actions
│   ├── customer/
│   │   ├── commerce/
│   │   │   ├── consumer/          # Routes Commerce customer events
│   │   │   └── sync/              # Commerce → NetSuite customer upsert
│   │   │       ├── index.js
│   │   │       ├── pre.js         # Fetches full customer from Commerce
│   │   │       ├── transformer.js # Maps Commerce customer → NetSuite customer
│   │   │       ├── sender.js      # PUT record/v1/customer/eid:<id>
│   │   │       └── post.js        # Writes netsuite_internal_id back to Commerce
│   │   └── external/
│   │       └── sync/              # NetSuite → Commerce customer delta sync
│   │           ├── index.js
│   │           ├── pre.js         # SuiteQL paginated fetch
│   │           ├── transformer.js # Maps NetSuite customer → Commerce customer
│   │           └── sender.js      # Search / create / update in Commerce
│   ├── order/
│   │   └── commerce/
│   │       ├── consumer/          # Routes Commerce order events
│   │       └── sync/              # Commerce → NetSuite order upsert
│   │           ├── index.js
│   │           ├── validator.js   # Validates order payload
│   │           ├── pre.js         # Reads guest customer / subsidiary config
│   │           ├── transformer.js # Maps Commerce order → NetSuite salesOrder
│   │           ├── sender.js      # PUT record/v1/salesOrder/eid:<increment_id>
│   │           └── post.js        # Writes netsuite_internal_id back to Commerce
│   └── oauth1a.js                 # NetSuite OAuth 1.0a helper
│
└── scripts/
    └── onboarding/                # Event provider & registration setup

Data Flows

Commerce → NetSuite: customers (actions/customer/commerce/sync)

Pipeline: preProcesstransformDatasendDatapostProcess

  • Trigger: com.adobe.commerce.<EVENT_PREFIX>.observer.customer_save_commit_after (via actions/customer/commerce/consumer).
  • Pre-process (pre.js):
    • Fetches the full Commerce customer by params.data.id using getCustomer API.
    • Skips fetch if params.data.id equals NETSUITE_GUEST_CUSTOMER.
  • Transform (transformer.js):
    • isPerson is true when company field is empty or missing.
    • Maps email, firstname, lastname from Commerce customer.
    • subsidiary defaults to { id: 1 } (can be overridden via options).
    • phone field: taken from data.phone, or from the first address with a non-empty telephone; included only when non-empty.
    • addressBook built from Commerce addresses:
      • Each address includes externalId, addressee, addr1, addr2, city, state (mapped from region_code), zip, country, addrPhone.
      • defaultBilling and defaultShipping flags are preserved.
      • Label assigned: "Default" (both flags), "Billing", "Shipping", or "Additional".
    • Country code mapping: UKGB.
    • Street lines are deduplicated and filtered.
  • Send (sender.js):
    • NetSuite SuiteTalk REST upsert via PUT record/v1/customer/eid:<commerce_customer_id>.
    • Uses OAuth 1.0a Token-Based Authentication.
    • Extracts NetSuite Internal ID from location response header.
  • Post-process (post.js):
    • Updates Commerce customer custom_attributes.netsuite_internal_id with NetSuite Internal ID.
    • Uses updateCustomer API.
    • Validates the update by checking the returned attribute value.

Commerce → NetSuite: orders (actions/order/commerce/sync)

Pipeline: validateDatapreProcesstransformDatasendDatapostProcess

  • Trigger: com.adobe.commerce.<EVENT_PREFIX>.observer.sales_order_save_commit_after (via actions/order/commerce/consumer).
  • Validation (validator.js):
    • Requires data.value.entity_id for correlation.
    • Requires data.value.increment_id.
    • Requires data.value.items (non-empty array).
    • Each item must have sku and qty_ordered.
    • Requires data.value.addresses.
    • For guest orders: requires data.value.customer_email.
    • For registered orders: requires data.value.customer_id.
  • Pre-process (pre.js):
    • Reads NETSUITE_GUEST_CUSTOMER and NETSUITE_DEFAULT_SUBSIDIARY_ID (defaults to "1").
    • Returns { guestCustomerInternalId, subsidiaryId }.
  • Transform (transformer.js):
    • entity.externalId:
      • Registered customers: uses customer_id.
      • Guest orders: uses NETSUITE_GUEST_CUSTOMER or guest_<email>.
    • otherrefnum = increment_id.
    • billingAddress and shippingAddress:
      • Shipping falls back to billing if not present.
      • Checks data.addresses array and extension_attributes.shipping_assignments.
      • Each address includes addressee, addr1, addr2, city, state, zip, country.
    • item.items:
      • Excludes configurable parent items (product_type !== "configurable").
      • Only includes items with qty_ordered > 0.
      • Maps skuitem.externalId, qty_orderedquantity, pricerate, namedescription.
    • Country code mapping: UKGB.
    • Street lines are deduplicated and filtered.
  • Send (sender.js):
    • NetSuite SuiteTalk REST upsert via PUT record/v1/salesOrder/eid:<increment_id>.
    • Uses OAuth 1.0a Token-Based Authentication.
    • Extracts NetSuite Internal ID from location response header.
    • Parses error details from o:errorDetails if available.
  • Post-process (post.js):
    • Updates Commerce order custom_attributes.netsuite_internal_id with NetSuite Internal ID.
    • Uses updateOrder API.
    • Logs success or failure; does not fail the action if update fails.

NetSuite → Commerce: customers (actions/customer/external/sync)

Pipeline: getNetSuiteCustomerstransformCustomerssendData → state update

  • Trigger: scheduled cron (not event-driven).
    • Cron trigger configured in app.config.yaml under application.runtimeManifest.triggers.customer-sync-trigger.
    • The cron expression is provided by NETSUITE_CUSTOMER_CRON_SYNC_EXPRESSION (see .env/env.dist).
  • Delta sync:
    • State key customer_sync_last_date stores last sync cursor (TTL 365 days = 31,536,000 seconds).
    • If missing, uses NETSUITE_CUSTOMER_LAST_SYNC_DATE or performs full sync.
    • Current sync timestamp is captured before fetching data.
    • lastmodifieddate in NetSuite is a timestamp field; SuiteQL comparisons use TO_TIMESTAMP with YYYY-MM-DD HH24:MI:SS.
    • The connector normalizes sync dates to YYYY-MM-DD 00:00:00 before querying.
  • Fetch (pre.js):
    • SuiteQL query: SELECT * FROM customer WHERE externalid != '<NETSUITE_GUEST_CUSTOMER>'.
    • Delta sync adds: AND lastmodifieddate > TO_TIMESTAMP('<lastSyncDate>', 'YYYY-MM-DD HH24:MI:SS').
    • Ordered by lastmodifieddate ASC, id ASC.
    • Paginated in pages of 1000 (OFFSET ... ROWS FETCH NEXT 1000 ROWS ONLY).
    • Retries on HTTP 429 with linear backoff (max 3 retries, delay = 1000ms × attempt).
    • Uses Prefer: transient header.
    • Fetches customer addresses via NetSuite Record API in 3 steps:
      1. GET record/v1/customer/{id}/addressbook — retrieves the list of address entry IDs (via links[0].href).
      2. GET record/v1/customer/{id}/addressBook/{entryId} — retrieves each entry with defaultBilling, defaultShipping, and a link to addressBookAddress.
      3. GET record/v1/customer/{id}/addressBook/{entryId}/addressBookAddress — retrieves the full address fields (addr1, city, state, zip, country, addrPhone).
    • Address fetch errors for individual entries are logged and skipped without failing the sync.
  • Transform (transformer.js):
    • Maps to Commerce customer payload:
      • email, firstname (defaults to "Unknown"), lastname (defaults to "Unknown").
      • website_id, store_id, group_id from COMMERCE_DEFAULT_* or 1.
      • custom_attributes.netsuite_internal_id = <netsuite id>.
    • Filters out customers without email.
    • Transforms NetSuite address entries into Commerce addresses:
      • street from addr1, city, postcode from zip, country_id from country.id.
      • region from state (full name), region_id defaults to 0.
      • telephone from addrPhone, falls back to "00000000".
      • firstname/lastname from the parent Commerce customer.
      • default_billing and default_shipping flags preserved.
  • Sync (sender.js):
    • Searches Commerce customers by email using customers/search API.
    • Fetches NetSuite addresses via getNetSuiteCustomerAddresses (see pre.js) and includes them in the payload.
    • Updates existing customers with updateCustomer API.
    • Creates new customers with createCustomer API.
    • Returns { success, created, updated, failed, errors }.
    • Marks overall sync as failed if all customers failed.
  • State update (index.js):
    • Updates customer_sync_last_date with currentSyncDate on success.
    • Also updates when there are no customers to sync (checkpoint).
    • TTL: 365 days (31,536,000 seconds).

Prerequisites

Create App Builder Project

Go to the Adobe Developer Console:

  • Click Create project from template → select App Builder.
  • Choose a name and title; select or create a Stage workspace.
  • Add the following API services (select default OAuth Server-to-Server):
    • I/O Events
    • I/O Management
    • Adobe I/O Events for Adobe Commerce
    • Adobe Commerce as a Cloud Service (SaaS only)
  • Download the workspace configuration JSON and save it as workspace.json in ./scripts/onboarding/config/.

Install Commerce Eventing Module

Required only for Adobe Commerce 2.4.4 or 2.4.5. Follow the installation documentation.

Upgrading to Adobe I/O Events for Adobe Commerce module version 1.6.0 or greater enables additional automated onboarding steps.

Deploy & Onboard

1. Download and Configure

cp env.dist .env
# Fill in all required values (see Environment Variables below)
npm install

2. Connect to App Builder Workspace

aio login
aio console org select
aio console project select
aio console workspace select
aio app use  # Choose 'm' (merge)

3. Deploy

aio app deploy

Confirm the deployment in the Adobe Developer Console under the Runtime section of your workspace.

4. Onboard Events

Configure event providers and registrations:

npm run onboard

The script outputs provider IDs — save the commerce instance ID, provider ID, and backoffice provider ID for the next steps.

5. Subscribe to Commerce Events

If your Adobe I/O Events for Adobe Commerce module is version 1.6.0 or greater:

npm run commerce-event-subscribe

Otherwise, subscribe manually following the documentation.

Required event subscriptions:

Entity Event Required fields
Customer observer.customer_save_commit_after created_at, updated_at
Order observer.sales_order_save_commit_after created_at, updated_at

6. Configure Adobe Commerce Eventing

In Commerce Admin → Stores > Settings > Configuration > Adobe Services > Adobe I/O Events:

  • Set Adobe I/O Authorization Type to OAuth (Recommended).
  • Paste the workspace configuration JSON into Adobe I/O Workspace Configuration.
  • Enter the commerce provider instance ID and provider ID from the onboarding output.
  • Enable Commerce Eventing and ensure cron is running.

Configuration

Environment Variables

Copy env.dist to .env and fill in the values:

# NetSuite credentials
NETSUITE_ACCOUNT_ID=
NETSUITE_CONSUMER_KEY=
NETSUITE_CONSUMER_SECRET=
NETSUITE_TOKEN_ID=
NETSUITE_TOKEN_SECRET=

# Commerce API (PaaS - OAuth1)
COMMERCE_BASE_URL=https://[environment].magentosite.cloud/rest/
COMMERCE_CONSUMER_KEY=
COMMERCE_CONSUMER_SECRET=
COMMERCE_ACCESS_TOKEN=
COMMERCE_ACCESS_TOKEN_SECRET=

# Commerce API (SaaS - IMS) — use instead of OAuth1 for SaaS
# COMMERCE_BASE_URL=https://na1-sandbox.api.commerce.adobe.com/[tenant-id]/
# OAUTH_CLIENT_ID=
# OAUTH_CLIENT_SECRET=
# OAUTH_SCOPES=AdobeID, openid, read_organizations, ...

# Sync settings
NETSUITE_GUEST_CUSTOMER=
NETSUITE_DEFAULT_SUBSIDIARY_ID=1
NETSUITE_CUSTOMER_LAST_SYNC_DATE=        # Optional: YYYY-MM-DD
NETSUITE_CUSTOMER_CRON_SYNC_EXPRESSION=  # e.g. 0 * * * *

# Commerce defaults for NetSuite → Commerce customer creation
COMMERCE_DEFAULT_WEBSITE_ID=1
COMMERCE_DEFAULT_STORE_ID=1
COMMERCE_DEFAULT_GROUP_ID=1

# Observability
LOG_LEVEL=info
ENABLE_TELEMETRY=false

Authentication Modes

The connector supports both Commerce deployment types:

Mode Auth When to use
PaaS OAuth 1.0a Traditional Adobe Commerce (on-premise / cloud)
SaaS IMS OAuth Server-to-Server Adobe Commerce as a Cloud Service

OAuth1 is tried first. To use SaaS/IMS, leave COMMERCE_CONSUMER_KEY and related variables blank or unset.

PaaS — Configure Commerce Integration

In Commerce Admin → System > Extensions > Integrations:

  • Click Add New Integration, give it a name.
  • Under API, grant access to all resources.
  • Activate the integration and copy the consumer key, consumer secret, access token, and access token secret to .env.

SaaS — Configure IMS OAuth

Follow the OAuth Server-to-Server guide and add credentials to .env:

OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_SCOPES=AdobeID, openid, read_organizations, ...

NetSuite API Client

All NetSuite requests use the npm package netsuite-api-client. Use this client for any new NetSuite integrations rather than issuing raw HTTP requests.

Where to Find Configuration Values

Variable Location
NETSUITE_ACCOUNT_ID NetSuite Admin > Setup > Company > Company Information
NETSUITE_CONSUMER_KEY/SECRET NetSuite Admin > Setup > Integration > Manage Integrations
NETSUITE_TOKEN_ID/SECRET NetSuite Admin > Setup > Users/Roles > Access Tokens
COMMERCE_BASE_URL Commerce Admin URL + /rest/ (PaaS) or tenant API endpoint (SaaS)
OAUTH_CLIENT_ID/SECRET Adobe Developer Console > Project > OAuth Server-to-Server

Commerce Custom Attributes

The connector uses Custom Attributes to store NetSuite Internal IDs in Adobe Commerce entities.

Ensure a custom attribute named netsuite_internal_id exists for:

  • Customer entity (required): Used by customer sync in both directions.
  • Order entity (optional): Used by order post-process. If the attribute doesn't exist or update fails, order sync still succeeds.

These fields are written after a successful NetSuite upsert to support correlation and idempotency.

Commerce Data Requirements

Customer Sync (Commerce → NetSuite)

  • params.data.id: Commerce customer ID (required).
  • email: Customer email (required).
  • firstname, lastname: Customer name (required).
  • addresses: Array of customer addresses (optional, used for addressBook).
    • Each address: id, street, city, region, region_code, postcode, country_id, default_billing, default_shipping.

Order Sync (Commerce → NetSuite)

  • data.value.entity_id: Commerce order ID (required).
  • data.value.increment_id: Commerce order number (required).
  • data.value.items: Array of order items (required, non-empty).
    • Each item: sku, qty_ordered, price, name, product_type.
  • data.value.addresses: Array of order addresses (required).
    • Each address: address_type, street, city, region, region_code, postcode, country_id, firstname, lastname.
  • data.value.customer_is_guest: Boolean flag for guest orders.
  • data.value.customer_email: Customer email (required for guest orders).
  • data.value.customer_id: Customer ID (required for registered orders).
  • data.value.billing_address: Billing address (optional, can be in addresses array).
  • data.value.extension_attributes.shipping_assignments: Shipping assignments (optional, fallback for shipping address).

Customer Sync (NetSuite → Commerce)

  • NetSuite customer record: id, email, firstname, lastname, externalid, lastmodifieddate.
  • NetSuite address entry (per addressBook item): defaultBilling, defaultShipping.
  • NetSuite address sub-resource (addressBookAddress): addr1, city, state, zip, country.id, addrPhone.

Development

Local Testing

npm run app-dev

Running Tests

# Run all tests
npm test

# Run tests with coverage
npm test -- --coverage

# Run a specific test file
npm test -- test/actions/customer/commerce/sync/transformer.test.js

Logs

# View runtime logs
aio app logs

# View specific action logs
aio runtime activation logs <activation-id>

# List recent activations
aio runtime activation list

Telemetry

The connector uses @adobe/aio-lib-telemetry for OpenTelemetry instrumentation (traces, metrics, logs).

To enable telemetry:

  1. Set ENABLE_TELEMETRY=true in the action's inputs in app.config.yaml.
  2. Configure exporters in actions/telemetry.js.

For local development, spin up the included telemetry stack:

docker compose up

Adding a New Event

  1. Add the event to ./scripts/onboarding/config/events.json under the relevant entity and flow.
  2. Run npm run onboard.
  3. Create the action handler in actions/{entity}/{flow}/{operation}/index.js.
  4. Register it in actions/{entity}/{flow}/actions.config.yaml.
  5. Add a case to the consumer's switch statement in actions/{entity}/{flow}/consumer/index.js.
  6. Deploy: aio app deploy.

Troubleshooting

Issue Solution
NetSuite 401 Unauthorized Verify OAuth 1.0a credentials (NETSUITE_* vars) and token permissions
NetSuite 429 Too Many Requests Built-in retry with linear backoff (max 3 retries); reduce sync frequency if persistent
Customer not synced to NetSuite Check that netsuite_internal_id custom attribute exists on Customer entity
Order sync succeeds but no internal ID netsuite_internal_id attribute on Order entity is optional; check Commerce logs
Delta sync not running Verify NETSUITE_CUSTOMER_CRON_SYNC_EXPRESSION and cron trigger in app.config.yaml
Guest customer not excluded Ensure NETSUITE_GUEST_CUSTOMER matches the NetSuite external ID used for guests
Commerce auth errors For PaaS: check COMMERCE_* OAuth1 vars; for SaaS: check OAUTH_* IMS vars

Notes and Limitations

  • NetSuite customer address telephone fallback: If addrPhone is absent in NetSuite, telephone in Commerce is set to "00000000" (Commerce requires a non-empty value).
  • Customer and order upserts rely on NetSuite externalId: Ensure NetSuite allows externalId values for customer and salesOrder records.
  • Order line items exclude configurable parents: Only simple/virtual items with qty_ordered > 0 are synced.
  • Shipping address fallback: If a shipping address is not present, a billing address is used.
  • Delta sync state: The customer_sync_last_date state is updated even when no customers are synced to maintain the checkpoint.
  • Error handling: Order post-process does not fail the action if Commerce update fails; the order is already created in NetSuite.
  • Rate limiting: NetSuite API calls retry on HTTP 429 with linear backoff (max 3 retries).
  • Telemetry: Actions use OpenTelemetry instrumentation for observability (controlled by ENABLE_TELEMETRY).

References

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors