My App
Email Ingestion

NetSuite Integration

Automatically create NetSuite records from processed documents

NetSuite Integration

The Email Ingestion service can automatically create records in NetSuite from extracted document data.

Overview

When a document is processed, the system can:

  1. Resolve entities: Match extracted names to NetSuite records
  2. Map fields: Convert extracted data to NetSuite field format
  3. Create records: Generate vendor bills, invoices, POs, or expense reports
  4. Attach files: Upload the original document to File Cabinet
flowchart LR
    A[Extracted Data] --> B[Entity Resolution]
    B --> C[Field Mapping]
    C --> D[Record Creation]
    D --> E[File Attachment]
    E --> F[NetSuite Record]

Prerequisites

NetSuite Connection

Before using NetSuite integration:

  1. Connect your NetSuite account: Go to Settings > Integrations > NetSuite
  2. Grant API access: Ensure your NetSuite user has API permissions
  3. Configure subsidiary: Select default subsidiary for records
  4. Set default accounts: Configure expense accounts, etc.

Required Permissions

Your NetSuite integration user needs:

PermissionFor
Vendor BillsCreating vendor bills
InvoicesCreating customer invoices
Purchase OrdersCreating POs
Expense ReportsCreating expense reports
File CabinetUploading document attachments
SuiteQLEntity lookups

Entity Resolution

How It Works

The Entity Resolver matches extracted names to NetSuite internal IDs:

// Resolve a vendor by name
const result = await entityResolver.resolveVendor(
  "Acme Corporation",  // Extracted vendor name
  orgId,
  netsuiteConnectionId
);

// Result
{
  success: true,
  internalId: "12345",
  name: "Acme Corp",
  entityType: "vendor",
  confidence: 0.95,
  matchMethod: "fuzzy"  // 'exact' | 'fuzzy' | 'alias'
}

Match Methods

MethodDescriptionConfidence
exactPerfect string match1.0
fuzzyLevenshtein distance match0.7 - 0.99
aliasMatched via configured alias0.9
defaultFallback/default entity used0.5

Fuzzy Matching

The system uses Levenshtein distance for fuzzy matching:

// Extracted: "ACME CORP"
// NetSuite records:
// - "Acme Corporation" (similarity: 0.75)
// - "Acme Corp" (similarity: 0.95) <- Selected
// - "Acme Inc" (similarity: 0.60)

String normalization:

  • Lowercase conversion
  • Removal of special characters
  • Whitespace normalization
  • Common abbreviation expansion (Corp -> Corporation)

Configuring Aliases

Set up vendor/customer aliases for better matching:

// In NetSuite or via API
{
  entityType: "vendor",
  internalId: "12345",
  aliases: [
    "ACME",
    "ACME CORPORATION",
    "ACME CORP.",
    "Acme Ltd"
  ]
}

Entity Types

TypeNetSuite RecordUsed For
vendorVendorVendor bills
customerCustomerCustomer invoices
itemItemLine items
employeeEmployeeExpense reports
accountAccountExpense categories

Record Creation

Vendor Bill

Created from supplier invoices:

const result = await recordCreation.createVendorBill({
  vendorId: "12345",           // Resolved vendor internal ID
  tranDate: "2024-11-23",      // Invoice date
  dueDate: "2024-12-23",       // Payment due date
  tranId: "INV-001",           // External reference
  memo: "From email ingestion",

  lineItems: [
    {
      item: "54321",           // Resolved item internal ID
      description: "Consulting Services",
      quantity: 10,
      rate: 150.00,
      amount: 1500.00,
      department: "100",       // Optional
      class: "200"             // Optional
    }
  ],

  // Original document
  attachmentFileId: "doc_abc123"
}, {
  connectionId: "conn_xyz",
  isDraft: false,              // Create as pending approval
  uploadToFileCabinet: true
});

Response:

{
  success: true,
  recordType: "vendorbill",
  internalId: "98765",
  externalId: "adteco-inv-001",
  transactionNumber: "BILL-0042",
  fileCabinetId: "file_123"
}

Customer Invoice

Created from outbound invoices:

const result = await recordCreation.createCustomerInvoice({
  customerId: "67890",
  tranDate: "2024-11-23",
  tranId: "INV-2024-001",

  lineItems: [
    {
      item: "54321",
      description: "Software License",
      quantity: 1,
      rate: 5000.00,
      amount: 5000.00
    }
  ],

  terms: "30",  // Net 30
  memo: "Annual license renewal"
}, {
  connectionId: "conn_xyz",
  isDraft: true
});

Purchase Order

Created from PO documents:

const result = await recordCreation.createPurchaseOrder({
  vendorId: "12345",
  tranDate: "2024-11-23",
  tranId: "PO-2024-001",
  expectedReceiptDate: "2024-12-01",
  shipTo: "Main Warehouse",

  lineItems: [
    {
      item: "11111",
      description: "Office Supplies",
      quantity: 100,
      rate: 5.00,
      amount: 500.00
    }
  ]
}, {
  connectionId: "conn_xyz"
});

Expense Report

Created from expense receipts:

const result = await recordCreation.createExpenseReport({
  employeeId: "999",
  tranDate: "2024-11-23",
  memo: "Business trip to NYC",

  expenseItems: [
    {
      date: "2024-11-20",
      category: "travel",
      account: "6010",        // Travel expense account
      amount: 450.00,
      memo: "Flight LAX-JFK",
      isBillable: false,
      attachmentId: "file_001"
    },
    {
      date: "2024-11-21",
      category: "meals",
      account: "6020",
      amount: 75.00,
      memo: "Client dinner"
    }
  ]
}, {
  connectionId: "conn_xyz",
  isDraft: false
});

