QuickBooks Connector

Technical Documentation - Manual Record Creation


1. Overview

The Manual Record Creation system is the interactive, UI-driven layer of the CloudMaven QB Connector. It exposes a Lightning Web Component (cmQBConnectorMaster) embedded on any Salesforce record page that allows users to:

  • Push Salesforce records (Customers, Vendors, Products, Invoices, Sales Receipt) to QuickBooks Online

  • Pull QuickBooks records back into Salesforce

  • Create net-new QB entities (Customers, Vendors, Products, Invoices, Sales Receipt) directly from the Salesforce UI with live field pre-fill from the source record

The system is backed by three Apex controller classes — QuickbooksController (Customer, Vendor, Product, Account sync), QBSalesReceiptController (Sales Receipt-specific operations) and QBInvoiceController (Invoice-specific operations) — and relies on the shared service layer (WS_Quickbooks, QBApiDataWrapper, CMQBConnectorUtility, Data) used across all integration pathways.


2. Component Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                          Salesforce Record Page                              │
│                                                                              │
│  ┌────────────────────────────────────────────────────────────────────────┐  │
│  │                         cmQBConnectorMaster (LWC)                      │  │
│  │                                                                        │  │
│  │                         Screen State Machine                           │  │
│  │                           (Boolean Flags)                              │  │
│  │                                                                        │  │
│  │  ┌──────────────┐    ┌───────────────┐    ┌─────────────────────────┐  │  │
│  │  │ Company      │    │ Home Screen   │    │ Feature Sections        │  │  │
│  │  │ Select       │    │               │    │                         │  │  │
│  │  │ Screen       │    │ Push / Pull   │    │ • Invoice Section       │  │  │
│  │  │              │    │               │    │ • Product Section       │  │  │
│  │  │              │    │               │    │ • Sales Receipt Section │  │  │
│  │  └──────┬───────┘    └──────┬────────┘    └───────────┬─────────────┘  │  │
│  │         │                    │                         │                │  │
│  │         └────────────────────┴─────────────────────────┘                │  │
│  │                              │                                          │  │
│  │                    @AuraEnabled Apex Calls                             │  │
│  └──────────────────────────────┼─────────────────────────────────────────┘  │
│                                 │                                            │
│       ┌─────────────────────────┼──────────────────────────┐                 │
│       │                         │                          │                 │
│  ┌────▼────────────────┐  ┌────▼─────────────────┐  ┌────▼─────────────────┐│
│  │ QuickbooksController│  │ QBInvoiceController  │  │ QBSalesReceipt       ││
│  │                     │  │                      │  │ Controller            ││
│  │ • Companies         │  │ • checkExistingInvoice│ │ • checkExisting       ││
│  │ • Customers         │  │ • fetchRelatedCustomer││   SalesReceipt         ││
│  │ • Vendors           │  │ • sendPrefilledInvoice││ • fetchRelatedCustomer││
│  │ • Products          │  │ • createInvoice...    ││ • sendPrefilled        ││
│  │ • Accounts          │  │ • syncInvoice...      ││   SalesReceipt         ││
│  │                     │  │                      │  │ • createSalesReceipt...││
│  │                     │  │                      │  │ • syncSalesReceipt... ││
│  └──────────┬──────────┘  └──────────┬───────────┘  └──────────┬───────────┘│
│             │                        │                         │             │
│             └────────────────────────┼─────────────────────────┘             │
│                                      │                                       │
│  ┌───────────────────────────────────▼────────────────────────────────────┐  │
│  │                         Shared Service Layer                            │  │
│  │                                                                        │  │
│  │  WS_Quickbooks  │  QBApiDataWrapper  │  Data                          │  │
│  │  CMQBConnectorUtility  │  UpsertCustomRecords                          │  │
│  └───────────────────────────────────┬────────────────────────────────────┘  │
│                                      │                                       │
│                                      ▼                                       │
│                            QuickBooks Online API                             │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

3. LWC Component — cmQBConnectorMaster

3.1 Component Identity

Property

Value

Component Name

cmQBConnectorMaster

JS Class Name

InvoiceController (extends NavigationMixin(LightningElement))

Template

cmQBConnectorMaster.html

Location

force-app/main/default/lwc/cmQBConnectorMaster/

Apex Imports

25+ @AuraEnabled methods from QuickbooksController, QBInvoiceController, CMQBConnectorUtility, QBSalesReceiptController

3.2 Screen State Machine

The component renders exactly one screen at a time using mutually exclusive boolean flags:

Flag

Screen Rendered

Trigger

companySelectionScreen

Company picker (only when >1 company)

connectedCallback when >1 company returned

homeScreen

Push/Pull tab interface

After company selected (or single company auto-selected)

invoiceSection

Invoice creation form

User clicks Push Invoice button

productSection

Product creation form

User clicks Push Product button

createNewCustomer

Warning: go create QB customer first

fetchRelatedCustomerFromParent returns no linked QB customer

bQB_IdFound = false

Record picker to link existing QB record

Retrieve tab flow when no existing QB record found

salesReceiptCreatorVisible

Open the child component to create sales receipt (salesReceiptCreator)

User clicks Push Sales Receipt button

3.3 Initialization Flow (connectedCallback)

connectedCallback()
  │
  ├─ @wire(CurrentPageReference) → extracts recordId from URL state
  ├─ @wire(getObjectInfo, {objectApiName: Quickbooks_Invoice__c}) → schema metadata
  ├─ @wire(getPicklistValues) → Terms picklist on Quickbooks_Invoice__c
  ├─ @wire(getPicklistValues) → Product_Type__c on Quickbooks_Product__c
  │
  └─ getConnectedCompanies()
       │
       ├─ Calls: getListCompanies() [QuickbooksController]
       │
       ├─ 0 companies → show error toast
       ├─ 1 company  → auto-select, call getFieldMappings()
       └─ >1 company → show companySelectionScreen

3.4 Home Screen Initialization (getFieldMappings)

getFieldMappings(companyRecordId)
  │
  └─ Calls: getQBObjectMappingForUI() [CMQBConnectorUtility]
       │
       └─ Returns list of compatible service tabs
            (e.g., Customer, Vendor, Invoice, Product, SalesReceipt)
            Each tab renders a Push card + Retrieve card

3.5 Invoice Push Flow

User clicks "Push Invoice"
  │
  ├─ checkExistingInvoice(recordId, companyRecordId)
  │     └─ If exists → show "already created" warning (Yes/No)
  │
  └─ If not exists:
        ├─ fetchRelatedCustomerFromParent(recordId, companyRecordId)
        │     ├─ Success → populate customerRef on invoice wrapper
        │     └─ No QB customer found:
        │           ├─ custSourceOnInvoice returned → show createNewCustomer screen
        │           └─ Other failure → show error
        │
        ├─ getTermsOptions(label)  → populate Terms combobox
        ├─ getQuickbooksClasses(companyId)  → populate Class combobox (if enabled)
        │
        ├─ sendPrefilledInvoice(recordId, companyId, ...) [pre-fill path]
        │     └─ Populates invoice fields from mapped source sObject fields
        │     OR
        │     createEmptyInvoiceWrapper(mapCustomFields, companyId, parentRecordId)
        │     └─ Blank invoice with today's date, default due date
        │
        └─ Render invoiceSection

