QuickBooks Connector

Technical Documentation - QuickBooks Automation Service


1. Overview

The QuickBooks Automation Service is the entry-point layer for all Salesforce Flow / Process Builder-triggered synchronisation with QuickBooks Online (QBO). It exposes a single Invocable Method (callQBService) that Salesforce Automations can call to push records — Customers, Products, or Invoices — into QuickBooks without requiring custom code from the admin configuring the flow.

The service intelligently routes work between:

  • Synchronous bulk processing (multiple records → staged in Salesforce with Process_via_Batch__c = true for batch pickup)

  • Asynchronous real-time processing (single record → enqueued via QueueableAutomateQBService for an immediate API callout to QBO)


2. Component Architecture

Salesforce Flow / Process Builder
          │
          ▼
┌─────────────────────────────────┐
│  InvocableAutomateQBService     │  ← Global Apex Class (Invocable)
│                                 │
│  callQBService()                │  → Customer / Product
│  createQBInvoiceFromMapping()   │  → Invoice (primary path)
│  QBInvoiceCallout()             │  → Invoice (bulk path)
│  createJournalLedgerRecords()   │  → Journal Entry
│  createErrorRecords()           │  → Failure logging
└────────────┬────────────────────┘
             │ single record
             ▼
┌─────────────────────────────────┐
│  QueueableAutomateQBService     │  ← Queueable + AllowsCallouts
│                                 │
│  execute()                      │
│  createQBCustomer()             │  → HTTP POST to QBO API
│  createQBInvoice()              │  → HTTP POST to QBO API
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  WS_Quickbooks                  │  ← OAuth + QBO REST API
└─────────────────────────────────┘

Supporting Classes:
  CMQBConnectorUtility   → FLS, request/record factory methods
  QBApiDataWrapper       → Request/Response wrapper models
  Data                   → CRUD + FLS enforced DML
  QuickbooksController   → Customer/Product ID resolution


3. Class Reference

3.1 InvocableAutomateQBService

File: force-app/main/default/classes/InvocableAutomateQBService.cls Access Modifier: global with sharing

The primary service class. Contains the @InvocableMethod entry point and all orchestration logic.

Property

Value

Sharing Model

with sharing

Access

global

FLS Enforcement

Dynamic — reads from CMQBConnectorUtility.enforceFLS


3.1.1 Method: callQBService

@InvocableMethod(label='Call QB Service' description='This function is responsible to call QB Web Service.')
global static void callQBService(List<QBWrapper> listRecords)

Purpose: Primary invocable entry point for Customer and Product synchronisation. Called directly from Salesforce Flows.

Routing Logic:

Condition

Behaviour

listRecords.size() == 1 AND requestType != 'Product'

Enqueues QueueableAutomateQBService for real-time API callout

listRecords.size() > 1 or requestType == 'Product'

Bulk path: builds staging records with Process_via_Batch__c = true

companyRecordId is blank

Calls createErrorRecords()

No custom field mappings found

Calls createErrorRecords()

Bulk Processing Flow (Customer):

  1. Resolves the default or specified QuickBooks Company and Realm ID.

  2. Queries cm_finance__Custom_Field_Mapping__c for the sObject's field-to-QB mapping.

  3. Dynamically queries the source sObject for all mapped fields.

  4. Calls QuickbooksController.getRelatedQuickbooksCustomers() to detect if a QB Customer record already exists (update vs. create decision).

  5. Builds QBApiDataWrapper.QBRequestWrapper per record and calls WS_Quickbooks.createCustomerResponseWrapper().

  6. Creates or upserts cm_finance__Quickbooks_Customer__c records using Composite_Unique_Key__c (QB_Id::Customer::RealmId) as the upsert key.

  7. Sets Process_via_Batch__c = true and Ignore_Webhook__c = true on all staged records.

Bulk Processing Flow (Product):

  1. Same company/mapping resolution as Customer.

  2. Resolves optional Account QB IDs for Income, Expense, and Asset account lookups.

  3. Builds product response wrappers and maps SF account record IDs → QB account IDs.

  4. Creates or upserts cm_finance__Quickbooks_Product__c records using Composite_Unique_Key__c (QB_Id::Product::RealmId).


3.1.2 Method: createQBInvoiceFromMapping (Currently Active)

global static void createQBInvoiceFromMapping(List<QBWrapper> listRecords)

Purpose: Primary invoice creation path. Uses cm_finance__Custom_Field_Mapping__c configuration to dynamically build an invoice from any source sObject. This is the recommended and currently active method for invoice automation.

Prerequisites (must be configured in cm_finance__Custom_Field_Mapping__c):

