Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

The NetSuite Pro

The NetSuite Pro Logo The NetSuite Pro Logo

The NetSuite Pro Navigation

  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask A Question
  • Home
  • About Us
  • Tutorials
    • NetSuite Scripting
    • NetSuite Customization
    • NetSuite Integration
    • NetSuite Advanced PDF Templates
    • NetSuite Reporting & Analytics Guide
    • Real-World NetSuite Examples
  • Blog
  • Contact Us
Home/ Real-World NetSuite Examples/Automating Customer Credit Hold with SuiteScript

Automating Customer Credit Hold with SuiteScript

The business problem

AR teams want orders blocked (or flagged) when a customer:

  1. exceeds their credit limit, and/or
  2. has overdue AR beyond a threshold.

Doing this by hand is risky and slow. We’ll automate it with SuiteScript and keep it safe for high volumes.


The approach (2 layers)

Layer A — Nightly enforcement (Map/Reduce):

  • Input = a saved search that returns Customer internal IDs who violate your policy (over-limit and/or overdue).
  • Script sets creditholdoverride = ON.
  • Optionally auto-releases customers who are now compliant by setting creditholdoverride = AUTO.

Layer B — Real-time guard (User Event on Sales Order):

  • If the selected customer is on credit hold, we prevent the order (or mark it On Hold) and show a friendly error.

Why two layers? Nightly keeps data clean at scale; real-time prevents edge-case orders in the daytime before the nightly job runs.


Configuration you’ll set once

1) Saved Search (Transactions → Invoices → summary by Customer)

Type: Transaction
Criteria (suggested):

  • Type = Invoice
  • Status = Open
  • Main Line = false
  • Due Date = on or before “days ago” (parameterized number, e.g., 15/30)

Results (Summary):

  • Group by Customer (Entity / Internal ID as one column too)
  • Sum of Amount Remaining
  • (Optional) Summary Filter: Sum(Amount Remaining) greater than some threshold (e.g., 100)

Name it something like Credit Hold Candidates (Overdue) and note its Internal ID, e.g., customsearch_credit_hold_candidates.
You can also create a separate Customer search for “over credit limit” using Formula (Numeric): CASE WHEN {balance} > {creditlimit} THEN 1 ELSE 0 END with Summary filter > 0, or simply combine logic in the script.


Script A — Map/Reduce (SuiteScript 2.1)

What it does

  • Reads parameters (search IDs, thresholds, auto-release, etc.)
  • Iterates customers to apply credit hold
  • Optionally scans a “compliant customers” search to release holds back to AUTO
  • Uses submitFields (fast, governance-friendly)

Create script parameters (Deployment level) so Finance can tune behavior without code changes.

Parameters (suggested)

  • custscript_hold_search_id (Free-Form Text): Internal ID of “candidates to hold” saved search (Transaction summary).
  • custscript_release_search_id (Free-Form Text, optional): Internal ID of “candidates to release” (Customer search where balance ≤ creditlimit and overdue ≤ threshold).
  • custscript_auto_release (Checkbox): Auto release compliant customers? (T/F)
  • custscript_reason_text (Free-Form Text): Default hold reason text (stored on a custom entity field, optional).
  • custscript_page_size (Integer): Default 100–1000 (governance + throughput).
  • custscript_dry_run (Checkbox): Log only; don’t update.
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 * @author The NetSuite Pro
 * Title: MR | Enforce Customer Credit Hold
 */