3.6 Invoice Submission

User clicks "Create Invoice"
  │
  ├─ validateBusinessRules()
  │     ├─ Name required
  │     ├─ No negative prices
  │     ├─ Inventory type: qty + startDate + assetAccount required
  │     ├─ incomeAccount always required
  │     └─ expenseAccount required only if purchaseCost > 0
  │
  └─ createInvoiceInQuickbooks(sInvoiceWrapper, parentRecordId, companyId, companyRecordId)
        └─ Returns {isSuccess, message, apiResponse}

3.7 Sales Receipt Push Flow

User clicks "Push Sales Receipt"
  │
  ├─ checkExistingSalesReceipt(recordId, companyRecordId)
  │     └─ If exists → show "already created" warning (Yes/No)
  │
  └─ If not exists:
        ├─ fetchRelatedCustomerFromParent(recordId, companyRecordId)
        │     ├─ Success → populate customerRef on sales receipt wrapper
        │     └─ No QB customer found:
        │           ├─ custSourceOnSalesReceipt returned → show createNewCustomer screen
        │           └─ Other failure → show error
        │
        ├─ getQuickbooksClasses(companyId)  → populate Class combobox (if enabled)
        │
        ├─ sendPrefilledSalesReceipt(recordId, companyId, ...) [pre-fill path]
        │     └─ Populates sales receipt fields from mapped source sObject fields
        │     OR
        │     createEmptySalesReceiptWrapper(mapCustomFields, companyId, parentRecordId)
        │     └─ Blank invoice with today's date, default due date
        │
        └─ Render salesReceiptSection

3.8 Sales Receipt Submission

User clicks "Create Sales Receipt"
  │
  ├─ validateBusinessRules()
  │     ├─ Name required
  │     ├─ No negative prices
  │     └─ depositAccount always required
  │
  └─ createSalesReceiptInQuickbooks(sSalesReceiptWrapper, parentRecordId, companyId, companyRecordId)
        └─ Returns {isSuccess, message, apiResponse}

3.9 Product Push Flow

User clicks "Push Product"
  │
  ├─ getCreateProductWrapper(parentRecordId, companyId, companyRecordId)
  │     └─ Returns pre-filled QBRequestWrapper from source sObject + field mapping
  │
  └─ Render productSection
        ├─ Basic Info accordion: name, description, type, SKU, price, purchaseCost
        ├─ Pricing accordion: sales price, purchase cost
        ├─ Inventory Tracking accordion (only if type=Inventory):
        │     qty on hand, start date, asset account (lightning-record-picker)
        └─ Accounts accordion:
              income account (lightning-record-picker, filtered: type=Income)
              expense account (lightning-record-picker, filtered: type in [Expense, COGS])
              asset account (lightning-record-picker, filtered: type=Other Current Asset, subType=Inventory)

3.10 Retrieve (Pull) Flow

User clicks "Retrieve" on any entity tab
  │
  ├─ validateQBId(recordId, companyId)
  │     └─ Checks if source record has a QB_Id field populated
  │
  ├─ QB_Id found → call appropriate sync method:
  │       Customer → syncCustomerFromQuickbooks(lstRecordIds, companyId, companyRecordId)
  │       Vendor   → syncVendorFromQuickbooks(lstRecordIds, companyId, companyRecordId)
  │       Product  → syncProductsFromQuickbooks(lstRecordIds, companyId, companyRecordId)
  │       Invoice  → syncInvoiceFromQuickbooks(recordId, companyId, companyRecordId)
  │ Sales Receipt  → syncSalesReceiptFromQuickbooks(recordId, companyId, companyRecordId)
  │
  └─ QB_Id not found → show lightning-record-picker
        User selects existing QB record → populateParentRecordId()
              Customer → populateParentRecordIdOnCustomer(recordId, parentRecordId)
              Vendor   → populateParentRecordIdOnVendor(recordId, parentRecordId)
              Product  → populateParentOnProduct(recordId, parentRecordId)
              Invoice  → populateParentRecordIdOnInvoice(recordId, parentRecordId)
         SalesReceipt  → populateParentRecordIdOnSalesReceipt(recordId, parentRecordId)

3.11 Key LWC Properties

Property

Type

Purpose

recordId

String

SF record ID from page URL state

selectedCompanyId

String

QB Realm ID of active company

selectedCompanyRecordId

String

SF record ID of QB Company

invoiceWrapper

Object

Live invoice data model bound to the form

lineItems

Array

Line item rows in invoice form

productWrapper

Object

Product data model bound to product form

customerRef

Object

{Cust_QBID, CustName, CustRecordId} — customer for invoice

showClass

Boolean

Drives QB Class column visibility in line items

mapFieldMapping

Object

Field mapping for invoice pre-fill

mapCustomFields

Object

Custom field mapping for invoice custom fields

mapProductFields

Object

Field mapping for product line items

salesReceiptWrapper

Object

Live sales receipt data model bound to the form

3.12 Due Date Calculation

// When Terms selection changes:
dueDate = invoiceDate + parseInt(termValue)

// termValue sourced from getTermsOptions() metadata
// e.g., Net30 → 30, Net60 → 60, Net90 → 90

3.13 Line Item Totals

calculateTotals() {
  subtotal = sum of (qty * unitPrice) for all lineItems
  taxableSubtotal = sum of (qty * unitPrice) for lineItems where taxable = true
}


4. Apex Controller — QuickbooksController

Class declaration: public with sharing class QuickbooksController FLS enforcement: public static Boolean enforceFLS = CMQBConnectorUtility.enforceFLS

4.1 Company Methods


getListCompanies()List<cm_finance__Quickbooks_Company__c>

Retrieves all active QB companies ordered by Default descending, then CreatedDate descending. The LWC uses this to populate the company selector on first load.



Access

@AuraEnabled

Query

WHERE Active__c = true ORDER BY Default__c DESC, CreatedDate DESC

Returns

List of company records or null on error


4.2 Customer Methods


syncCustomerFromQuickbooks(List<Id> lstRecordIds, String companyId, String companyRecordId)Map<String, Object>

Retrieves one or more QB Customer records from QuickBooks Online using existing Quickbooks_Id__c values from linked SF Customer records, then upserts them in Salesforce on Composite_Unique_Key__c.

Flow:

  1. Query cm_finance__Quickbooks_Customer__c WHERE Parent_Record_ID__c IN lstRecordIds to collect existing QB IDs

  2. Build QB SQL: SELECT * FROM Customer WHERE Id IN ('<id1>','<id2>',...)

  3. Call WS_Quickbooks.queryCustomer(oWrapper)

  4. On success: call WS_Quickbooks.createCustomerRecords() to build SF records

  5. Resolve sub-customer parent lookups via a secondary QB ID → SF ID map

  6. Set Composite_Unique_Key__c = QB_Id::Customer::RealmId, set Ignore_Webhook__c = true

  7. Upsert on Composite_Unique_Key__c via data.upsurt()

  8. On failure: insert a Failed status customer record



Access

@AuraEnabled

Returns