Mapping Field

Purpose

cm_finance__Field_Mapping__c

JSON: QB invoice field → SF source field

cm_finance__Source_Invoice_Customer_Lookup_field__c

API name of the SF field holding the Customer record ID

cm_finance__Invoice_Product_Pre_Population_Mapping__c

JSON configuration for child line-item records

cm_finance__QB_Custom_Fields_Mapping__c

JSON: QB custom field Definition ID → SF field

Processing Flow:

1. Resolve QB company from QBWrapper.oRequestWrapper.companyId
2. Query Custom_Field_Mapping__c for Mapping_Type__c = 'Invoice' and matching sObject
3. Extract all required SF field names from all mapping configurations
4. Query source sObject for all fields in one SOQL
5. For each record:
   a. Build QBApiDataWrapper.Invoice via populateField() from cm_finance__Field_Mapping__c
   b. Resolve QB Customer ID via cm_finance__Source_Invoice_Customer_Lookup_field__c
      → If no QB Customer found for 1 record: auto-trigger callQBService() to create it first
      → If no QB Customer found for >1 record: log Failed invoice record
   c. Build Line Items from child sObjects via Invoice_Product_Pre_Population_Mapping__c
      → Resolves QB Product IDs from cm_finance__Quickbooks_Product__c
      → Resolves QB Class IDs from cm_finance__Quickbooks_Class__c
   d. Populate QB Custom Fields from cm_finance__QB_Custom_Fields_Mapping__c
6. Call QBInvoiceCallout() with all valid requests
7. Any pre-validation failures → insert Failed cm_finance__Quickbooks_Invoice__c records immediately

Line Item Mapping Configuration (Invoice_Product_Pre_Population_Mapping__c JSON structure):

{
  "query": "SELECT Id, {0}, ProductId__c, UnitPrice__c, Quantity__c, Description__c, QBClass__c FROM OpportunityLineItem WHERE {0} IN :parentRecordId",
  "lookupFieldName": "OpportunityId",
  "fieldMapping": {
    "sfProductId": "ProductId__c",
    "ProductQuantity": "Quantity__c",
    "ProductUnitPrice": "UnitPrice__c",
    "ProductDescription": "Description__c",
    "QBClassLookup": "QBClass__c"
  }
}

Key

Description

query

SOQL template for fetching child records. Use {0} as placeholder for the parent lookup field name.

lookupFieldName

Child object field that holds the parent record ID

sfProductId

Child field containing the Salesforce Product record ID

ProductQuantity

Child field for line item quantity

ProductUnitPrice

Child field for unit price

ProductDescription

Child field for line item description

QBClassLookup

Child field holding QB Class lookup ID

Failure Modes & Error Records Created:

Scenario

Error Message on cm_finance__Quickbooks_Invoice__c

QB Company not provided

Quickbooks Company not found on Request wrapper

No Custom Field Mapping found

Custom Field Mapping not found for current object

No invoice line items resolved

Invoice Line Items not found on source object

Customer lookup field empty

Customer reference field not populated on source object

QB Customer not found (>1 record)

Quickbooks Customer not found on source object


3.1.3 Method: QBInvoiceCallout

global static void QBInvoiceCallout(List<QBWrapper> listRecords)

Purpose: Second-stage invoice processor. Accepts pre-built QBApiDataWrapper.Invoice objects inside QBWrapper.oRequestWrapper and stages them for QB sync.

Routing:

Condition

Behaviour

listRecords.size() == 1

Sets requestType = 'Invoice' and enqueues QueueableAutomateQBService for real-time API callout

listRecords.size() > 1

Bulk path — builds and inserts staged cm_finance__Quickbooks_Invoice__c and cm_finance__Quickbooks_Invoice_Item__c records

Bulk Lookup Resolution:

The bulk path resolves all lookups in-memory before DML, performing 4 SOQL queries against:

  1. cm_finance__Quickbooks_Customer__c → maps QB_Id::CompanyRecordId → SF Customer Record Id

  2. cm_finance__Quickbooks_Product__c → maps QB_Id::CompanyRecordId → SF Product Record Id

  3. cm_finance__Quickbooks_Class__c → maps QB_Id::CompanyRecordId → SF Class Record Id

  4. cm_finance__Quickbooks_Custom_Field__c → maps DefinitionId~CompanyRecordId → SF Custom Field Record Id

Custom Field Encoding: Custom field values are temporarily stored in cm_finance__Custom_Field_1__c / _2__c / _3__c using the format DefinitionId::FieldValue. The method splits this, resolves the SF Custom Field record ID, and writes the final value.


