NetSuite Architect — SuiteScript 2.1, SuiteQL, Advanced Revenue Management, SuiteCommerce, Shopify Plus. Based in Bengaluru, India.
NetSuite Certified ERP Consultant #4936I design and build on NetSuite for companies whose ERP has outgrown its original configuration — usually somewhere between "the saved searches no longer answer the question" and "nobody trusts the numbers."
The work splits three ways: SuiteScript and SuiteQL development at volumes where governance limits and silent failures start to matter; revenue and receivables, including Advanced Revenue Management, ASC 606 and collections design; and commerce integration, mostly Shopify Plus to NetSuite order, inventory and fulfilment flows.
Fifteen years in ERP, and I take over stalled implementations as readily as I start new ones.
Figures are the public totals on my Upwork profile, where I hold Top Rated status.
Tested SuiteQL queries for open AR, AR aging, revenue by month, open purchase orders, inventory valuation and duplicate customer detection — each executed against a live NetSuite account before publishing, and each documenting the trap it avoids.
A Map/Reduce template whose summarize stage actually reports failures, a governance guard for bulk operations, and field notes on NetSuite's async entity deduplication engine.
Because of a prefixed status code.
Saved searches display status in a prefixed form — PurchOrd:B. The transaction.status column in SuiteQL stores the bare code — B. The prefixed form is a syntactically valid string comparison: it throws no error, logs no warning, and matches nothing.
-- Returns 0 rows, silently WHERE t.type = 'PurchOrd' AND t.status = 'PurchOrd:B' -- Returns your open POs WHERE t.type = 'PurchOrd' AND t.status = 'B'
This is dangerous precisely because zero rows looks like a legitimate business answer. "We have no open POs" is a sentence somebody will believe. Full write-up and the diagnostic query in GOTCHAS.md.
Because errors thrown in map() and reduce() do not fail the script. NetSuite catches them, records them against the key, and hands them to summarize(). If you never drain mapSummary.errors and reduceSummary.errors, those errors disappear — the deployment screen shows a completed run, and some fraction of your records were never processed.
Two details that catch people: .errors is an iterator, not an array, so .forEach() on it is a silent no-op; and the .each() callback must return true to continue, or you see the first failure and assume it was the only one.
Working template in suitescript-2.1-patterns.
Because in a bulk run, the async EntityDeduplicationTask engine does not reliably honour masterRecordId. Groups come back merged into a master that is not the id you supplied — in production I have seen a Lead record survive instead of the nominated Customer.
The master is the record that survives; everything else in the group is absorbed into it. So the surviving entity carries the wrong stage, and every downstream integration holding the Customer's internal id now points at an absorbed record. No error is raised.
Setting masterSelectionMode to SELECT_BY_ID is necessary but not sufficient — the behaviour above was observed with it set. Verify survivors after the run, and capture the intended mapping beforehand, because absorbed records are gone and the mapping cannot be reconstructed. Full notes and a safe four-step procedure in deduplication-notes.md.
I take consulting engagements and one-off consultations through Upwork, and I am reachable on LinkedIn.
Most useful first message: what you are trying to achieve, what you have already tried, and the actual error text, query, or script.