Field Mapping

Default Mappings

Extracted fields are automatically mapped:

Extracted FieldNetSuite FieldRecord Types
vendorNameentity (resolved)Vendor Bill, PO
customerNameentity (resolved)Invoice
invoiceNumbertranidAll
invoiceDatetrandateAll
dueDateduedateVendor Bill, Invoice
totaltotal (calculated)All
lineItems[].descriptionitem.descriptionAll
lineItems[].quantityitem.quantityAll
lineItems[].rateitem.rateAll

Custom Field Mapping

Configure custom field mappings:

{
  netsuiteConnectionId: "conn_xyz",
  customMappings: [
    {
      extractedField: "projectCode",
      netsuiteField: "custbody_project",
      recordTypes: ["vendorbill", "purchaseorder"]
    },
    {
      extractedField: "costCenter",
      netsuiteField: "class",
      transform: "lookup",  // Lookup internal ID
      recordTypes: ["vendorbill"]
    }
  ]
}

Value Transformations

TransformDescriptionExample
directCopy value directlyName -> Name
lookupFind NetSuite internal ID"Sales" -> "12"
formatApply format pattern"2024-11-23" -> "11/23/2024"
calculateCompute from other fieldsquantity * rate
defaultUse default if emptynull -> "1"

File Cabinet Integration

Uploading Documents

Original documents are uploaded to NetSuite File Cabinet:

// Configuration
{
  fileCabinetFolder: "/Documents/Invoices",  // Target folder
  filenamePattern: "{docType}_{vendorName}_{date}_{id}",
  allowedTypes: ["application/pdf", "image/png", "image/jpeg"]
}

Generated filename: VendorBill_AcmeCorp_2024-11-23_inv001.pdf

Attaching to Records

Documents are automatically attached to created records:

// After record creation
{
  recordType: "vendorbill",
  internalId: "98765",
  attachments: [
    {
      fileId: "file_123",
      filename: "invoice.pdf",
      fileCabinetPath: "/Documents/Invoices/2024/11/"
    }
  ]
}

Error Handling

Entity Not Found

When a vendor/customer/item cannot be resolved:

{
  success: false,
  error: {
    code: "ENTITY_NOT_FOUND",
    message: "Vendor 'Unknown Corp' not found in NetSuite",
    entityType: "vendor",
    searchTerm: "Unknown Corp",
    suggestions: [
      { internalId: "123", name: "Unknown Corporation", confidence: 0.85 },
      { internalId: "456", name: "Unknowncorp LLC", confidence: 0.72 }
    ]
  },
  action: "REQUIRES_REVIEW"
}

Resolution options:

  1. Select from suggestions
  2. Create new entity
  3. Map to existing entity
  4. Skip this field

Validation Errors

NetSuite field validation failures:

{
  success: false,
  error: {
    code: "VALIDATION_ERROR",
    message: "Required field 'account' missing on line 2",
    field: "lineItems[1].account",
    recordType: "vendorbill"
  }
}

Connection Errors

Authentication or API issues:

{
  success: false,
  error: {
    code: "CONNECTION_ERROR",
    message: "NetSuite authentication failed",
    details: "Invalid credentials or token expired"
  },
  action: "RECONNECT_REQUIRED"
}

Approval Workflows

Draft Mode

Create records as drafts for review:

const result = await recordCreation.createVendorBill(data, {
  connectionId: "conn_xyz",
  isDraft: true  // Creates in draft status
});

Pending Approval

Integrate with NetSuite approval workflows:

{
  // Configuration
  approvalSettings: {
    vendorBill: {
      // Auto-approve under $1000
      autoApproveThreshold: 1000,
      // Require approval for specific vendors
      alwaysRequireApproval: ["vendor_789"],
      // Default approver
      defaultApprover: "employee_123"
    }
  }
}

Monitoring & Reporting

Sync Status

Track NetSuite sync status:

StatusDescription
pendingWaiting to sync
syncingIn progress
syncedSuccessfully created
failedSync failed
reviewNeeds manual review

Sync History

View sync history for each document:

GET /api/documents/{id}/netsuite-sync

{
  documentId: "doc_123",
  syncAttempts: [
    {
      timestamp: "2024-11-23T10:30:00Z",
      status: "failed",
      error: "Vendor not found"
    },
    {
      timestamp: "2024-11-23T10:35:00Z",
      status: "synced",
      netsuiteId: "98765",
      recordType: "vendorbill"
    }
  ]
}

Retry Failed Syncs

Retry failed NetSuite syncs:

POST /api/documents/{id}/netsuite-sync/retry

{
  // Optional: override entity mappings
  overrides: {
    vendorId: "12345"  // Force specific vendor
  }
}

Best Practices

Entity Management

  1. Maintain NetSuite data: Keep vendor/customer names consistent
  2. Use aliases: Configure aliases for known variations
  3. Regular cleanup: Merge duplicate entities

Error Prevention

  1. Required fields: Ensure all required NetSuite fields are mapped
  2. Test connections: Regularly test NetSuite connectivity
  3. Monitor failures: Set up alerts for sync failures

Performance

  1. Batch processing: Large documents processed off-peak
  2. Rate limiting: Respect NetSuite API limits
  3. Caching: Entity lookups are cached