3.1.4 Method: createQBSalesReceiptFromMapping (Currently Active)

global static void createQBSalesReceiptFromMapping(List<QBWrapper> listRecords)

Purpose: Primary Sales Receipt creation path. Uses cm_finance__Custom_Field_Mapping__c configuration to dynamically build a Sales Receipt from any source sObject. This is the recommended and currently active method for Sales Receipt automation.

Prerequisites (must be configured in cm_finance__Custom_Field_Mapping__c):

Mapping Field

Purpose

cm_finance__Field_Mapping__c

JSON: QB Sales Receipt field → Salesforce source field

cm_finance__Source_Invoice_Customer_Lookup_field__c

API name of the Salesforce field holding the Customer record ID

cm_finance__Invoice_Product_Pre_Population_Mapping__c

JSON configuration for child line-item records

cm_finance__QB_Custom_Fields_Mapping__c

JSON: QB custom field Definition ID → Salesforce field

Processing Flow:

1. Resolve QB company from QBWrapper.oRequestWrapper.companyId
2. Query Custom_Field_Mapping__c for Mapping_Type__c = 'SalesReceipt' and matching sObject
3. Extract all required Salesforce field names from the mapping configurations
4. Query the source sObject for all fields in one SOQL query
5. For each record:
   a. Build QBApiDataWrapper.SalesReceipt via populateField() from cm_finance__Field_Mapping__c
   b. Resolve the QB Customer ID via cm_finance__Source_Invoice_Customer_Lookup_field__c
      → If no QB Customer is found for 1 record: auto-trigger callQBService() to create it first
      → If no QB Customer is found for >1 record: log a Failed Sales Receipt record
   c. Build line items from child sObjects via Invoice_Product_Pre_Population_Mapping__c
      → Resolve QB Product IDs from cm_finance__Quickbooks_Product__c
      → Resolve QB Class IDs from cm_finance__Quickbooks_Class__c
   d. Populate QB custom fields from cm_finance__QB_Custom_Fields_Mapping__c
6. Call QBSalesReceiptCallout() with all valid requests
7. Insert Failed cm_finance__Quickbooks_Sales_Receipt__c records for pre-validation failures

Line Item Mapping: Uses the configured child-record query, parent lookup field, product field, quantity, unit price, description, and optional QB Class lookup to build Sales Receipt line items. Product and Class references are resolved from their corresponding QuickBooks mapping records before the request is sent.

Failure Modes & Error Records Created:

Scenario

Error Message on cm_finance__Quickbooks_Sales_Receipt__c

QB Company not provided

Quickbooks Company not found on Request wrapper

No Custom Field Mapping found

Custom Field Mapping not found for current object

No Sales Receipt line items resolved

Sales Receipt Line Items not found on source object

Customer lookup field empty

Customer reference field not populated on source object

QB Customer not found (>1 record)

Quickbooks Customer not found on source object


3.1.5 Method: QBSalesReceiptCallout

global static void QBSalesReceiptCallout(List<QBWrapper> listRecords)

Purpose: Second-stage Sales Receipt processor. Accepts pre-built QBApiDataWrapper.SalesReceipt objects inside QBWrapper.oRequestWrapper and either sends them through the real-time queueable path or stages them for bulk QB synchronisation.

Routing:

Condition

Behaviour

listRecords.size() == 1

Sets requestType = 'SalesReceipt' and enqueues QueueableAutomateQBService for a real-time API callout.

listRecords.size() > 1

Bulk path — builds and inserts staged cm_finance__Quickbooks_Sales_Receipt__c and cm_finance__Quickbooks_Sales_Receipt_Item__c records for batch processing.

Bulk Lookup Resolution:

The bulk path resolves all references in memory before DML, using Salesforce mapping records for:

  1. cm_finance__Quickbooks_Customer__c → QB Customer ID to Salesforce Customer record

  2. cm_finance__Quickbooks_Product__c → QB Product ID to Salesforce Product record

  3. cm_finance__Quickbooks_Class__c → QB Class ID to Salesforce Class record

  4. cm_finance__Quickbooks_Custom_Field__c → Definition ID and company to Salesforce Custom Field record

  5. cm_finance__Payment_Method__c → QB Payment Method ID to Salesforce Payment Method record

    1. cm_finance__Quickbooks_Account__c → QB Account ID to Salesforce QB Account record

Custom Field Encoding: Custom field values are temporarily stored in cm_finance__Custom_Field_1__c, _2__c, and _3__c using the format DefinitionId::FieldValue. The method splits each value, resolves the Salesforce Custom Field record ID, and writes the final value to the staged Sales Receipt record.


3.1.6 Method: createErrorRecords

public static void createErrorRecords(List<QBWrapper> listRecords)

Purpose: Fallback error handler. Creates Failed status records in the corresponding QB object for each item in the list, using upsert on cm_finance__Parent_Record_Id__c.

requestType

Record Created

Upsert Key

Customer

cm_finance__Quickbooks_Customer__c

cm_finance__Parent_Record_Id__c

Product

cm_finance__Quickbooks_Product__c

cm_finance__Parent_Record_Id__c

Invoice

cm_finance__Quickbooks_Invoice__c

cm_finance__Parent_Record_Id__c

Sales Receipt

cm_finance__Quickbooks_Sales_Receipt__c

cm_finance__Parent_Record_Id__c


3.1.7 Method: createJournalLedgerRecords

global static Map<String, Object> createJournalLedgerRecords(List<QBWrapper> lstRecords)

Purpose: Delegates journal ledger creation to QuickbooksController.createJournalLedgerInQuickbooks().

Required QBWrapper Fields:

Field

Description

companyRecordId

Salesforce ID of the QB Company record

companyId

QuickBooks Realm ID

requestBody

Pre-built JSON request body for the journal entry API

Return Value: Map<String, Object> with keys:

  • isSuccess (Boolean)

  • message (String) — error detail if isSuccess = false


3.2 Inner Class: QBWrapper

global class QBWrapper { ... }

The data transfer object passed to all invocable and static methods in this service. Fields marked with @InvocableVariable are directly assignable from Salesforce Flows.

Field

Type

Invocable

Required

Description

recordId

Id

Salesforce record ID of the source sObject being synced

requestType

String

Type of entity: Customer, Product, SalesReceipt or Invoice

companyRecordId

Id

Salesforce record ID of the QB Company. Falls back to default company if blank.

parentCustomerRecordId

Id

(Customer) SF record ID of the parent QB Customer (for sub-customer creation)

assetAccountId

Id

(Product) SF record ID of the QB Account to use as Asset Account

incomeAccountId

Id

(Product) SF record ID of the QB Account to use as Income Account

expenseAccountId

Id

(Product) SF record ID of the QB Account to use as Expense Account

companyId

String

QuickBooks Realm/Company ID (internal use)

requestBody

String

Pre-built JSON string (used for journal ledger)

oRequestWrapper

QBApiDataWrapper.QBRequestWrapper

Pre-built API request object (populated programmatically for invoice flow)


3.3 QueueableAutomateQBService

File: force-app/main/default/classes/QueueableAutomateQBService.cls Access Modifier: public with sharing Implements: Queueable, Database.AllowsCallouts

Handles the real-time single-record path. Enqueued by InvocableAutomateQBService when listRecords.size() == 1.

Property

Value

PDF Generation

Controlled by cm_finance__Quickbooks_Common_Settings__c (instance name: GenerateInvoicePDF for Invoice and GenerateSalesReceiptPDF for Invoice)

FLS Enforcement

Inherited from CMQBConnectorUtility.enforceFLS

Method: execute

Routes to createQBCustomer(), createQBSalesReceipt() or createQBInvoice() based on requestType.

Customer → Invoice Chaining: After a Customer is successfully created in QBO:

  • If the wrapper also contains a pre-built oRequestWrapper.oInvoice, the method automatically re-enqueues itself with requestType = 'Invoice'.

  • This enables a customer-first, invoice-second sequential flow in a single automation trigger.

Flow execution:
  Queueable[1]: requestType='Customer'
    → Creates customer in QBO
    → If invoice is pending in wrapper:
        enqueue Queueable[2]: requestType='Invoice'

  Queueable[2]: requestType='Invoice'
    → Creates invoice in QBO, sets customerRef from Queueable[1] result

Customer → Sales Receipt Chaining: After a Customer is successfully created in QBO:

  • If the wrapper also contains a pre-built oRequestWrapper.oSalesReceipt, the method automatically re-enqueues itself with requestType = 'SalesReceipt'.

  • This enables a customer-first, sales receipt-second sequential flow in a single automation trigger.

Flow execution:
  Queueable[1]: requestType='Customer'
    → Creates customer in QBO
    → If sales receipt is pending in wrapper:
        enqueue Queueable[2]: requestType='SalesReceipt'

  Queueable[2]: requestType='SalesReceipt'
    → Creates sales receipt in QBO, sets customerRef from Queueable[1] result

Method: createQBCustomer

public static List<cm_finance__Quickbooks_Customer__c> createQBCustomer(InvocableAutomateQBService.QBWrapper oReqWrapper)

Steps:

  1. Resolves QB Company and Realm ID.

  2. Calls CMQBConnectorUtility.getQBObjectMapping() to get Customer field mapping for the source sObject.

  3. Queries source sObject for all mapped fields.

  4. Checks for existing QB Customer (update scenario) via QuickbooksController.getRelatedQuickbooksCustomers().

  5. Calls WS_Quickbooks.createCustomer()live HTTP callout to QBO.

  6. On success: enriches and upserts cm_finance__Quickbooks_Customer__c using Composite_Unique_Key__c.

  7. On failure: upserts a Failed status Customer record using Parent_Record_Id__c.

Composite Unique Key format: {QB_Id}::Customer::{RealmId}

Method: createQBInvoice

public static void createQBInvoice(InvocableAutomateQBService.QBWrapper oWrapper)

Steps:

  1. Validates QB Company.

  2. Calls WS_Quickbooks.createInvoice()live HTTP callout to QBO.

  3. Resolves all reference lookups (Customer, Product, Class, Custom Fields) from Salesforce.

  4. Sets invoice status to Invoice PDF Pending if GenerateInvoicePDF setting is true, else Awaiting Salesforce Sync.

  5. Inserts cm_finance__Quickbooks_Invoice__c and child cm_finance__Quickbooks_Invoice_Item__c records.

  6. Sets Composite_Unique_Key__c ({QB_Id}::{RealmId}) and Ignore_Webhook__c = true.

  7. On failure: upserts a Failed status invoice record.

Method: createQBSalesReceipt

public static void createQBSalesReceipt(InvocableAutomateQBService.QBWrapper oWrapper)

Steps:

  1. Validates QB Company.

  2. Calls WS_Quickbooks.createQBSalesReceipt()live HTTP callout to QBO.

  3. Resolves all reference lookups (Customer, Product, Class, Custom Fields, Payment Method, QB Account) from Salesforce.

  4. Sets sales receipt status to Sales Receipt PDF Pending if GenerateSalesReceiptPDF setting is true, else Awaiting Salesforce Sync.

  5. Inserts cm_finance__Quickbooks_Sales_Receipt__c and child cm_finance__Quickbooks_Sales_Receipt_Item__c records.

  6. Sets Composite_Unique_Key__c ({QB_Id}::{RealmId}) and Ignore_Webhook__c = true.

  7. On failure: upserts a Failed status invoice record.


3.4 Supporting Class: CMQBConnectorUtility

File: force-app/main/default/classes/CMQBConnectorUtility.cls Access Modifier: public with sharing

Central utility class used by all automation service classes.

Method

Purpose

enforceFLSSetting()

Returns false for guest/automated users (bypasses FLS); true otherwise. Guest users are configurable via GuestUsers custom setting.

getDefaultCompanyId()

Returns the default QB Company Salesforce record ID

getDefaultCompanyRealmId()

Returns the default QB Realm/Company ID

getQBObjectMapping(objectName, mappingType, companyId)

Queries Custom_Field_Mapping__c and returns a map with isSuccess and mapFieldMapping keys

createCustomerRequestWrapper(sObject, wrapper, fieldMapping)

Populates a QBApiDataWrapper.Customer from an sObject using the provided mapping

createProductRequestWrapper(sObject, wrapper, fieldMapping)

Populates a QBApiDataWrapper.Product

createInvoiceRequestWrapper(sObject, wrapper, fieldMapping)

Populates a QBApiDataWrapper.Invoice

createCustomerRecord(id, parentId, status, description, existing)

Factory: builds/enriches a cm_finance__Quickbooks_Customer__c

createProductRecord(id, parentId, status, description, existing)

Factory: builds/enriches a cm_finance__Quickbooks_Product__c

createInvoiceRecord(id, parentId, status, description, existing)

Factory: builds/enriches a cm_finance__Quickbooks_Invoice__c

createVendorRecord(...)

Factory: builds/enriches a cm_finance__Quickbooks_Vendor__c

createQBAccountRecord(...)

Factory: builds/enriches a cm_finance__Quickbooks_Account__c

validateEmail(email)

Validates email format via regex; returns null if invalid

parseDate(dateString)

Parses YYYY-MM-DD string to Date

populateParentLookup(records, objApiName)

Dynamically resolves the correct SF lookup field from cm_finance__Parent_Record_Id__c


3.5 Supporting Class: QBApiDataWrapper

File: force-app/main/default/classes/QBApiDataWrapper.cls Access Modifier: global with sharing

All data models for the QB API integration layer.

Key Wrapper Classes

Class

Purpose

QBRequestWrapper

Container passed to WS_Quickbooks methods. Holds one entity object plus metadata (companyId, realmId, accessToken, parentRecordId).

