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 │ │ QBBillController ││
│ │ │ │ │ │ Controller │ │ ││
│ │ • Companies │ │ • checkExistingInvoice│ │ • checkExisting │ │ • checkExistingBill││
│ │ • Customers │ │ • fetchRelatedCustomer││ SalesReceipt │ │ • fetchRelatedVendor││
│ │ • Vendors │ │ • sendPrefilledInvoice││ • fetchRelatedCustomer│ │ FromParent ││
│ │ • Products │ │ • createInvoice... │ │ • sendPrefilled │ │ • sendPrefilledBill││
│ │ • Accounts │ │ • syncInvoice... │ │ SalesReceipt │ │ • createBillIn ││
│ │ │ │ │ │ • createSalesReceipt │ │ Quickbooks ││
│ │ │ │ │ │ • 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 Bill Push Flow
Unlike Invoice (inline in cmQBConnectorMaster) or Sales Receipt (delegated to salesReceiptCreator), Bill push is delegated to the child component billCreator, shown when billCreatorVisible = true.
User clicks "Push Bill" → showBillCreator() sets billCreatorVisible = true
•
billCreator (child) connectedCallback → init()
•
├─ existingVendorFound → checkExistingBill()
•
└─ else → fetchRelatedVendorFromParent(recordId, companyRecordId)
├─ Success → existingVendorWrapper populated → checkExistingBill()
├─ No QB vendor found (vendSourceOnBill returned) → show createNewVendor screen
│ • user confirms → createVendorsInQuickbooks(...) → createVendorWrapper(existingVendorId) → checkExistingBill()
└─ Other failure → show error, close modal
checkExistingBill(recordId, companyRecordId)
├─ Bill already exists → show "already created" state, stop
└─ Not exists:
├─ mappings.prefillBill = true → fetchPrefillBill()
│ └─ sendPrefilledBill(recordId, companyId, sourceObjectName, mapCustomFields, mapFieldMapping, mapProductFields)
│ populates billWrapper + line items from mapped source sObject fields
└─ mappings.prefillBill = false → createEmptyBillWrapper(mapCustomFields, companyId, parentRecordId)
blank bill: today's date, default "Net 30" terms, calculated due date
→ applyVendorToWrapper() binds vendorRef onto billWrapper
→ getQuickbooksClasses(companyId) populates Class combobox (if any)
→ Render billSection
3.11 Bill Submission
User clicks "Create Bill"
•
└─ handleCreateBill()
└─ createBillInQuickbooks(sBillWrapper, parentRecordId, companyId, companyRecordId)
└─ Returns {isSuccess, message, apiResponse}
Note: unlike Sales Receipt Submission, billCreator has no client-side
validateBusinessRules() step — the wrapper is submitted as-is.
3.12 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)
│ Bill → syncBillFromQuickbooks(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)
Bill → populateParentRecordIdOnBill(recordId, parentRecordId)
3.13 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 |
|
|
Object |
Live bill data model bound to the form (billCreator child component) |
3.14 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.15 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— Reads theGenerateSalesReceiptPDFcustom setting and controls the initial Sales Receipt status. -
enforceFLS— FromCMQBConnectorUtility.enforceFLS.
The controller is the AuraEnabled backend for the Create/View QB Sales Receipt LWC flow. It supports dynamic field mapping for Sales Receipt creation, including source-record fields, custom fields, product/line-item mappings, payment methods, deposit accounts, and QuickBooks Classes.
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 Salesforce parent record and company. The LWC calls this first when the user clicks “Push Sales Receipt” to decide whether to show a duplicate warning.
|
Access |
|
|
Returns |
|
fetchRelatedCustomerFromParent(String recordId, String companyRecordId) → Map<String, Object>
Resolves the QB Customer linked to a Sales Receipt source record using the configured Sales Receipt customer lookup field mapping.
Resolution chain:
Custom_Field_Mapping__c (Mapping_Type__c = 'SalesReceipt')
→ configured Sales Receipt customer lookup field
→ Source sObject[customerLookupField]
→ Quickbooks_Customer__c[Parent_Record_ID__c = customerLookupValue]
→ createCustomerWrapper(QB Customer Id)
checkExistingQBCustomer(String recordId, String mapFields) → Map<String, Object>
Checks the configured QuickBooks Customer lookup field on the source record and returns the related QB Customer wrapper when the source record directly stores a QB Customer lookup.
createCustomerWrapper(String recordId) → String
Retrieves a cm_finance__Quickbooks_Customer__c record and converts it into a QBApiDataWrapper.Customer JSON wrapper for the Sales Receipt LWC.
getQuickbooksClasses(String companyId) → List<cm_finance__Quickbooks_Class__c>
Returns active QuickBooks Classes for the selected QuickBooks company when the ShowClassInLWC setting is enabled.
6.2 Sales Receipt Wrapper Methods
sendPrefilledSalesReceipt(String recordId, String companyId, String sourceObjectName, String mapCustomFields, String mapFieldMapping, String mapProductFields) → String
Reads configured field mappings from the source record and returns a pre-filled Sales Receipt wrapper. It also resolves configured custom fields, products, payment method, and deposit account values.
-
Merges source-record fields from field mappings, custom field mappings, and line-item mappings into one query field set.
-
Populates the Sales Receipt wrapper using mapped source field values.
-
Builds
lstCustomFieldsfrom QuickBooks custom field definition mappings. -
Builds line-item data by resolving mapped Salesforce products to
cm_finance__Quickbooks_Product__crecords. -
Resolves payment method and deposit account values when configured.
createEmptySalesReceiptWrapper(String mapCustomFields, String companyId, String parentRecordId) → String
Creates an empty Sales Receipt wrapper with default values and populates configured custom fields. It is used when there is no pre-fill mapping or as a fallback from sendPrefilledSalesReceipt.
6.3 Sales Receipt Create / Sync Methods
createSalesReceiptInQuickbooks(String sSalesReceiptWrapper, String parentRecordId, String companyId, String companyRecordId) → String
Creates a Sales Receipt in QuickBooks using the supplied Sales Receipt wrapper, creates the corresponding Salesforce Sales Receipt and line item records, and maps related Customer, Product, Class, Payment Method, Deposit Account, and Custom Field records.
-
Deserializes the LWC form state into a typed Sales Receipt request wrapper.
-
Calls
WS_Quickbooks.createSalesReceipt(). -
Sets
Composite_Unique_Key__c = QB_Id::SalesReceipt::RealmId. -
Sets
Ignore_Webhook__c = trueto suppress the round-trip webhook. -
Sets the initial status to
'Sales Receipt PDF Pending'when PDF generation is enabled; otherwise it uses'Awaiting Salesforce Sync'. -
Creates the Sales Receipt and related line-item records in Salesforce.
populateParentRecordIdOnSalesReceipt(String recordId, String parentRecordId) → void
Updates the Parent Record ID on an existing QuickBooks Sales Receipt record. This is called when the user manually links an existing QB Sales Receipt to the Salesforce source record through the LWC retrieve flow.
syncSalesReceiptFromQuickbooks(String recordId, String companyId, String companyRecordId) → Map<String, Object>
Retrieves the latest linked Sales Receipt from QuickBooks and syncs it back into Salesforce with line item reconciliation.
-
Queries the latest linked Sales Receipt and builds a QuickBooks query for its QB Id.
-
Resolves Customer, Product, Class, Payment Method, Deposit Account, and Custom Field references to Salesforce records.
-
Upserts the Sales Receipt by
Composite_Unique_Key__c. -
Upserts incoming line items and archives or deletes orphaned lines based on
CMQBConnectorUtility.deleteExtraLineItemsSetting().
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 QuickBooks API callout.
Key mappings: customer reference, billing/shipping addresses, transaction date, deposit amount, payment reference number, deposit-to account reference, line items, classes, memo, and custom fields.
Usage:
// LWC: @wire or imperative call
QBSalesReceiptController.checkExistingSalesReceipt(recordId, companyRecordId);
7. Apex Controller — QBBillController
Class declaration: public with sharing class QBBillController
Static properties:
-
enforceFLS— FromCMQBConnectorUtility.enforceFLS
Note: unlike QBInvoiceController, this class has no storePDF property (Bills have no PDF-generation step) and no getTermsOptions/checkExistingQBCustomer-equivalent methods — vendor resolution has a single path only.
7.1 Pre-Flight Check Methods
checkExistingBill(String recordId, String companyRecordId) → Map<String, Object>
Checks whether a non-Failed QB Bill already exists for the given SF record + company combination. Bill-side counterpart to checkExistingInvoice — the LWC calls this first when the user clicks "Push Bill" to decide whether to show a duplicate warning.
|
Access |
|
|
Query |
|
|
Returns |
|
fetchRelatedVendorFromParent(String recordId, String companyRecordId) → Map<String, Object>
Resolves the QB Vendor linked to a bill's source record by traversing a field-mapping chain — the vendor-side equivalent of fetchRelatedCustomerFromParent. This is the only vendor resolution path (there is no direct-lookup alternate like checkExistingQBCustomer on the Invoice side).
Resolution chain:
Custom_Field_Mapping__c (Mapping_Type__c = 'Bill')
→ Source_Invoice_Customer_Lookup_field__c ← same field API name reused from Invoice mapping
→ Source sObject[vendorLookupField]
→ Quickbooks_Vendor__c[Parent_Record_ID__c = vendorLookupValue]
→ createVendorWrapper(QB Vendor Id)
Note: the field that stores the vendor lookup API name on the mapping record is literally cm_finance__Source_Invoice_Customer_Lookup_field__c — the same field used for Invoice customer mapping, disambiguated only by cm_finance__Mapping_Type__c = 'Bill'.
|
Response Key |
Meaning |
|---|---|
|
|
Vendor found and serialized |
|
|
SF source found but no linked QB vendor — triggers "create vendor first" screen |
|
|
Vendor lookup resolved but |
|
|
Mapping misconfiguration or missing field |
|
Access |
|
|
Mapping source |
|
createVendorWrapper(String recordId) → String
Serializes a cm_finance__Quickbooks_Vendor__c record into a QBApiDataWrapper.Vendor JSON string for use by the bill form's vendor reference field.
Fields serialized: Company_Name__c, Display_Name__c, First_Name__c, Last_Name__c, Phone__c, Email__c, Website__c, billing address (5 fields: Street__c, State_Code__c, Postal_Code__c, City__c, Country__c), Quickbooks_Id__c, Id
Note: Vendor has only a billing address, no shipping address — unlike createCustomerWrapper on the Invoice side.
|
Access |
|
|
Returns |
JSON-serialized |
getQuickbooksClasses(String companyId) → List<cm_finance__Quickbooks_Class__c>
Identical contract to the Invoice controller's method of the same name. Returns active QB Class records for the company. Returns null without querying if the ShowClassInLWC custom setting is not true.
|
Access |
|
|
Guard |
|
|
Query |
|
7.2 Bill Wrapper Methods
sendPrefilledBill(String recordId, String companyId, String sourceObjectName, String mapCustomFields, String mapFieldMapping, String mapProductFields) → String
The primary bill pre-fill method — bill-side counterpart to sendPrefilledInvoice, built around Lines (expense lines) instead of Invoice's product line items, with vendor + AP account in place of customer.
Phase 1 — Field collection:
-
Merges all SF field names from
mapFieldMappingvalues,mapProductFields.fieldMappingvalues, andmapCustomFieldsvalues into a singlesetFields -
Executes a single dynamic SOQL against
sourceObjectName WHERE Id = :recordId
Phase 2 — Bill population:
-
Calls
oBill.populateField(key, value)for each mapped field -
vendorRefandapAccountRefinitialized as emptyAccountReferenceobjects up front -
Default
txnDate= today if not mapped -
Unlike Invoice, there is no default
dueDatecalculation here —getDefaultDaysForDueDateInvoice()is Invoice-only.
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 — Line items:
-
Parses
mapProductFields.query(dynamic child SOQL template) andmapProductFields.lookupFieldName, or falls back to the already-queried source record if no child query is configured -
Resolves SF Product IDs → QB Product records via
cm_finance__Quickbooks_Product__c, first by directIdmatch, then byParent_Record_Id__cfor any remaining unmatched IDs -
Returns
productsArrayas list of{ProductRecordId, productQBId, ...}plus any other mapped fields (qty, rate, amount, etc.)
Fallback: If no fields resolve from the source query, delegates to createEmptyBillWrapper().
|
Access |
|
|
Returns |
JSON |
createEmptyBillWrapper(String mapCustomFields, String companyId, String parentRecordId) → String
Creates a blank bill wrapper with sensible defaults. Used when there is no field mapping configured or as a fallback from sendPrefilledBill.
Default values:
-
txnDate=System.today() -
privateNote='',docNumber='' -
vendorRefandapAccountRefinitialized as emptyAccountReferenceobjects -
No address defaults — unlike
createEmptyInvoiceWrapper, the Bill wrapper carries no billing/shipping address fields of its own -
If
mapCustomFieldsis provided, queries the source object and custom field definitions to pre-populatelstCustomFieldswith empty or mapped values
|
Access |
|
|
Returns |
JSON |
7.3 Bill Create / Sync Methods
createBillInQuickbooks(String sBillWrapper, String parentRecordId, String companyId, String companyRecordId) → String
The main bill creation method. Deserializes the LWC's bill form state, builds a typed QBApiDataWrapper.Bill via createBillWrapperAPI(), calls the QB API, and persists the result in Salesforce.
Pre-condition: mapBill.get('Lines') must be non-null — bills require at least one line. (Note the key is Lines, not Products as on the Invoice side, reflecting Bill's dual line-type model.)
Phase 1 — API callout:
-
Builds
QBApiDataWrapper.Billvia privatecreateBillWrapperAPI(mapBill) -
Calls
WS_Quickbooks.createBill(oRequestWrapper)
Phase 2 — Lookup resolution (on success):
-
Collects QB IDs for: vendors, AP accounts, GL accounts, products, classes
-
No custom-field-definition-id resolution here — unlike
createInvoiceInQuickbooks, this method does not decode/resolve custom fields on the returned Bill record. -
Resolves each to SF record IDs via batch queries:
Quickbooks_Vendor__c,Quickbooks_Account__cfor AP/GL accounts,Quickbooks_Product__c, andQuickbooks_Class__c.
Phase 3 — Record persistence:
-
Sets
Composite_Unique_Key__c = QB_Id::Bill::RealmId -
Sets
Ignore_Webhook__c = true -
Sets initial
Status__c = 'Awaiting Salesforce Sync'unconditionally — no PDF-pending branch, since Bills have no PDF generation step -
data.create(lstAllBill)→ thendata.create(lstAllLineItems)(sequential, not upsert) -
On any failure: Creates a Failed bill record with the error message via
CMQBConnectorUtility.createBillRecord
populateParentRecordIdOnBill(String recordId, String parentRecordId) → void
Sets Parent_Record_ID__c on an existing cm_finance__Quickbooks_Bill__c record. Bill-side counterpart to populateParentRecordIdOnInvoice — called from the LWC when the user manually links an existing QB bill to the SF record via record picker.
syncBillFromQuickbooks(String recordId, String companyId, String companyRecordId) → Map<String, Object>
Retrieves the most recent QB Bill for the SF record from QuickBooks Online and syncs it back into Salesforce with full line item reconciliation.
-
Queries the latest linked bill, then builds QB SQL:
SELECT * FROM Bill WHERE Id IN ('<QB_Id>') -
Resolves Vendor, AP Account, GL Account, Product, and Class QB IDs to SF records.
-
Reconciles line items by
Composite_Unique_Key__c; incoming lines are upserted and orphaned lines are archived or hard-deleted based onCMQBConnectorUtility.deleteExtraLineItemsSetting().
|
Access |
|
|
Callout |
|
|
Returns |
|
|
Orphan handling |
Setting-driven: archive or hard delete |
7.4 Private Helper
createBillWrapperAPI(Map<String, Object> mapBill) → QBApiDataWrapper.Bill (private)
Deserializes the raw LWC JSON map (passed from createBillInQuickbooks) into a fully typed QBApiDataWrapper.Bill object ready for the API callout. Bill-side counterpart to createInvoiceWrapperAPI, but built around a dual line-type model (Item-based vs. Account-based expense lines) instead of Invoice's single sales-item-line type.
Note: unlike createInvoiceWrapperAPI, this method does not read lstCustomFields from mapBill — custom fields populated earlier in sendPrefilledBill/createEmptyBillWrapper are not carried through into the QB API payload.
Key mappings:
|
LWC Map Key |
Bill Field |
Notes |
|---|---|---|
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Direct string |
|
|
|
Carries the QB term name chosen in the UI; the Id ( |
|
|
|
QB Vendor ID |
|
|
|
Falls back to |
|
|
|
SF Vendor record ID |
|
|
|
Resolved via a fresh |
|
|
|
Resolved via |
|
|
|
Resolved via |
|
|
|
Direct string, shared across both line detail types |
|
|
|
Optional "billable to customer" reference on the line, resolved via |
|
|
|
Decimal |
|
|
|
String |
Line items are numbered sequentially starting at 1 via a count variable — incremented for both Item-based and Account-based lines.
8. End-to-End Flow Diagrams
8.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
8.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
8.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
8.4 Bill Push workflow
User: clicks "Push Bill" on SF Record Page
│
▼
[LWC] checkExistingBill()
│
┌──────┴──────┐
exists? not exists
│ │
"Already exists" [LWC] fetchRelatedVendorFromParent()
show warning │
│ ┌───────┴─────────────────────────────┐
│ QB vendor No QB vendor
│ found found
│ │ │
│ [LWC] getQuickbooksClasses() Show "create vendor"
│ [LWC] sendPrefilledBill() screen (end)
│ │
│ Render bill form
│ │
│ User fills/edits form
│ │
│ [LWC] validateBusinessRules()
│ │ fails → show errors
│ │ passes →
│ [LWC] createBillInQuickbooks()
│ │
│ ┌─────┴──────┐
│ Success Failure
│ │ │
│ SF records Failed bill record
│ created created in SF
│ │
└────▶ Success toast → navigate to home screen
9. Shared Data Model
9.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 |
9.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 |
9.3 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 |
|
9.4 QBApiDataWrapper.Bill
|
Field |
Type |
Description |
|---|---|---|
|
|
String |
Salesforce record ID for the linked QuickBooks Bill record |
|
|
String |
Bill document number |
|
|
Decimal |
Total bill amount returned from QuickBooks |
|
|
Decimal |
Outstanding balance on the bill |
|
|
String |
Bill transaction date (YYYY-MM-DD) |
|
|
String |
Bill due date (YYYY-MM-DD) |
|
|
String |
QuickBooks Bill ID |
|
|
String |
QuickBooks SyncToken used for updates |
|
|
String |
Private note or memo stored on the bill |
|
|
AccountReference |
|
|
|
AccountReference |
|
|
|
String |
Payment terms label selected for the bill |
|
|
AccountReference |
QuickBooks term reference; populated with the selected term name during field population |
|
|
TxnTaxDetail |
Transaction tax detail for the bill |
|
|
List<Line> |
Bill line items, including item-based and account-based expense lines |
|
|
List<CustomField> |
QB custom fields mapped onto the bill wrapper |
9.5 Composite_Unique_Key__c Format
|
Entity |
Format |
|---|---|
|
Customer |
|
|
Vendor |
|
|
Product |
|
|
Account |
|
|
Invoice |
|
|
Invoice Line Item |
|
|
Sales Receipt |
|
|
Sales Receipt Line Item |
|
|
Payment Method |
|
|
Bill |
|
|
Bill Line Item |
|
10. 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.