define(['N/search','N/record','N/runtime','N/log'], (search, record, runtime, log) => {

  const F_CREDITHOLD = 'creditholdoverride'; // values: 'AUTO','ON','OFF'
  const F_REASON = 'custentity_credit_hold_reason'; // optional custom entity field

  const getParams = () => {
    const s = runtime.getCurrentScript();
    return {
      holdSearchId:      (s.getParameter('custscript_hold_search_id') || '').trim(),
      releaseSearchId:   (s.getParameter('custscript_release_search_id') || '').trim(),
      autoRelease:       s.getParameter('custscript_auto_release') === 'T',
      reasonText:        s.getParameter('custscript_reason_text') || 'Over limit or overdue AR',
      pageSize:          parseInt(s.getParameter('custscript_page_size') || '500', 10),
      dryRun:            s.getParameter('custscript_dry_run') === 'T'
    };
  };

  const searchToCustomerIds = (searchId, pageSize) => {
    if (!searchId) return [];
    const srch = search.load({ id: searchId });
    const paged = srch.runPaged({ pageSize: Math.min(Math.max(pageSize, 100), 1000) });
    const ids = [];
    paged.pageRanges.forEach(range => {
      const page = paged.fetch({ index: range.index });
      page.data.forEach(r => {
        // Try to derive a customer internal ID from summary grouping or explicit field:
        let id = null;
        const entity = r.getValue({ name: 'entity', summary: search.Summary.GROUP });
        const internalid = r.getValue({ name: 'internalid', summary: search.Summary.GROUP }) ||
                           r.getValue({ name: 'internalid' });
        id = internalid || entity;
        if (id) ids.push(String(id));
      });
    });
    return [...new Set(ids)];
  };

  const putOnHold = (customerId, reasonText, dryRun) => {
    if (dryRun) {
      log.audit('DRY-RUN putOnHold', { customerId, reasonText });
      return;
    }
    record.submitFields({
      type: record.Type.CUSTOMER,
      id: customerId,
      values: {
        [F_CREDITHOLD]: 'ON',
        ...(F_REASON ? { [F_REASON]: reasonText } : {})
      },
      options: { enableSourcing: false, ignoreMandatoryFields: true }
    });
  };

  const releaseHold = (customerId, dryRun) => {
    if (dryRun) {
      log.audit('DRY-RUN releaseHold', { customerId });
      return;
    }
    record.submitFields({
      type: record.Type.CUSTOMER,
      id: customerId,
      values: {
        [F_CREDITHOLD]: 'AUTO'
      },
      options: { enableSourcing: false, ignoreMandatoryFields: true }
    });
  };

  const input = () => {
    const p = getParams();
    const holdIds = searchToCustomerIds(p.holdSearchId, p.pageSize);
    const releaseIds = p.autoRelease ? searchToCustomerIds(p.releaseSearchId, p.pageSize) : [];
    return { params: p, holdIds, releaseIds };
  };

  const getInputData = () => {
    const { params, holdIds, releaseIds } = input();
    // Build a single stream with type flags:
    const rows = [];
    holdIds.forEach(id => rows.push({ action: 'HOLD', id }));
    releaseIds.forEach(id => rows.push({ action: 'RELEASE', id }));
    return rows;
  };

  const map = (ctx) => {
    const row = JSON.parse(ctx.value);
    ctx.write({ key: row.action, value: row.id });
  };

  const reduce = (ctx) => {
    const p = getParams();
    const action = ctx.key; // HOLD or RELEASE
    const ids = ctx.values || [];
    ids.forEach(customerId => {
      try {
        if (action === 'HOLD') putOnHold(customerId, p.reasonText, p.dryRun);
        else if (action === 'RELEASE') releaseHold(customerId, p.dryRun);
      } catch (e) {
        log.error(`Failed ${action} for ${customerId}`, e);
      }
    });
  };

  const summarize = (summary) => {
    log.audit('Credit Hold MR Summary', {
      usage: summary.usage,
      concurrency: summary.concurrency,
      yields: summary.yields
    });
    summary.inputSummary.error && log.error('Input Error', summary.inputSummary.error);
    summary.mapSummary.errors.iterator().each((k, e) => (log.error(`Map Err ${k}`, e), true));
    summary.reduceSummary.errors.iterator().each((k, e) => (log.error(`Reduce Err ${k}`, e), true));
  };

  return { getInputData, map, reduce, summarize };
});

Deployment tips

  • Schedule nightly (e.g., 1:00 AM) and optionally hourly during busy season.
  • Set pageSize = 500 (start safe), then tune.
  • Start with dry_run = T once to validate logs.

Script B — Sales Order User Event (guardrail)

What it does

  • On beforeSubmit of Sales Order: if the customer’s creditholdoverride is ON, either:
    • Throw an error to block creation, or
    • Set the SO field orderstatus/onhold or a custom approval field to stop downstream processing.

Choose one approach below. Blocking with an error is crystal-clear to users.

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * Title: UE | Block SO if Customer on Credit Hold
 */
define(['N/record','N/error'], (record, error) => {

  const BLOCK_WITH_ERROR = true; // Set false if you prefer to just flag the order.

  const beforeSubmit = (ctx) => {
    if (ctx.type !== ctx.UserEventType.CREATE && ctx.type !== ctx.UserEventType.COPY && ctx.type !== ctx.UserEventType.EDIT) return;

    const so = ctx.newRecord;
    const customerHold = so.getValue({ fieldId: 'entitycredithold' })   // read-only mirror on SO
                       || so.getValue({ fieldId: 'creditholdoverride' }); // fallback if available

    // In many accounts, SO exposes 'entitycredithold' (T when hold = ON).
    const isOnHold = (String(customerHold).toUpperCase() === 'ON' || customerHold === true);

    if (!isOnHold) return;

    if (BLOCK_WITH_ERROR) {
      throw error.create({
        name: 'CUSTOMER_ON_CREDIT_HOLD',
        message: 'This customer is on Credit Hold. Please contact Accounts Receivable.',
        notifyOff: false
      });
    } else {
      // Soft-guard example:
      // so.setValue({ fieldId: 'onhold', value: true }); // if your account uses this field
      // or set a custom approval field:
      // so.setValue({ fieldId: 'custbody_needs_finance_review', value: true });
    }
  };

  return { beforeSubmit };
});

