Skip to content

Accounting: NetSuite ledger sync (SuiteTalk REST) #1735

Description

@Dhirenderchoudhary

Is your proposal related to a problem?

Carbon already syncs to Xero, QuickBooks Online, and Rillet. If the ledger the company actually closes is NetSuite, there is nothing to connect.

I checked the repo. NetSuite only shows up as a comparison for period close and for AR/AP settlements. There is no client, no provider, and no settings page.

Sage Intacct is on the integrations list with active: false and no settings. That is just a logo. This is not a request for another one of those.

Describe the solution you'd like

Add NetSuite as another accounting provider, same job as Rillet: Carbon pushes the accounting documents, NetSuite keeps the books.

Once it is connected, Carbon should push customers, vendors, items, sales invoices, bills, and the journals it posts automatically. A payment recorded in NetSuite should come back and settle the matching invoice or bill in Carbon. Journals someone posts by hand in Carbon should stay in Carbon.

I would land it in pieces:

  1. Log into a NetSuite sandbox and list the chart of accounts.
  2. Add the provider and the settings page, so account mapping shows those accounts.
  3. Push journals. Set NetSuite externalId from our mapping so a retry updates the same journal instead of creating another one. Failures show in Sync Activity.
  4. Customers, vendors, items, invoices, and bills.
  5. Pull payments back.

Use SuiteTalk REST. The host is https://{accountId}.suitetalk.api.netsuite.com. A sandbox is a different account id, not a different product.

Auth: OAuth 2.0 with a certificate if the account has it. If not, token-based auth (account id, consumer key, consumer secret, token id, token secret). Neither one is the Xero refresh-token flow or the Rillet API key. providerMetadata already holds Xero's tenantId and Rillet's subsidiaryId, so the NetSuite account id and token fields can go there too.

NetSuite does not have a normal webhook for this. Poll lastmodifieddate with SuiteQL. That matches the pull sweep the other providers already use. A SuiteScript script that posts to Carbon can come later if we want it faster.

Requests have to stay under that account's concurrency cap. A bad TBA token comes back as 401 and there is nothing to refresh. Treat it as an auth failure, same as a revoked key on the other integrations.

Roughly where the code goes:

  • ProviderID in packages/ee/src/accounting/core/models.ts (SAGE is already commented out there)
  • packages/ee/src/accounting/providers/netsuite/ for the provider and the syncers
  • packages/ee/src/accounting/core/subscriptions.ts for the table subscriptions
  • packages/ee/src/netsuite/config.tsx, and register it next to Rillet in packages/ee/src/index.ts
  • apps/erp/app/routes/x+/settings+/integrations.$id.tsx already branches on xero, quickbooks, and rillet
  • the same three ids are hardcoded on the accounting layout and in ACCOUNTING_SYNC_INTEGRATION_IDS in apps/erp/app/modules/accounting/accounting.service.ts
  • docs/content/docs/integrations/accounting.mdx

Inventory, manufacturing, and the warehouse stay in Carbon. This only mirrors the ledger.

Describe alternatives you've considered

Leaving a disabled card in the list, like Sage. It does not post anything, so it is not worth adding.

Syncing inventory and work orders both ways. Carbon is already the system running those. NetSuite only needs the accounting result.

Putting Celigo or another middleware in the middle. That can work for one company, but then this install does not get account mapping, Sync Activity, or the tie-out report.

SOAP SuiteTalk. REST plus SuiteQL covers the records above.

Additional context

Current providers:

export enum ProviderID {
  XERO = "xero",
  QUICKBOOKS = "quickbooks",
  RILLET = "rillet"
  // SAGE = "sage",
}

AccountingProvider is XeroProvider | QboProvider | RilletProvider. Each one extends BaseProvider in packages/ee/src/accounting/core/types.ts and registers its syncers. Rillet is the one to copy: packages/ee/src/accounting/providers/rillet/ and packages/ee/src/rillet/config.tsx. It is REST, it has an optional subsidiary, it pushes journals, and it pulls payments.

Things that are different on NetSuite:

  • Almost every reference is an internal id. An item is not one record type. Inventory, non-inventory, and service items are separate. Invoice tax is SuiteTax or the legacy tax codes, and the payload is not the same.
  • A journal line needs a subsidiary, a currency, and the account's internal id. Department, class, location, and custom segments are the dimensions. That is more than the two slots Xero and QuickBooks have, so maxJournalDimensionSlots should be left unset. Rillet is the closer example.
  • externalId on the NetSuite record is what we already store in externalIntegrationMapping.
  • Concurrency limits are per account, not per integration app.

Done for the first piece (login and accounts only):

  • A company can save NetSuite credentials and Carbon can tell whether they work against a sandbox.
  • Account mapping lists that chart of accounts.
  • A bad or revoked token shows up as an auth failure.
  • No journals or invoices go out yet.

I can take this after the needs-approval label is removed. I have a NetSuite sandbox for the first piece.

Requirement/Document

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions