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 |
|
|
JS Class Name |
|
|
Template |
|
|
Location |
|
|
Apex Imports |
25+ |
3.2 Screen State Machine
The component renders exactly one screen at a time using mutually exclusive boolean flags:
|
Flag |
Screen Rendered |
Trigger |
|---|---|---|
|
|
Company picker (only when >1 company) |
|
|
|
Push/Pull tab interface |
After company selected (or single company auto-selected) |
|
|
Invoice creation form |
User clicks Push Invoice button |
|
|
Product creation form |
User clicks Push Product button |
|
|
Warning: go create QB customer first |
|
|
|
Record picker to link existing QB record |
Retrieve tab flow when no existing QB record found |
|
|
Open the child component to create sales receipt ( |
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 |
|---|---|---|
|
|
String |
SF record ID from page URL state |
|
|
String |
QB Realm ID of active company |
|
|
String |
SF record ID of QB Company |
|
|
Object |
Live invoice data model bound to the form |
|
|
Array |
Line item rows in invoice form |
|
|
Object |
Product data model bound to product form |
|
|
Object |
|
|
|
Boolean |
Drives QB Class column visibility in line items |
|
|
Object |
Field mapping for invoice pre-fill |
|
|
Object |
Custom field mapping for invoice custom fields |
|
|
Object |
Field mapping for product line items |
|
|
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 |
|
|
Query |
|
|
Returns |
List of company records or |
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:
-
Query
cm_finance__Quickbooks_Customer__cWHEREParent_Record_ID__c IN lstRecordIdsto collect existing QB IDs -
Build QB SQL:
SELECT * FROM Customer WHERE Id IN ('<id1>','<id2>',...) -
Call
WS_Quickbooks.queryCustomer(oWrapper) -
On success: call
WS_Quickbooks.createCustomerRecords()to build SF records -
Resolve sub-customer parent lookups via a secondary QB ID → SF ID map
-
Set
Composite_Unique_Key__c = QB_Id::Customer::RealmId, setIgnore_Webhook__c = true -
Upsert on
Composite_Unique_Key__cviadata.upsurt() -
On failure: insert a Failed status customer record
|
|
|
|---|---|
|
Access |
|
|
Returns |
|
|
Error record |
Created with Status = |
|
Upsert key |
|
|
Echo suppression |
|
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:
-
Resolve source sObject name from record ID
-
Call
CMQBConnectorUtility.getQBObjectMapping(objectName, 'Customer', companyRecordId)to get field mappings -
Call
getRelatedQuickbooksCustomers(lstRecordIds, companyRecordId)→Map<String, String>(parentId →QB_Id::SyncToken) -
Query source sObject fields per field mapping
-
For each record: build
QBRequestWrapperviaCMQBConnectorUtility.createCustomerRequestWrapper(); if existing QB record, populateQB_IdandQB_SyncTokenon the wrapper -
Call
WS_Quickbooks.createCustomer(oWrapper)per record -
On success: upsert SF Customer record on
Composite_Unique_Key__c; return{isSuccess: true, customerId: <SF ID>} -
On failure: insert Failed customer record
|
|
|
|---|---|
|
Access |
|
|
Create vs Update |
Determined by presence of existing QB ID — no separate code path |
|
Returns |
|
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 |
|
|
DML |
|
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 |
|---|---|
|
|
Retrieves QB Vendors by existing QB IDs; upserts in SF |
|
|
Pushes SF records as QB Vendors; supports create-or-update |
|
|
Links an existing QB Vendor record to an SF source record |
|
|
Internal helper: |
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:
-
Validate input object type is
cm_finance__Quickbooks_Account__c -
Extract existing
Quickbooks_Id__cvalues from the provided records -
Look up the company from the first record's
cm_finance__Quickbooks_Company__c -
Build QB SQL:
SELECT * FROM Account WHERE Id IN (...) -
Call
WS_Quickbooks.queryAccount(oWrapper) -
On success: upsert SF records on
Composite_Unique_Key__c = QB_Id::Account::RealmId
|
|
|
|---|---|
|
Access |
|
|
Input type |
Must be |
|
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:
-
Resolve source object name from
parentRecordId -
Call
CMQBConnectorUtility.getQBObjectMapping(objectName, 'Product', companyRecordId) -
Call
getRelatedQuickbooksProducts([parentRecordId], companyRecordId)— returns existing QB product if any -
Query source sObject fields per field mapping
-
Build
QBRequestWrapperviaCMQBConnectorUtility.createProductRequestWrapper() -
If existing QB product found: populate
QB_Id,SyncToken,name,type, and account refs (asName::QB_Id::SF_Idencoded strings) -
Ensure
incomeAccount,expenseAccount,assetAccountare always initialized (empty if not set) -
Return
{isSuccess, ProductWrapper (JSON), sMessage}
|
|
|
|---|---|
|
Access |
|
|
Failure handling |
Creates Failed product record if mapping not found |
|
Account encoding |
|
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:
-
Deserialize
wrapperJson→QBApiDataWrapper.QBRequestWrapper -
For each of
incomeAccount,expenseAccount,assetAccount: ifrecordIdis set, queryQuickbooks_Account__cto resolveNameandQuickbooks_Id__c -
Set credentials:
WS_Quickbooks.sCompanyId = oWrapper.companyRealmId -
Call
WS_Quickbooks.createProduct(oWrapper) -
On success: upsert SF product on
Composite_Unique_Key__c = QB_Id::Product::RealmId; setIgnore_Webhook__c = true; set account lookups from wrapper's account record IDs -
On failure: insert Failed product record
|
|
|
|---|---|
|
Access |
|
|
Account resolution |
Batch-queries all three account types before callout |
|
Returns |
|
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:
-
Query
Quickbooks_Product__cWHEREParent_Record_ID__c IN lstRecordIdsto collect QB IDs -
Build QB SQL:
SELECT * FROM Item WHERE Id IN (...) -
Call
WS_Quickbooks.queryProduct(oWrapper) -
On success: call
WS_Quickbooks.createProductRecords()to build SF records -
Collect all income/expense/asset account QB IDs from returned products
-
Batch-resolve account QB IDs → SF record IDs via
Quickbooks_Account__cquery -
Set account lookups,
Composite_Unique_Key__c,Ignore_Webhook__c = true -
Upsert on
Composite_Unique_Key__c
|
|
|
|---|---|
|
Access |
|
|
Account resolution |
Single batch query for all three account types |
|
Upsert key |
|
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: ReadsGenerateInvoicePDFcustom setting — controls initial invoice status -
enforceFLS: FromCMQBConnectorUtility.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 |
|
|
Query |
|
|
Returns |
|
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 |
|
|
Default |
|
|
Returns |
JSON string of |
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 |
|---|---|
|
|
Customer found and serialized |
|
|
SF source found but no linked QB customer — triggers "create customer first" screen |
|
|
Mapping misconfiguration or missing field |
|
|
|
|---|---|
|
Access |
|
|
Mapping source |
|
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 |
|
|
Returns |
|
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 |
|
|
Returns |
JSON-serialized |
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 |
|
|
Guard |
|
|
Query |
|
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
mapFieldMappingvalues andmapCustomFieldsvalues into a singlesetFields -
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
lstCustomFieldswith{definitionId, customFieldName, value}for each
Phase 4 — Product line items:
-
Parses
mapProductFields.query(a dynamic child SOQL template) andmapProductFields.lookupFieldName -
Executes child query against
parentRecordId -
Resolves SF Product IDs → QB Product records via
cm_finance__Quickbooks_Product__c -
Returns
productsArrayas list of{ProductRecordId, productQBId, qty, rate, ...}
Fallback: If mapFieldMapping is empty (no configured mapping), delegates to createEmptyInvoiceWrapper().
|
|
|
|---|---|
|
Access |
|
|
Returns |
JSON |
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 |
|
|
Returns |
JSON |
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.Invoicevia privatecreateInvoiceWrapperAPI(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__cset to the value,Quickbooks_Custom_Field_N__cset 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'ifstorePDF = true, else'Awaiting Salesforce Sync' -
data.create(lstAllInvoice)→ thendata.create(lstAllLineItems)(sequential, not upsert)
On any failure: Creates a Failed invoice record with the error message.
|
|
|
|---|---|
|
Access |
|
|
Required guard |
Products list must not be empty |
|
Returns |
JSON |
|
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 |
|
|
DML |
|
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::Valueformat
Phase 3 — Line item reconciliation:
-
Before the callout, loads all existing line items into
mapInvLineItem(keyed byComposite_Unique_Key__c); items without a key go intolstDeleteLineItem -
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 ondeleteExtraLineItemsSetting()custom setting
|
|
|
|---|---|
|
Access |
|
|
Callout |
|
|
Returns |
|
|
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 |
|---|---|---|
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Decimal |
|
|
|
Resolved via |
|
|
|
Direct string |
|
|
|
Decimal |
|
|
|
Decimal |
|
|
|
Decimal |
|
|
|
String |
|
|
|
QB Customer ID |
|
|
|
Customer display name |
|
|
|
SF Customer record ID |
|
|
|
Memo string |
|
|
|
Only non-null pairs included |
|
|
|
5 address fields |
|
|
|
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: ReadsGenerateSalesReceiptPDFcustom setting — controls initial invoice status -
enforceFLS: FromCMQBConnectorUtility.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 |
|---|---|---|
|
|
String |
Email for QB invoice delivery |
|
|
String |
Invoice date (YYYY-MM-DD) |
|
|
String |
Payment due date (YYYY-MM-DD) |
|
|
String |
Payment terms label (e.g., "Net30") |
|
|
Decimal |
Deposit amount |
|
|
AccountReference |
|
|
|
AccountReference |
|
|
|
Address |
Street, city, state, postal, country |
|
|
Address |
Street, city, state, postal, country |
|
|
List<Line> |
Line items |
|
|
List<CustomField> |
QB custom fields |
7.2 QBApiDataWrapper.SalesReceipt
|
Field |
Type |
Description |
|---|---|---|
|
|
String |
Email for QB invoice delivery |
|
|
String |
Invoice date (YYYY-MM-DD) |
|
|
Decimal |
Deposit amount |
|
|
AccountReference |
|
|
|
String |
Payment Reference number |
|
|
AccountReference |
|
|
|
AccountReference |
|
|
|
Address |
Street, city, state, postal, country |
|
|
Address |
Street, city, state, postal, country |
|
|
List<Line> |
Line items |
|
|
List<CustomField> |
QB custom fields |
7.2 QBApiDataWrapper.Product
|
Field |
Type |
Description |
|---|---|---|
|
|
String |
Product/service name |
|
|
String |
|
|
|
String |
Sales description |
|
|
String |
Purchase description |
|
|
Decimal |
Unit sales price |
|
|
Decimal |
Purchase cost |
|
|
String |
SKU code |
|
|
Decimal |
Quantity on hand (Inventory only) |
|
|
String |
Inventory start date (Inventory only) |
|
|
AccountReference |
|
|
|
AccountReference |
|
|
|
AccountReference |
|
7.3 Composite_Unique_Key__c Format
|
Entity |
Format |
|---|---|
|
Customer |
|
|
Vendor |
|
|
Product |
|
|
Account |
|
|
Invoice |
|
|
Invoice Line Item |
|
|
Sales Receipt |
|
|
Sales Receipt Line Item |
|
|
Payment Method |
|
8. Configuration Reference
|
Setting |
Type |
Key/Label |
Effect |
|---|---|---|---|
|
Company active flag |
Custom Object field |
|
Only active companies shown in selector |
|
Company default |
Custom Object field |
|
Default company auto-selects when only one |
|
Batch size |
Custom Setting ( |
|
Controls batch size for webhook batches (not used here) |
|
Generate PDF |
Custom Setting |
and |
When |
|
Show QB Classes |
Custom Setting |
|
When |
|
Default due days |
Custom Setting |
(via |
Default days added to invoice date for due date |
|
Terms options |
Custom Metadata ( |
MasterLabel per terms config |
JSON |
|
Customer lookup field |
Custom Field Mapping ( |
|
Field on the source sObject that points to the QB Customer's SF parent record |
|
FLS enforcement |
Code constant |
|
When |
|
Delete extra line items |
Custom Setting |
|
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 |
|
|
Expense Account |
|
|
Asset Account |
|
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:
-
sInvoiceWrapperis blank -
parentRecordIdis blank -
companyIdis 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_QBChangeHandler → QuickbooksIntegrationHelper 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.