{isSuccess, message?, apiResponse?}

Error record

Created with Status = Failed and error message

Upsert key

Composite_Unique_Key__c

Echo suppression

Ignore_Webhook__c = true


createCustomersInQuickbooks(List<Id> lstRecordIds, String companyId, String companyRecordId)Map<String, Object>

Pushes one or more Salesforce records as QB Customers. Supports create-or-update: if a QB Customer already exists for the record, its QB_Id and SyncToken are included in the request body (making it an update).

Flow:

  1. Resolve source sObject name from record ID

  2. Call CMQBConnectorUtility.getQBObjectMapping(objectName, 'Customer', companyRecordId) to get field mappings

  3. Call getRelatedQuickbooksCustomers(lstRecordIds, companyRecordId)Map<String, String> (parentId → QB_Id::SyncToken)

  4. Query source sObject fields per field mapping

  5. For each record: build QBRequestWrapper via CMQBConnectorUtility.createCustomerRequestWrapper(); if existing QB record, populate QB_Id and QB_SyncToken on the wrapper

  6. Call WS_Quickbooks.createCustomer(oWrapper) per record

  7. On success: upsert SF Customer record on Composite_Unique_Key__c; return {isSuccess: true, customerId: <SF ID>}

  8. On failure: insert Failed customer record



Access

@AuraEnabled

Create vs Update

Determined by presence of existing QB ID — no separate code path

Returns

{isSuccess, customerId?, message?, apiResponse?}


populateParentRecordIdOnCustomer(String recordId, String parentRecordId)void

Sets Parent_Record_ID__c on an existing cm_finance__Quickbooks_Customer__c record. Called from the LWC when the user manually links an existing QB customer to the SF record via the record picker.



Access

@AuraEnabled

DML

data.modify()


getRelatedQuickbooksCustomers(List<Id> lstRecordIds, String companyRecordId)Map<String, String>

Non-@AuraEnabled helper. Returns a map of parentRecordId → QB_Id::SyncToken for all non-Failed QB Customer records linked to the provided record IDs. Used internally by createCustomersInQuickbooks to detect existing QB entities before push, enabling upsert semantics.


4.3 Vendor Methods

The Vendor methods mirror the Customer methods in structure and logic:

Method

Description

syncVendorFromQuickbooks(lstRecordIds, companyId, companyRecordId)

Retrieves QB Vendors by existing QB IDs; upserts in SF

createVendorsInQuickbooks(lstRecordIds, companyId, companyRecordId)

Pushes SF records as QB Vendors; supports create-or-update

populateParentRecordIdOnVendor(recordId, parentRecordId)

Links an existing QB Vendor record to an SF source record

getRelatedQuickbooksVendors(lstRecordIds, companyRecordId)

Internal helper: parentRecordId → QB_Id::SyncToken map for vendors

Key field: Composite_Unique_Key__c = QB_Id::Vendor::RealmId


4.4 Account Methods


syncAccountFromQuickbooks(List<Id> lstRecordIds)Map<String, Object>

Syncs cm_finance__Quickbooks_Account__c records from QB. Unlike Customer/Vendor, this method accepts QB Account records directly (not source SF records), so it derives company information from the QB Account's company lookup rather than from parameters.

Flow:

  1. Validate input object type is cm_finance__Quickbooks_Account__c

  2. Extract existing Quickbooks_Id__c values from the provided records

  3. Look up the company from the first record's cm_finance__Quickbooks_Company__c

  4. Build QB SQL: SELECT * FROM Account WHERE Id IN (...)

  5. Call WS_Quickbooks.queryAccount(oWrapper)

  6. On success: upsert SF records on Composite_Unique_Key__c = QB_Id::Account::RealmId



Access

@AuraEnabled

Input type

Must be cm_finance__Quickbooks_Account__c records (not arbitrary SF objects)

Company resolution

Derived from record's company lookup, not parameter


4.5 Product Methods


getCreateProductWrapper(String parentRecordId, String companyId, String companyRecordId)Map<String, Object>

Pre-fills a QBRequestWrapper for product creation using the source SF record's field values and the configured field mapping. If the product already exists in QB, its existing QB_Id, SyncToken, name, type, and account references are pre-populated on the wrapper (enabling update semantics on submit).

Flow:

  1. Resolve source object name from parentRecordId

  2. Call CMQBConnectorUtility.getQBObjectMapping(objectName, 'Product', companyRecordId)

  3. Call getRelatedQuickbooksProducts([parentRecordId], companyRecordId) — returns existing QB product if any

  4. Query source sObject fields per field mapping

  5. Build QBRequestWrapper via CMQBConnectorUtility.createProductRequestWrapper()

  6. If existing QB product found: populate QB_Id, SyncToken, name, type, and account refs (as Name::QB_Id::SF_Id encoded strings)

  7. Ensure incomeAccount, expenseAccount, assetAccount are always initialized (empty if not set)

  8. Return {isSuccess, ProductWrapper (JSON), sMessage}



Access

@AuraEnabled

Failure handling

Creates Failed product record if mapping not found

Account encoding

Name::QB_Id::SF_RecordId triple for each account reference


createProductInQuickbooks(String wrapperJson)Map<String, Object>

Submits a product to QB using a pre-built QBRequestWrapper JSON. Resolves account SF record IDs to QB IDs before the callout by querying Quickbooks_Account__c records.

Flow:

  1. Deserialize wrapperJsonQBApiDataWrapper.QBRequestWrapper

  2. For each of incomeAccount, expenseAccount, assetAccount: if recordId is set, query Quickbooks_Account__c to resolve Name and Quickbooks_Id__c

  3. Set credentials: WS_Quickbooks.sCompanyId = oWrapper.companyRealmId

  4. Call WS_Quickbooks.createProduct(oWrapper)

  5. On success: upsert SF product on Composite_Unique_Key__c = QB_Id::Product::RealmId; set Ignore_Webhook__c = true; set account lookups from wrapper's account record IDs

  6. On failure: insert Failed product record



Access

@AuraEnabled

Account resolution

Batch-queries all three account types before callout

Returns

{isSuccess, message?, apiResponse?}


syncProductsFromQuickbooks(List<Id> lstRecordIds, String companyId, String companyRecordId)Map<String, Object>

Retrieves QB Item records from QuickBooks using existing QB IDs, then upserts them in Salesforce with full account relationship resolution (income/expense/asset accounts mapped by QB ID → SF record ID).

Flow:

  1. Query Quickbooks_Product__c WHERE Parent_Record_ID__c IN lstRecordIds to collect QB IDs

  2. Build QB SQL: SELECT * FROM Item WHERE Id IN (...)

  3. Call WS_Quickbooks.queryProduct(oWrapper)

  4. On success: call WS_Quickbooks.createProductRecords() to build SF records

  5. Collect all income/expense/asset account QB IDs from returned products

  6. Batch-resolve account QB IDs → SF record IDs via Quickbooks_Account__c query

  7. Set account lookups, Composite_Unique_Key__c, Ignore_Webhook__c = true

  8. Upsert on Composite_Unique_Key__c



Access

@AuraEnabled

Account resolution

Single batch query for all three account types

Upsert key

Composite_Unique_Key__c


populateParentOnProduct(String recordId, String parentRecordId)void

Sets Parent_Record_ID__c on an existing QB Product record. Called from the LWC when the user manually links an existing QB product to the SF source record via the record picker.


getRelatedQuickbooksProducts(List<Id> lstRecordIds, String companyRecordId)Map<String, cm_finance__Quickbooks_Product__c>

Non-@AuraEnabled helper. Returns a map of parentRecordId → Quickbooks_Product__c including full account relationship fields. Used internally by getCreateProductWrapper to pre-fill product form with existing QB data.


5. Apex Controller — QBInvoiceController

Class declaration: public with sharing class QBInvoiceController Static properties:

  • storePDF: Reads GenerateInvoicePDF custom setting — controls initial invoice status

  • enforceFLS: From CMQBConnectorUtility.enforceFLS

5.1 Pre-Flight Check Methods


checkExistingInvoice(String recordId, String companyRecordId)Map<String, Object>

Checks whether a non-Failed QB Invoice already exists for the given SF record + company combination. The LWC calls this first when the user clicks "Push Invoice" to decide whether to show a duplicate warning.



Access

@AuraEnabled

Query

WHERE Parent_Record_Id__c = :recordId AND Quickbooks_Company__c = :companyRecordId AND Status__c != 'Failed'

Returns

{isSuccess, exists (Boolean), message?}


getTermsOptions(String label)String

Retrieves the payment terms JSON for the invoice Terms combobox from cm_finance__CM_QB_Connector_Settings__mdt custom metadata by MasterLabel.



Access

@AuraEnabled

Default

'{"Net30":30,"Net60":60,"Net90":90}' when metadata record not found

Returns

JSON string of {termLabel: daysInteger} pairs


fetchRelatedCustomerFromParent(String recordId, String companyRecordId)Map<String, Object>

Resolves the QB Customer linked to an invoice's source record by traversing a 4-step chain through configured field mappings. This is the primary customer resolution path when opening the invoice form.

Resolution chain:

Custom_Field_Mapping__c
  → Source_Invoice_Customer_Lookup_field__c
       → Source sObject[customerLookupField]
            → Quickbooks_Customer__c[Parent_Record_ID__c = customerLookupValue]
                 → createCustomerWrapper(QB Customer Id)

Response key

Meaning

{isSuccess: true, customerWrapper, customerId}

Customer found and serialized

{isSuccess: false, custSourceOnInvoice: <SF Id>}

SF source found but no linked QB customer — triggers "create customer first" screen

{isSuccess: false, errorMessage}

Mapping misconfiguration or missing field



Access

@AuraEnabled

Mapping source

cm_finance__Custom_Field_Mapping__c.Source_Invoice_Customer_Lookup_field__c


checkExistingQBCustomer(String recordId, String mapFields)Map<String, Object>

Alternative customer resolution for cases where the source record directly holds a QB Customer lookup field (rather than going through the 4-step chain). Used by the LWC when the field mapping includes a QuickbooksCustomer key.



Access

@AuraEnabled

Returns

{isSuccess, customerWrapper?} or {isSuccess: false, isRelatedCustomerNotFound: true}


createCustomerWrapper(String recordId)String

Serializes a cm_finance__Quickbooks_Customer__c record into a QBApiDataWrapper.Customer JSON string for use by the invoice form's customer reference fields.

Fields serialized: Company_Name__c, First_Name__c, Last_Name__c, Phone__c, Email__c, billing address (5 fields), shipping address (5 fields), Quickbooks_Id__c, Id



Access

@AuraEnabled

Returns

JSON-serialized QBApiDataWrapper.Customer or null


getQuickbooksClasses(String companyId)List<cm_finance__Quickbooks_Class__c>

Returns active QB Class records for the company. Returns null without querying if the ShowClassInLWC custom setting is not true.



Access

@AuraEnabled

Guard

ShowClassInLWC custom setting must = true

Query

WHERE Quickbooks_Company__c = :companyId AND Active__c = true


5.2 Invoice Wrapper Methods


sendPrefilledInvoice(String recordId, String companyId, String sourceObjectName, String mapCustomFields, String mapFieldMapping, String mapProductFields)String

The primary invoice pre-fill method. Queries the source sObject, maps all configured fields onto a QBApiDataWrapper.Invoice instance, resolves custom fields from QB definition IDs, and resolves product line items by traversing the child relationship.

Phase 1 — Field collection:

  • Merges all SF field names from mapFieldMapping values and mapCustomFields values into a single setFields

  • Executes a single dynamic SOQL against sourceObjectName WHERE Id = :recordId

Phase 2 — Invoice population:

  • Calls oInvoice.populateField(key, value) for each mapped field

  • Default txnDate = today if not mapped

  • Default dueDate = txnDate + getDefaultDaysForDueDateInvoice() (fallback 30) if not mapped

Phase 3 — Custom fields:

  • Queries cm_finance__Quickbooks_Custom_Field__c WHERE Definition_Id__c IN mapCustom.keySet()

  • Builds lstCustomFields with {definitionId, customFieldName, value} for each

Phase 4 — Product line items:

  • Parses mapProductFields.query (a dynamic child SOQL template) and mapProductFields.lookupFieldName

  • Executes child query against parentRecordId

  • Resolves SF Product IDs → QB Product records via cm_finance__Quickbooks_Product__c

  • Returns productsArray as list of {ProductRecordId, productQBId, qty, rate, ...}

Fallback: If mapFieldMapping is empty (no configured mapping), delegates to createEmptyInvoiceWrapper().



Access

@AuraEnabled

Returns

JSON {bSuccess, invoiceWrapper?, invoicePopulated?, productsPopulated?, productsArray?, message}


createEmptyInvoiceWrapper(String mapCustomFields, String companyId, String parentRecordId)String

Creates a blank invoice wrapper with sensible defaults. Used when there is no field mapping configured or as a fallback from sendPrefilledInvoice.

Default values:

  • txnDate = System.today()

  • dueDate = txnDate + getDefaultDaysForDueDateInvoice() (fallback 30)

  • billingEmail = '', terms = '', deposit = 0

  • Billing address fields initialized to empty strings

If mapCustomFields is provided, queries the source object and custom field definitions to pre-populate lstCustomFields with empty or mapped values.



Access

@AuraEnabled

Returns

JSON {isSuccess: true, invoiceWrapper} or {isSuccess: false, message}


5.3 Invoice Create / Sync Methods


createInvoiceInQuickbooks(String sInvoiceWrapper, String parentRecordId, String companyId, String companyRecordId)String

The main invoice creation method. Deserializes the LWC's invoice form state, builds a typed QBApiDataWrapper.Invoice via createInvoiceWrapperAPI(), calls the QB API, and persists the result in Salesforce.

Pre-condition: mapInvoice.get('Products') must be non-null and non-empty — invoices require at least one line item.

Phase 1 — API callout:

  • Builds QBApiDataWrapper.Invoice via private createInvoiceWrapperAPI(mapInvoice)

  • Calls WS_Quickbooks.createInvoice(oRequestWrapper)