QBResponseWrapper

Container returned from WS_Quickbooks methods. Holds lists of parsed entity objects plus API status.

APIResponse

Encapsulates isSuccess, errorMessage, and raw HTTP response

Entity Classes

Class

Key Fields

Notable

Customer

QB_Id, QB_SyncToken, displayName, email, phone, billingAddress, shippingAddress, parentReference, taxable, active

populateField(key, value) supports dot-notation for nested address fields (e.g., billingAddress.city)

Vendor

QB_Id, QB_SyncToken, displayName, email, billingAddress, vendor1099, accountNumber, website

Product

QB_Id, QB_SyncToken, name, type, incomeAccount, expenseAccount, assetAccount, active

Invoice

QB_Id, QB_SyncToken, customerRef, txnDate, dueDate, totalAmount, lstLines, lstCustomFields, balance

lstLines contains Line objects with SalesLineItemDetail

SalesReceipt

QB_Id, QB_SyncToken, customerRef, txnDate, paymentRefNum, lstLines, lstCustomFields, customerMemo, deposit,depositToAccountRef

lstLines contains Line objects with SalesLineItemDetail

Account

QB_Id, QB_SyncToken, name, accountType, accountSubType, classification

Payment

QB_Id, QB_SyncToken, totalAmount, customerRef, txnDate, depositAccount

JournalLedger

Journal entry data structure

Used for createJournalLedgerRecords() path

TaxRate

QB_Id, taxName, rateValue, specialTaxType, active

QBClass

QB_Id, className, fullyQualifiedName, parentReference, subClass

CustomField

definitionId, customFieldName, value

Maps to QB's custom fields on Invoice

PaymentMethod

QB_Id,QB_SyncToken, name, type, active

Supporting Classes

Class

Purpose

Address

Id, city, stateCode, country, addressLine1, postalCode

AccountReference

value (QB ID string), name, recordId (SF record ID)

Line

Invoice/Journal line item: amount, description, detailType, lineId, salesItemDetail

SalesLineItemDetail

itemRef (product), classRef, quantity, unitPrice

JournalEntryLineDetail

Account ref and posting type for journal lines


4. End-to-End Flow Diagrams

4.1 Customer Automation (Single Record)

Flow triggers with 1 Customer record
  │
  ▼
callQBService()
  │
  ├── size == 1 AND type != 'Product'
  │       │
  │       ▼
  │   System.enqueueJob(QueueableAutomateQBService)
  │       │
  │       ▼
  │   execute() → createQBCustomer()
  │       │
  │       ├── [Success] → Upsert cm_finance__Quickbooks_Customer__c
  │       │                Status: 'Awaiting Salesforce Sync'
  │       │                Composite_Unique_Key__c set
  │       │
  │       └── [Failure] → Upsert cm_finance__Quickbooks_Customer__c
  │                        Status: 'Failed', error in Status_Description__c
  │
  └── (end)

4.2 Invoice Automation (Single Record) — Primary Path

Flow triggers with 1 Invoice record
  │
  ▼
createQBInvoiceFromMapping()
  │
  ├── Resolve Custom Field Mapping for source sObject + company
  ├── Query source sObject fields
  ├── Resolve QB Customer ID from lookup field
  │       │
  │       ├── [QB Customer Not Found, 1 record]
  │       │       │
  │       │       ▼
  │       │   callQBService() → creates Customer first
  │       │   (Invoice creation deferred to Queueable chain)
  │       │
  │       └── [QB Customer Found]
  │               │
  │               ▼
  │           Build Invoice + Line Items + Custom Fields
  │               │
  │               ▼
  │           QBInvoiceCallout()
  │               │
  │               ├── size == 1
  │               │       │
  │               │       ▼
  │               │   System.enqueueJob(QueueableAutomateQBService)
  │               │       │
  │               │       ▼
  │               │   createQBInvoice() → Live API callout
  │               │       │
  │               │       ├── [Success] → Insert Invoice + Invoice Items
  │               │       │               Status: 'Invoice PDF Pending' or 'Awaiting Salesforce Sync'
  │               │       │
  │               │       └── [Failure] → Upsert Invoice, Status: 'Failed'
  │               │
  │               └── (end)
  │
  └── (end)

4.3 Bulk Customer/Product Automation

Flow triggers with N records (N > 1 or type = 'Product')
  │
  ▼