If your account doesn’t expose entitycredithold on SO, you can look it up via N/search on the Customer by internalid = entity.


Optional: custom “Reason” field (nice to have)

Create a Customer field:

  • ID: custentity_credit_hold_reason (type: Free-Form Text, display only)
  • Our MR sets this with a friendly explanation (e.g., “Over limit or overdue AR”).

Rollback / release logic

  • Add a second saved search (Customer type) for compliant customers:
    • Formula (Numeric): CASE WHEN {balance} <= {creditlimit} THEN 1 ELSE 0 END (Summary > 0)
    • Also ensure overdue sum ≤ threshold (via a joined transaction summary, or simply use another transaction search and merge results—light customization acceptable).
  • Point custscript_release_search_id to it and enable custscript_auto_release = T.

Testing checklist (quick)

  1. Turn DRY RUN = T and execute MR → confirm logs list intended customers.
  2. Turn DRY RUN = F, run MR → inspect a few customers (Credit Hold = ON, reason set).
  3. Try to create a Sales Order for a held customer → blocked (or flagged).
  4. Receive payments to clear balance/overdue → run MR with auto release → hold returns to AUTO.

Governance & performance notes

  • submitFields avoids full record load → minimal governance.
  • Use paged results and a tunable page size.
  • Map/Reduce keeps you safe for thousands of customers.
  • Start with nightly schedule; if AR is volatile, consider every 2–4 hours.

Troubleshooting (common)

  • Saved search returns no rows: loosen criteria; verify Main Line = false for invoice lines; confirm overdue date filter.
  • Not finding customer ID in results: add Internal ID (Group) as a results column.
  • Orders still sneaking through: ensure the UE is deployed on CREATE/COPY/EDIT and that no workflow or other script flips onhold or bypasses errors.

FAQs

Q: Why set creditholdoverride and not credithold?
creditholdoverride is the supported field to programmatically force hold (ON), release (OFF), or revert to system (AUTO).

Q: Can we base hold on % over limit?
Yes—add a Customer search returning {balance} and {creditlimit}; filter with a formula like {balance} > ({creditlimit} * 1.10) for 110%.

Q: Can Finance temporarily allow one order?
Yes—flip the customer to OFF (or add a role-based bypass in the UE via runtime.getCurrentUser().role).


What you can copy next

  • Paste the two scripts above as:
    • customscript_mr_credit_hold (Map/Reduce)
    • customscript_ue_so_credit_guard (User Event on Sales Order)
  • Create the two saved searches and set parameters.
  • Deploy, run in dry run, then go live.

If you want, I can tailor the saved-search formulas to your exact policy (e.g., “over 30 days overdue OR over 120% of credit limit”) and wire them into the MR so Finance can tweak thresholds in parameters instead of editing searches.

Share
  • Facebook

Leave a ReplyCancel reply

Sidebar

Ask A Question

Stats

  • Questions 6
  • Answers 6
  • Best Answers 0
  • Users 2
  • Popular
  • Answers
  • Rocky

    Issue in running a client script in NetSuite SuiteScript 2.0 ...

    • 1 Answer
  • admin

    How can I send an email with an attachment in ...

    • 1 Answer
  • admin

    How do I avoid SSS_USAGE_LIMIT_EXCEEDED in a Map/Reduce script?

    • 1 Answer
  • admin
    admin added an answer The issue is usually caused by following Wrong script file… September 14, 2025 at 10:33 pm
  • admin
    admin added an answer Steps to send an Invoice PDF by email: define(['N/email', 'N/render',… August 28, 2025 at 3:05 am
  • admin
    admin added an answer This error means your script hit NetSuite’s governance usage limit… August 28, 2025 at 3:02 am

Top Members

Rocky

Rocky

  • 1 Question
  • 22 Points
Begginer
admin

admin

  • 5 Questions
  • 2 Points

Trending Tags

clientscript netsuite scripting suitescript

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

© 2025 The NetSuite Pro. All Rights Reserved