Phase 2 — Lookup resolution (on success):

  • Collects QB IDs for: customers, products, classes, custom field definition IDs

  • Resolves each to SF record IDs via batch queries:

    • Quickbooks_Customer__c WHERE Quickbooks_Id__c IN setCustomerQBId

    • Quickbooks_Product__c WHERE Quickbooks_Id__c IN setProductQBId

    • Quickbooks_Class__c WHERE Quickbooks_Id__c IN setClassQBId

    • Quickbooks_Custom_Field__c WHERE Definition_Id__c IN setCustomFieldId

Phase 3 — Custom field decoding:

  • Raw format stored by QB: DefinitionId::Value

  • After resolution: Custom_Field_N__c set to the value, Quickbooks_Custom_Field_N__c set to the SF Custom Field record ID

Phase 4 — Record persistence:

  • Sets Composite_Unique_Key__c = QB_Id::Invoice::RealmId

  • Sets Ignore_Webhook__c = true

  • Sets initial Status__c = 'Invoice PDF Pending' if storePDF = true, else 'Awaiting Salesforce Sync'

  • data.create(lstAllInvoice) → then data.create(lstAllLineItems) (sequential, not upsert)

On any failure: Creates a Failed invoice record with the error message.



Access

@AuraEnabled

Required guard

Products list must not be empty

Returns

JSON {isSuccess, message?, apiResponse?}

Error record

Always created on any failure path


populateParentRecordIdOnInvoice(String recordId, String parentRecordId)void

Sets Parent_Record_ID__c on an existing cm_finance__Quickbooks_Invoice__c record. Called from the LWC when the user manually links an existing QB invoice to the SF record via record picker.



Access

@AuraEnabled

DML

data.modify()


syncInvoiceFromQuickbooks(String recordId, String companyId, String companyRecordId)Map<String, Object>

Retrieves the most recent QB Invoice for the SF record from QuickBooks Online and syncs it back into Salesforce with full line item reconciliation.

Phase 1 — Identify target:

  • Queries cm_finance__Quickbooks_Invoice__c WHERE Parent_Record_ID__c = :recordId AND QB_Id != null ORDER BY CreatedDate DESC LIMIT 1

  • Builds QB SQL: SELECT * FROM Invoice WHERE Id IN ('<QB_Id>')

Phase 2 — Lookup resolution (same as createInvoice):

  • Customer QB ID → SF Customer record

  • Product QB IDs → SF Product records

  • Class QB IDs → SF Class records

  • Custom field definition IDs → SF Custom Field records

  • Custom field decoded from DefinitionId::Value format

Phase 3 — Line item reconciliation:

  • Before the callout, loads all existing line items into mapInvLineItem (keyed by Composite_Unique_Key__c); items without a key go into lstDeleteLineItem

  • After callout: upserts invoice on Composite_Unique_Key__c

  • For incoming line items: upserts on Composite_Unique_Key__c — existing lines are updated, new lines are inserted

  • Orphaned line items (in SF but not in QB response): archived (Archived__c = true) or hard-deleted based on deleteExtraLineItemsSetting() custom setting



Access

@AuraEnabled

Callout

WS_Quickbooks.queryInvoice(oWrapper)

Returns

{isSuccess, message}

Orphan handling

Setting-driven: archive or hard delete


5.4 Private Helper


createInvoiceWrapperAPI(Map<String, Object> mapInvoice)QBApiDataWrapper.Invoice (private)

Deserializes the raw LWC JSON map (passed from createInvoiceInQuickbooks) into a fully typed QBApiDataWrapper.Invoice object ready for the API callout.

Key mappings:

LWC Map Key

Invoice Field

Notes

billingEmail

oInvoice.billingEmail

Direct string

terms

oInvoice.terms

Direct string

txnDate

oInvoice.txnDate

Direct string

dueDate

oInvoice.dueDate

Direct string

deposit

oInvoice.deposit

Decimal

Products[].ProductRecordId

itemRef.value/name

Resolved via Quickbooks_Product__c query

Products[].qbClassId

classRef.value

Direct string

Products[].ProductQuantity

salesItemDetail.quantity

Decimal

Products[].ProductUnitPrice

salesItemDetail.unitPrice

Decimal

Products[].ProductAmount

oLine.amount

Decimal

Products[].ProductDescription

oLine.description

String

customerRef.Cust_QBID

customerRef.value

QB Customer ID

customerRef.CustName

customerRef.name

Customer display name

customerRef.CustRecordId

customerRef.recordId

SF Customer record ID

customerMemo.value

customerMemo.value

Memo string

lstCustomFields[].definitionId + value

lstCustomFields

Only non-null pairs included

billingAddress.*

billingAddress.*

5 address fields

shippingAddress.*

shippingAddress.*

5 address fields

Line items are numbered sequentially starting at 1 via a count variable.

6. Apex Controller — QBSalesReceiptController

Class declaration: public with sharing class QBSalesReceiptController Static properties:

  • storePDF: Reads GenerateSalesReceiptPDF custom setting — controls initial invoice status

  • enforceFLS: From CMQBConnectorUtility.enforceFLS

6.1 Pre-Flight Check Methods

checkExistingSalesReceipt(String recordId, String companyRecordId)Map<String, Object>

Checks whether a non-Failed QB Sales Receipt already exists for the given SF record + company combination. Mirrors the invoice duplicate-check pattern; the LWC calls this first when the user clicks "Push Sales Receipt."



Access

@AuraEnabled

Query

WHERE Parent_Record_ID__c = :recordId AND Quickbooks_Company__c = :companyRecordId AND Status__c != 'Failed'

Returns

{isSuccess, exists (Boolean), message?}

fetchRelatedCustomerFromParent(String recordId, String companyRecordId)Map<String, Object>

Resolves the QB Customer linked to a sales receipt's source record by traversing a field-mapping chain scoped to Mapping_Type__c = 'SalesReceipt'. Same shape as the invoice equivalent, with the mapping type filter as the key difference.

Resolution chain:

Custom_Field_Mapping__c (Mapping_Type__c = 'SalesReceipt')
  → Source_Invoice_Customer_Lookup_field__c
       → Source sObject[customerLookupField]
            → Quickbooks_Customer__c[Parent_Record_ID__c = customerLookupValue]
                 → createCustomerWrapper(QB Customer Id)

Response key

Meaning

{isSuccess: true, customerWrapper, customerId}

Customer found and serialized

{isSuccess: false, custSourceOnInvoice: <SF Id>}

SF source found but no linked QB customer — triggers "create customer first" screen

{isSuccess: false, errorMessage}

Mapping misconfiguration or missing field



Access

@AuraEnabled

Mapping source

cm_finance__Custom_Field_Mapping__c.Source_Invoice_Customer_Lookup_field__c, filtered on Mapping_Type__c = 'SalesReceipt'

Error handling

Wrapped in try/catch → throw new AuraHandledException(e.getMessage()) (differs from the invoice version, which returns errors in the map instead of throwing)

checkExistingQBCustomer(String recordId, String mapFields)Map<String, Object>

Alternative customer resolution for cases where the source record directly holds a QB Customer lookup field. Identical pattern to the invoice controller — driven by a QuickbooksCustomer key in the field-mapping JSON.



Access

@AuraEnabled

Returns