callQBService()
  │
  ├── Resolve QB Company + Realm ID
  ├── Query Custom_Field_Mapping__c
  ├── Dynamically query source sObjects
  ├── For each record: build QBApiDataWrapper request wrapper
  │       (no live API callout here)
  ├── Create/Upsert cm_finance__Quickbooks_Customer__c or Product records
  │       Status: 'Awaiting QB Sync'
  │       Process_via_Batch__c = true
  │       Ignore_Webhook__c = true
  │
  └── Batch job picks up records and performs live QB API callout

4.4 Sales Receipt Automation (Single Record) — Primary Path

Flow triggers with 1 Sales Receipt record
  │
  ▼
createQBSalesReceiptFromMapping()
  │
  ├── Resolve Custom Field Mapping for source sObject + company
  ├── Query source sObject fields
  ├── Resolve QB Customer ID from lookup field
  │       │
  │       ├── [QB Customer Not Found, 1 record]
  │       │       │
  │       │       ▼
  │       │   callQBService() → creates Customer first
  │       │   (Sales Receipt creation deferred to Queueable chain)
  │       │
  │       └── [QB Customer Found]
  │               │
  │               ▼
  │           Build Sales Receipt + Line Items + Custom Fields
  │               │
  │               ▼
  │           QBSalesReceiptCallout()
  │               │
  │               ├── size == 1
  │               │       │
  │               │       ▼
  │               │   System.enqueueJob(QueueableAutomateQBService)
  │               │       │
  │               │       ▼
  │               │   createQBSalesReceipt() → Live API callout
  │               │       │
  │               │       ├── [Success] → Insert Sales Receipt + Sales Receipt Items
  │               │       │               Status: 'Sales Receipt PDF Pending' or
  │               │       │               'Awaiting Salesforce Sync'
  │               │       │
  │               │       └── [Failure] → Upsert Sales Receipt, Status: 'Failed'
  │               │
  │               └── (end)
  │
  └── (end)

4.4 Sales Receipt Automation (Single Record) — Primary Path

Flow triggers with 1 Sales Receipt record
  │
  ▼
createQBSalesReceiptFromMapping()
  │
  ├── Resolve Custom Field Mapping for source sObject + company
  ├── Query source sObject fields
  ├── Resolve QB Customer ID from lookup field
  │       │
  │       ├── [QB Customer Not Found, 1 record]
  │       │       │
  │       │       ▼
  │       │   callQBService() → creates Customer first
  │       │   (Sales Receipt creation deferred to Queueable chain)
  │       │
  │       └── [QB Customer Found]
  │               │
  │               ▼
  │           Build Sales Receipt + Line Items + Custom Fields
  │               │
  │               ▼
  │           QBSalesReceiptCallout()
  │               │
  │               ├── size == 1
  │               │       │
  │               │       ▼
  │               │   System.enqueueJob(QueueableAutomateQBService)
  │               │       │
  │               │       ▼
  │               │   createQBSalesReceipt() → Live API callout
  │               │       │
  │               │       ├── [Success] → Insert Sales Receipt + Sales Receipt Items
  │               │       │               Status: 'Sales Receipt PDF Pending' or
  │               │       │               'Awaiting Salesforce Sync'
  │               │       │
  │               │       └── [Failure] → Upsert Sales Receipt, Status: 'Failed'
  │               │
  │               └── (end)
  │
  └── (end)

4.4 Sales Receipt Automation (Single Record) — Primary Path

https://cloudmaven.atlassian.net/avpviz/c/a8fd9983-e91d-4da7-b3cc-922119981cc4/w/80bbde58-b86c-4fdc-8111-7f976d003d57/d/bfa8eb0d-2460-4679-b1c5-3b431ad5958a/chart/92733349-7539-4407-8181-c66ed92b6882


5. Custom Object Record Lifecycle

Stage

Status Value

Trigger

Automation created (bulk)

Awaiting QB Sync

callQBService() bulk path

Automation created (real-time, success)

Awaiting Salesforce Sync

QueueableAutomateQBService success

Automation created (real-time, invoice or sales receipt)

Invoice PDF Pending or Sales Receipt PDF Pending

createQBInvoice() or createQBSalesReceipt() when PDF generation enabled

Automation pre-validation failure

Failed

Pre-callout validation in createQBInvoiceFromMapping() or createQBSalesReceiptFromMapping()

Live API callout failure

Failed

createQBCustomer() / createQBInvoice()/ createQBSalesReceipt()on error


6. Configuration Reference

6.1 cm_finance__Custom_Field_Mapping__c

This object drives all dynamic field mapping. One record per sObject per company per mapping type.

Field

API Name

Purpose

Source

cm_finance__Source__c

Must be Quickbooks

sObject API Name

cm_finance__sObject_API_Name__c

API name of the source SF object

Mapping Type