{isSuccess, customerWrapper?} or {isSuccess: false, isRelatedCustomerNotFound: true}

createCustomerWrapper(String recordId)String

Serializes a cm_finance__Quickbooks_Customer__c record into a QBApiDataWrapper.Customer JSON string. Same field set as the invoice controller's version, plus a Web Address reference.

Fields serialized: Company_Name__c, First_Name__c, Last_Name__c, Phone__c, Email__c, billing address (5 fields), shipping address (5 fields), Web_Address__c (→ webAddress.uri), Quickbooks_Id__c, Id



Access

@AuraEnabled

Returns

JSON-serialized QBApiDataWrapper.Customer or null

getQuickbooksClasses(String companyId)List<cm_finance__Quickbooks_Class__c>

Returns active QB Class records for the company. Returns null without querying if the ShowClassInLWC custom setting is not true. Identical to the invoice controller's method.



Access

@AuraEnabled

Guard

ShowClassInLWC custom setting must = true

Query

WHERE Quickbooks_Company__c = :companyId AND Active__c = true

6.2 Sales Receipt Wrapper Methods

sendPrefilledSalesReceipt(String recordId, String companyId, String sourceObjectName, String mapCustomFields, String mapFieldMapping, String mapProductFields)String

The primary sales receipt pre-fill method. Queries the source sObject, maps configured fields onto a QBApiDataWrapper.SalesReceipt instance, resolves custom fields from QB definition IDs, resolves product line items, and additionally resolves a payment method and a deposit account — both of which have no equivalent on the invoice controller.

Phase 1 — Field collection:

  • Merges SF field names from mapFieldMapping values, the product mapFieldMapping (when no query is supplied), and mapCustomFields values into a single setFields

  • Executes a single dynamic SOQL against sourceObjectName WHERE Id = :recordId

Phase 2 — Sales receipt population:

  • Calls oSalesReceipt.populateField(key, value) for each mapped field

  • Default txnDate = today if not mapped

  • Note: unlike the invoice controller, there is no default dueDate — sales receipts are point-of-sale documents and have no due date field

  • Falls back to createEmptySalesReceiptWrapper() when mapFieldValue ends up empty (no fields resolved)

Phase 3 — Custom fields:

  • Queries cm_finance__Quickbooks_Custom_Field__c WHERE Definition_Id__c IN mapCustom.keySet()

  • Builds lstCustomFields with {definitionId, customFieldName, value} for each

  • Phase 4 — Product line items:

Two paths depending on whether mapProductFields.query is supplied:

  • Query path: parses the child SOQL template and lookupFieldName, executes it against the parent record, and resolves each row's SF Product ID to a QB Product — checking direct Quickbooks_Product__c.Id matches first, then falling back to Parent_Record_Id__c matches for any remaining IDs

  • Single-record path: when no query is supplied, resolves the product reference directly off the already-queried source object using the same direct-then-fallback resolution

Phase 5 — Payment method (no invoice equivalent):

If mapFields contains paymentMethodLookupFieldName, reads that field off the source object and returns the raw paymentMethodId (SF record Id) — no server-side resolution to a QB record here

Phase 6 — Deposit account (no invoice equivalent):

If mapFields contains depositAccountLookupFieldName, reads that field off the source object and returns the raw depositAccountId (SF record Id) in the same way



Access

@AuraEnabled

Returns

JSON {bSuccess, salesReceiptWrapper?, salesReceiptPopulated?, productsPopulated?, productsArray?, paymentMethodId?, depositAccountId?, message}

createEmptySalesReceiptWrapper(String mapCustomFields, String companyId, String parentRecordId)String

Creates a blank sales receipt wrapper with sensible defaults. Used when there is no field mapping configured or as a fallback from sendPrefilledSalesReceipt.

Default values:

  • txnDate = System.today()

  • deposit = 0

  • billingEmail = '', paymentRefNum = ''

  • Billing address fields initialized to empty strings

  • customerRef, customerMemo, paymentMethodRef, depositToAccountRef initialized as empty AccountReference shells (no shippingAddress shell, unlike the invoice controller's empty-wrapper method)

  • If mapCustomFields is provided, queries the source object and custom field definitions to pre-populate lstCustomFields with empty or mapped values



Access

@AuraEnabled

Returns

JSON {isSuccess: true, salesReceiptWrapper} or {isSuccess: false, message}

6.3 Sales Receipt Create / Sync Methods

createSalesReceiptInQuickbooks(String sSalesReceiptWrapper, String parentRecordId, String companyId, String companyRecordId)String

The main sales receipt creation method. Deserializes the LWC's form state, builds a typed QBApiDataWrapper.SalesReceipt via createSalesReceiptWrapperAPI(), calls the QB API, and persists the result in Salesforce. Structurally parallel to createInvoiceInQuickbooks, with two extra lookup types (payment method, deposit account) resolved alongside customer/product/class/custom field.

Pre-condition: mapSalesReceipt.get('Products') must be non-null — sales receipts require at least one line item.

Phase 1 — API callout:

  • Builds QBApiDataWrapper.SalesReceipt via private createSalesReceiptWrapperAPI(mapSalesReceipt)

  • Calls WS_Quickbooks.createSalesReceipt(oRequestWrapper)

Phase 2 — Lookup resolution (on success):

Collects QB IDs for: customers, products, classes, custom field definition IDs, payment methods, deposit accounts

Resolves each to SF record IDs via batch queries:

  • Quickbooks_Customer__c WHERE Quickbooks_Id__c IN setCustomerQBId

  • Quickbooks_Product__c WHERE Quickbooks_Id__c IN setProductQBId

  • Quickbooks_Class__c WHERE Quickbooks_Id__c IN setClassQBId

  • Payment_Method__c WHERE Quickbooks_Id__c IN setPaymentMethodQBId

  • Quickbooks_Account__c WHERE Quickbooks_Id__c IN setDepositAccountQBId

  • Quickbooks_Custom_Field__c WHERE Definition_Id__c IN setCustomFieldId

Phase 3 — Custom field decoding:

  • Raw format stored by QB: DefinitionId::Value

  • After resolution: Custom_Field_N__c set to the value, Quickbooks_Custom_Field_N__c set to the SF Custom Field record ID (three fixed slots — Custom_Field_1/2/3 — same as the invoice controller)

Phase 4 — Record persistence:

  • Sets Composite_Unique_Key__c = QB_Id::SalesReceipt::RealmId

  • Sets Ignore_Webhook__c = true

  • Sets initial Status__c = 'Sales Receipt PDF Pending' if storePDF = true, else 'Awaiting Salesforce Sync'

  • data.create(lstAllSalesReceipt) → then data.create(lstAllLineItems) (sequential, not upsert)

  • On any failure: Creates a Failed sales receipt record with the error message



Access

@AuraEnabled

Required guard

Products list must not be null

Returns

JSON {isSuccess, message?, apiResponse?}

Error record

Always created on any failure path

populateParentRecordIdOnSalesReceipt(String recordId, String parentRecordId)void

Sets Parent_Record_ID__c on an existing cm_finance__Quickbooks_Sales_Receipt__c record. Called from the LWC when the user manually links an existing QB sales receipt to the SF record via record picker.



Access

@AuraEnabled

DML

data.modify()

syncSalesReceiptFromQuickbooks(String recordId, String companyId, String companyRecordId)Map<String, Object>

Retrieves the most recent QB Sales Receipt for the SF record from QuickBooks Online and syncs it back into Salesforce with full line item reconciliation. Structurally parallel to syncInvoiceFromQuickbooks.

Phase 1 — Identify target:

  • Queries cm_finance__Quickbooks_Sales_Receipt__c WHERE Parent_Record_ID__c = :recordId AND Quickbooks_Id__c != null AND Quickbooks_Company__c = :companyRecordId ORDER BY CreatedDate DESC LIMIT 1

  • Builds QB SQL: SELECT * FROM SalesReceipt WHERE Id IN ('<QB_Id>')

Phase 2 — Lookup resolution (same as createSalesReceiptInQuickbooks):

  • Customer, product, class, payment method, deposit account, and custom field definition QB IDs → SF records

  • Custom field decoded from DefinitionId::Value format

Phase 3 — Line item reconciliation:

  • Before the callout, loads all existing line items into mapSRLineItem (keyed by Composite_Unique_Key__c); items without a key go into lstDeleteLineItem

  • After callout: upserts sales receipt on Composite_Unique_Key__c via Data.upsurt

  • For incoming line items: matched against mapSRLineItem by key — a match reuses the existing Id (update), no match is treated as a new insert; then upserted on Composite_Unique_Key__c

  • Orphaned line items (in SF but not in QB response): archived (Archived__c = true) or hard-deleted based on deleteExtraLineItemsSetting() custom setting — same setting-driven branch as the invoice controller



Access

@AuraEnabled

Callout

WS_Quickbooks.querySalesReceipt(oWrapper)

Returns

{isSuccess, message}

Orphan handling

Setting-driven: archive or hard delete

6.4 Private Helper

createSalesReceiptWrapperAPI(Map<String, Object> mapSalesReceipt) → QBApiDataWrapper.SalesReceipt (private)

Deserializes the raw LWC JSON map (passed from createSalesReceiptInQuickbooks) into a fully typed QBApiDataWrapper.SalesReceipt object ready for the API callout. Broader than the invoice controller's equivalent — it additionally resolves paymentMethodRef and depositToAccountRef by record ID lookup, and has no dueDate, deposit-terms, or Cust_QBID/CustName mapping notes beyond what invoice uses.

LWC Map Key

SalesReceipt Field

Notes

billingEmail

oSalesReceipt.billingEmail

Direct string

txnDate

oSalesReceipt.txnDate

Direct string

deposit

oSalesReceipt.deposit

Decimal

paymentRefNum

oSalesReceipt.paymentRefNum

Direct string

Products[].ProductRecordId

salesItemDetail.itemRef.value/name

Resolved via Quickbooks_Product__c query

Products[].qbClassId

salesItemDetail.classRef.value

Direct string

Products[].ProductQuantity

salesItemDetail.quantity

Decimal

Products[].ProductUnitPrice

salesItemDetail.unitPrice

Decimal

Products[].ProductAmount

oLine.amount

Decimal

Products[].ProductDescription

oLine.description

String

customerRef.Cust_QBID

customerRef.value

QB Customer ID

customerRef.CustName

customerRef.name

Customer display name

customerRef.CustRecordId

customerRef.recordId

SF Customer record ID

customerMemo.value

customerMemo.value

Memo string

paymentMethodRef.recordId

paymentMethodRef.value/name/recordId

Resolved via Payment_Method__c query — no invoice equivalent

depositToAccountRef.recordId

depositToAccountRef.value/name/recordId

Resolved via Quickbooks_Account__c query — no invoice equivalent

lstCustomFields[].definitionId + value

lstCustomFields

Only non-null pairs included

billingAddress.*

billingAddress.*

5 address fields

shippingAddress.*

shippingAddress.*

5 address fields

Line items are numbered sequentially starting at 1 via a count variable, and each is given detailType = 'SalesItemLineDetail' explicitly (set unconditionally, unlike the invoice controller where this is implied elsewhere).


6. End-to-End Flow Diagrams

6.1 Invoice Push (Full Path)

User: clicks "Push Invoice" on SF Record Page
           │
           ▼
[LWC] checkExistingInvoice()
           │
    ┌──────┴──────┐
   exists?       not exists
    │                  │
"Already exists"  [LWC] fetchRelatedCustomerFromParent()
 show warning          │
    │          ┌───────┴────────────────┐
    │     QB customer               No QB customer
    │     found                     found
    │          │                        │
    │    [LWC] getTermsOptions()    Show "create customer"
    │    [LWC] getQuickbooksClasses()   screen (end)
    │    [LWC] sendPrefilledInvoice()
    │          │
    │    Render invoice form
    │          │
    │    User fills/edits form
    │          │
    │    [LWC] validateBusinessRules()
    │          │ fails → show errors
    │          │ passes →
    │    [LWC] createInvoiceInQuickbooks()
    │          │
    │    ┌─────┴──────┐
    │  Success      Failure
    │    │              │
    │  SF records   Failed invoice record
    │  created      created in SF
    │    │
    └────▶ Success toast → navigate to home screen

6.2 Product Retrieve (Pull Path)

User: clicks "Retrieve" on Product tab
           │
           ▼
[LWC] validateQBId(recordId, companyId)
           │
    ┌──────┴──────────────┐
QB_Id field found    QB_Id field not found
    │                        │
[LWC] syncProductsFromQuickbooks()   Show lightning-record-picker
    │                        │
[QB API] queryProduct()      User selects existing QB product
    │                        │
Upsert SF product       populateParentOnProduct(selectedId, recordId)
on Composite_Unique_Key__c   │
    │                   Success toast
Success toast

6.3 Sales Receipt Push (Full Path)

User: clicks "Push Sales Receipt" on SF Record Page
           │
           ▼
[LWC] checkExistingSalesReceipt()
           │
    ┌──────┴──────┐
   exists?       not exists
    │                  │
"Already exists"  [LWC] fetchRelatedCustomerFromParent()
 show warning          │
    │          ┌───────┴─────────────────────────────┐
    │     QB customer                         No QB customer
    │     found                                   found
    │          │                                     │
    │    [LWC] getQuickbooksClasses()       Show "create customer"
    │    [LWC] sendPrefilledSalesReceipt()      screen (end)
    │          │
    │    Render sales receipt form
    │          │
    │    User fills/edits form
    │          │
    │    [LWC] validateBusinessRules()
    │          │ fails → show errors
    │          │ passes →
    │    [LWC] createSalesReceiptInQuickbooks()
    │          │
    │    ┌─────┴──────┐
    │  Success      Failure
    │    │              │
    │  SF records   Failed sales receipt record
    │  created      created in SF
    │    │
    └────▶ Success toast → navigate to home screen


7. Shared Data Model

7.1 QBApiDataWrapper.Invoice

Field

Type

Description

billingEmail

String

Email for QB invoice delivery

txnDate

String

Invoice date (YYYY-MM-DD)

dueDate

String

Payment due date (YYYY-MM-DD)

terms

String

Payment terms label (e.g., "Net30")

deposit

Decimal

Deposit amount

customerRef

AccountReference

{value: QB_Id, name, recordId}

customerMemo

AccountReference

{value: memo string}

billingAddress

Address

Street, city, state, postal, country

shippingAddress

Address

Street, city, state, postal, country

lstLines

List<Line>

Line items

lstCustomFields

List<CustomField>

QB custom fields

7.2 QBApiDataWrapper.SalesReceipt

Field

Type

Description

billingEmail

String

Email for QB invoice delivery

txnDate

String

Invoice date (YYYY-MM-DD)

deposit

Decimal

Deposit amount

depositToAccountRef

AccountReference

{value: QB_Id, name, recordId}

paymentRefNum

String

Payment Reference number

customerRef

AccountReference

{value: QB_Id, name, recordId}

customerMemo

AccountReference

{value: memo string}

billingAddress

Address

Street, city, state, postal, country

shippingAddress

Address

Street, city, state, postal, country

lstLines

List<Line>

Line items

lstCustomFields

List<CustomField>

QB custom fields

7.2 QBApiDataWrapper.Product

Field

Type

Description

name

String

Product/service name

typeOfProduct

String

Service, Inventory, NonInventory

description

String

Sales description

purchaseDescription

String

Purchase description

salesPrice

Decimal

Unit sales price

purchaseCost

Decimal

Purchase cost

sku

String

SKU code

qty

Decimal

Quantity on hand (Inventory only)

invStartDate

String

Inventory start date (Inventory only)

incomeAccount

AccountReference

{value: QB_Id, name, recordId: SF_Id}

expenseAccount

AccountReference

{value: QB_Id, name, recordId: SF_Id}

assetAccount

AccountReference

{value: QB_Id, name, recordId: SF_Id}

7.3 Composite_Unique_Key__c Format

Entity

Format

Customer

QB_Id::Customer::RealmId

Vendor

QB_Id::Vendor::RealmId

Product

QB_Id::Product::RealmId

Account

QB_Id::Account::RealmId

Invoice

QB_Id::Invoice::RealmId

Invoice Line Item

QBProductId::InvoiceId::QB_Id

Sales Receipt

QB_Id::SalesReceipt::RealmId

Sales Receipt Line Item

QBProductId::SalesReceiptId::QB_Id

Payment Method

QB_Id::PaymentMethod::RealmId


8. Configuration Reference

Setting

Type

Key/Label

Effect

Company active flag

Custom Object field

Active__c = true

Only active companies shown in selector

Company default

Custom Object field

Default__c DESC

Default company auto-selects when only one

Batch size

Custom Setting (Quickbooks_Common_Settings__c)

BatchSize

Controls batch size for webhook batches (not used here)

Generate PDF

Custom Setting

GenerateInvoicePDF

and GenerateSalesReceiptPDF

When true, initial invoice status = 'Invoice PDF Pending'; when false = 'Awaiting Salesforce Sync'. Same in case for the Sales Receipt the initial status will be 'Sales Receipt PDF Pending'

Show QB Classes

Custom Setting

ShowClassInLWC

When true, QB Class column visible on invoice line items

Default due days

Custom Setting

(via CMQBConnectorUtility.getDefaultDaysForDueDateInvoice())

Default days added to invoice date for due date

Terms options

Custom Metadata (CM_QB_Connector_Settings__mdt)

MasterLabel per terms config

JSON {label: days} — populates invoice Terms combobox

Customer lookup field

Custom Field Mapping (Custom_Field_Mapping__c)

Source_Invoice_Customer_Lookup_field__c

Field on the source sObject that points to the QB Customer's SF parent record

FLS enforcement

Code constant

CMQBConnectorUtility.enforceFLS

When true, all SOQL and DML respect field-level security

Delete extra line items

Custom Setting

deleteExtraLineItemsSetting()

Controls whether orphaned line items are hard-deleted or archived during invoice sync


9. Account Filter Criteria (Product Form)

The lightning-record-picker components in the product form use server-side filter criteria passed from the LWC to restrict which QB Accounts are selectable:

Account Field

Filter Criteria

Income Account

Type__c = 'Income'

Expense Account

Type__c IN ('Expense', 'Cost of Goods Sold')

Asset Account

Type__c = 'Other Current Asset' AND SubType__c = 'Inventory'


10. Error Handling Patterns

10.1 Failed Record Creation

All push methods (Customer, Vendor, Product, Invoice) follow a consistent pattern on failure: a record with Status__c = 'Failed' is always inserted in Salesforce, capturing the error message. This ensures failed pushes are visible and auditable in the SF org without requiring log inspection.

10.2 Validation Guards

createInvoiceInQuickbooks fails fast (before any callout) if:

  • sInvoiceWrapper is blank

  • parentRecordId is blank

  • companyId is blank

  • mapInvoice.get('Products') is null or empty

The LWC-side validateBusinessRules() provides a second layer of validation before the Apex call is made.

10.3 Echo Suppression

All records created by Push operations set Ignore_Webhook__c = true. This flag is checked by Webhook_QBChangeHandlerQuickbooksIntegrationHelper to prevent the resulting QB change notification from triggering a redundant re-sync back into Salesforce.


11. Design Decisions

11.1 Screen State Machine via Boolean Flags

Rather than a router or navigation component, cmQBConnectorMaster uses a flat set of boolean flags to manage screen visibility. This pattern avoids LWC child component instantiation overhead and keeps all screen state in a single JS class context, which simplifies data passing between screens. The tradeoff is that the HTML template becomes large and all screen logic is in one file.

11.2 Composite_Unique_Key__c as Upsert Key

Rather than using the QB-assigned ID as the external upsert key directly, the connector uses a compound key QB_Id::EntityType::RealmId. This ensures multi-company safety: two companies can have customers with the same QB ID, and the compound key disambiguates them. The ::EntityType:: segment also provides implicit self-documentation on the record.

11.3 Create and Update on the Same Code Path

For Customer, Vendor, and Product push, the code checks for an existing QB record via getRelatedQuickbooks*() helpers and — if one exists — populates QB_Id and SyncToken on the request wrapper. The QB API interprets a request with an existing Id + SyncToken as an update. This means a user clicking "Push" on a record that already exists in QB will correctly update it rather than create a duplicate, with no separate code path required.

11.4 Pre-fill vs Empty Invoice Path

sendPrefilledInvoice is called when a field mapping is configured for the source sObject. If the mapping returns no fields (or no mapping exists), it falls back to createEmptyInvoiceWrapper. This graceful degradation means the component always renders a usable form even when the admin has not completed the mapping setup.

11.5 Sequential Invoice + Line Item Insert

Invoice records are inserted with data.create() (not upserted) on the creation path because the Salesforce record ID is needed as the parent lookup before line items can be created. Upsert on Composite_Unique_Key__c is used only on the sync path where the record may already exist.

11.6 Custom Field Encoding

QB returns custom field values with a DefinitionId::Value encoded format in the raw response. createInvoiceInQuickbooks and syncInvoiceFromQuickbooks both decode this format after the API call, splitting on :: to separate the definition ID (for SF lookup resolution) from the actual value.