cm_finance__Mapping_Type__c

Customer, Product, SalesReceiptor Invoice

QB Company

cm_finance__Quickbooks_Company__c

Lookup to the QB Company record

Field Mapping

cm_finance__Field_Mapping__c

JSON: { "QB_field_key": "sf_field_api_name" }

Customer Lookup Field

cm_finance__Source_Invoice_Customer_Lookup_field__c

(Invoice only) SF field holding the parent Customer record ID

Product Pre-Population

cm_finance__Invoice_Product_Pre_Population_Mapping__c

(Invoice only) JSON config for child line-item records

QB Custom Fields

cm_finance__QB_Custom_Fields_Mapping__c

JSON: { "QB_definition_id": "sf_field_api_name" }

6.2 cm_finance__Quickbooks_Common_Settings__c (Custom Setting)

Instance Name

Value

Effect

GuestUsers

Semicolon-separated user type names

User types that bypass FLS enforcement. Default: AutomatedProcess;CloudIntegrationUser;Guest

GenerateInvoicePDF

true / false

If true, newly created invoices get status Invoice PDF Pending instead of Awaiting Salesforce Sync


7. Configuring a Flow Automation

Step 1 — Set Up Field Mapping

Create a cm_finance__Custom_Field_Mapping__c record:

Source:                 Quickbooks
sObject API Name:       Opportunity  (or any source object)
Mapping Type:           Invoice
QB Company:             [Your QB Company record]
Field Mapping:          {"txnDate": "CloseDate", "dueDate": "DueDate__c"}
Customer Lookup Field:  AccountId
Product Pre-Population: { "query": "SELECT Id, {0}, Product2Id, UnitPrice, Quantity FROM OpportunityLineItem WHERE {0} IN :parentRecordId", "lookupFieldName": "OpportunityId", "fieldMapping": { "sfProductId": "Product2Id", "ProductQuantity": "Quantity", "ProductUnitPrice": "UnitPrice" } }

Step 2 — Configure the Flow

  1. Add an Action element to your Flow.

  2. Search for "Call QB Service" (the @InvocableMethod label).

  3. Map the following input variables:

Flow Variable

QBWrapper Field

Notes

Record ID

recordId

The triggering record's ID

Request Type

requestType

Hardcode to Invoice (or Customer/Product/ SalesReceipt)

Company Record ID

companyRecordId

SF ID of the QB Company record. Optional if default is set.

  1. For Invoice flows, the oRequestWrapper is built internally by createQBInvoiceFromMapping() — no additional mapping needed in the Flow.

Step 3 — Invoke createQBInvoiceFromMapping for Invoice Flows

For invoice automations, do not call callQBService directly from the Flow. Instead, call createQBInvoiceFromMapping from Apex (e.g., a trigger or invocable wrapper) that passes the QBWrapper list with oRequestWrapper.companyId pre-populated.


8. Error Handling Summary

Layer

Mechanism

Output

Pre-validation

createErrorRecords() or inline createInvoiceRecord(..., 'Failed', errorMsg, null)

Failed status record in QB custom object

API callout failure (real-time)

createQBCustomer() / createQBInvoice() error branch

Failed status record, status message from QBO API

Unexpected exception

try/catch in all methods, System.debug output

No record created; debug logs only

Missing company

createErrorRecords()

Failed records for all input wrappers

Missing field mapping

createErrorRecords() or invoice pre-check

Failed records for all input wrappers

Note: Exceptions in the main methods are caught silently (debug only). Monitor debug logs or the Failed status records in QB custom objects to diagnose automation issues.


9. Key Design Decisions

Decision

Rationale

Single record → Queueable

Salesforce Flows cannot perform callouts in the same transaction. Queueable with Database.AllowsCallouts is required.

Multiple records → Batch staging

Avoids hitting Queueable chain limits (max 50 chained jobs). Bulk records are staged and processed by a scheduled batch.

Ignore_Webhook__c = true on all created records

Prevents the QB webhook listener from processing records already created by this automation, avoiding duplicate sync loops.

Process_via_Batch__c = true on bulk records

Flags records for batch job pickup, enabling the actual QBO API callout outside the automation transaction.

Upsert on Composite_Unique_Key__c

Handles both create and update scenarios in a single DML operation. Format: QB_Id::EntityType::RealmId.

Composite_Unique_Key__c for Invoice

Format: QB_Id::RealmId (no entity type, as each invoice is already scoped by company).

Auto customer creation for invoices

If createQBInvoiceFromMapping() detects a missing QB Customer (single record only), it automatically calls callQBService() to create the customer first, deferring the invoice to the Queueable chain.