/**
* @class BatchAutoMatchQBCustomers
* @description Iterates cm_finance__Quickbooks_Customer__c records where
* cm_finance__Parent_Record_ID__c != null and
* cm_finance__Auto_Matched__c = false,
* and attempts to match each to a Salesforce Account.
*
* On a successful match:
* - cm_finance__Auto_Matched__c is set to true
* - cm_finance__Auto_Matched_Salesforce_Record_ID__c is stamped with the Account Id
*
* Field mappings are stored as a JSON blob in cm_finance__CM_QB_Connector_Settings__mdt.
* Account records are loaded ONCE per execute() chunk — no
* repeated queries per QB record.
*
* Matching tiers (in JSON order):
* "Email" → normalize QB email domain,
* compare against SF Account website domain
* "Phone" → normalized phone digits
* "Company Name" → SOSL fuzzy name search
*
* Address SOSL always runs last as a silent built-in fallback.
* Sends a summary email to the running user on finish().
*
* @usage
* Database.executeBatch(new BatchAutoMatchQBCustomers(null), 1);
* Database.executeBatch(new BatchAutoMatchQBCustomers('AutoMatch_QBCustomerJson', 7), 1);
*/
/**
* @class BatchAllQBSFAccountsSync
* @description Pulls all Account records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Account__c records.
*
* When run as a Schedulable, first queries the QB API for the
* total account count, then self-dispatches as a batchable job
* sized by the Quickbooks_Common_Settings__c BatchSize value.
*
* Each execute() chunk calls the QB API with a startposition/
* maxresults page window (tracked via currentOffset), maps the
* response to cm_finance__Quickbooks_Account__c records via
* WS_Quickbooks.createAccountRecords(), builds a
* Composite_Unique_Key__c (QuickbooksId::Account::RealmId),
* and upserts using that key.
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Quickbooks Company record with lastSyncedAt and status.
*
* Duplicate-job guard: start() exits early if another instance
* of this batch is already Processing.
*
* @usage
* Database.executeBatch(new BatchAllQBSFAccountsSync(totalCount), batchSize);
* System.schedule('QB Accounts Sync', cronExpr, new BatchAllQBSFAccountsSync());
*/
/**
* @class BatchAllQBSFCustomersSync
* @description Pulls all Customer records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Customer__c records.
*
* Follows the same count-first / paginate-in-chunks pattern as
* BatchAllQBSFAccountsSync. Additionally resolves parent-child
* customer relationships using a QB-Id-to-Salesforce-Id map
* before upserting via Composite_Unique_Key__c.
*
* finish() updates cm_finance__Last_Sync_Config__c on the
* Company record and then chains BatchAutoMatchQBCustomers
* to attempt automatic Account matching on the newly synced
* customers.
*
* @usage
* Database.executeBatch(new BatchAllQBSFCustomersSync(totalCount), batchSize);
* System.schedule('QB Customers Sync', cronExpr, new BatchAllQBSFCustomersSync());
*/
/**
* @class BatchAllQBSFInvoiceSync
* @description Pulls all Invoice records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Invoice__c records, including line
* items as cm_finance__Quickbooks_Invoice_Item__c.
*
* For each page of invoices:
* - Resolves QB Customer, Product, Class, and Custom Field
* IDs to their Salesforce counterparts via pre-built maps.
* - Upserts invoices on Composite_Unique_Key__c.
* - Upserts line items on their own Composite_Unique_Key__c.
* - Removes or archives orphaned line items based on the
* deleteExtraLineItems setting.
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Company record.
*
* @usage
* Database.executeBatch(new BatchAllQBSFInvoiceSync(totalCount), batchSize);
* System.schedule('QB Invoices Sync', cronExpr, new BatchAllQBSFInvoiceSync());
*/
/**
* @class BatchAllQBSFSalesReceiptSync
* @description Performs a full, paginated pull of QuickBooks Online Sales Receipt
* transactions into Salesforce as
* cm_finance__Quickbooks_Sales_Receipt__c records, with related
* cm_finance__Quickbooks_Sales_Receipt_Item__c line items.
*
* Sales Receipts represent immediate-payment sales in QuickBooks.
* The sync preserves the transaction's customer, transaction date,
* document number, payment method, deposit account, memo, class,
* custom fields, amounts, discounts, tax, and total values whenever
* those fields are supported by the Salesforce data model.
*
* For each QuickBooks page, the batch:
* - Queries the SalesReceipt endpoint using startPosition and
* maxResults pagination.
* - Resolves QB Customer, Product/Item, Class, Tax, and Custom
* Field IDs to their Salesforce counterparts using pre-built
* maps.
* - Maps sales lines, quantities, rates, amounts, descriptions,
* taxable status, discounts, and tax details to receipt items.
* - Upserts receipts using Composite_Unique_Key__c, typically
* QuickbooksId::SalesReceipt::RealmId.
* - Upserts each line item using its own composite key and keeps
* the parent receipt relationship intact.
* - Deletes or archives line items no longer returned by
* QuickBooks according to deleteExtraLineItems.
*
* The batch uses the configured QuickBooks company, batch size,
* credentials, and API instance. It prevents overlapping full-sync
* runs, records failures with meaningful status/error information,
* and updates cm_finance__Last_Sync_Config__c on the
* cm_finance__Quickbooks_Company__c record in finish(), including
* the last sync timestamp and outcome.
*
* @usage
* Database.executeBatch(new BatchAllQBSFSalesReceiptSync(totalCount), batchSize);
* System.schedule(
* 'QB Sales Receipts Sync',
* cronExpr,
* new BatchAllQBSFSalesReceiptSync()
* );
*/
/**
* @class BatchAllQBSFPaymentSync
* @description Pulls all Payment records from QuickBooks into Salesforce as
* cm_finance__Payment_Transaction__c records.
*
* Each page calls the QB API, maps responses via
* WS_Quickbooks.createPaymentTxnRecords(), sets
* cm_finance__Payment_Status__c = 'Settled' and
* cm_finance__Amount_Captured__c, then upserts on
* Composite_Unique_Key__c.
*
* If the getPaymentReceipt setting is enabled, the initial
* status is set to 'Payment Receipt PDF Pending'.
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Company record.
*
* @usage
* Database.executeBatch(new BatchAllQBSFPaymentSync(totalCount), batchSize);
* System.schedule('QB Payments Sync', cronExpr, new BatchAllQBSFPaymentSync());
*/
/**
* @class BatchAllQBSFProductsSync
* @description Pulls all Item (Product/Service) records from QuickBooks into
* Salesforce as cm_finance__Quickbooks_Product__c records.
*
* Resolves income, expense, and asset QB Account IDs to their
* Salesforce cm_finance__Quickbooks_Account__c counterparts
* before upserting on Composite_Unique_Key__c.
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Company record.
*
* @usage
* Database.executeBatch(new BatchAllQBSFProductsSync(totalCount), batchSize);
* System.schedule('QB Products Sync', cronExpr, new BatchAllQBSFProductsSync());
*/
/**
* @class BatchAllQBSFVendorsSync
* @description Pulls all Vendor records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Vendor__c records.
*
* Follows the same count-first / paginate-in-chunks pattern.
* Records are upserted on Composite_Unique_Key__c
* (QuickbooksId::Vendor::RealmId).
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Company record.
*
* @usage
* Database.executeBatch(new BatchAllQBSFVendorsSync(totalCount), batchSize);
* System.schedule('QB Vendors Sync', cronExpr, new BatchAllQBSFVendorsSync());
*/
/**
* @class BatchQBClassSync
* @description Pulls all Class records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Class__c records.
*
* Resolves parent Class QB IDs to their Salesforce record IDs
* before upsert. Records are keyed on
* Composite_Unique_Key__c (QuickbooksId::Class::RealmId).
*
* @usage
* Database.executeBatch(new BatchQBClassSync(totalCount), batchSize);
* System.schedule('QB Class Sync', cronExpr, new BatchQBClassSync());
*/
/**
* @class BatchQBTaxRateSync
* @description Pulls all TaxRate records from QuickBooks into Salesforce as
* cm_finance__Tax_Rate__c records.
*
* Records are upserted on Composite_Unique_Key__c
* (UID::RealmId).
*
* @usage
* Database.executeBatch(new BatchQBTaxRateSync(totalCount), batchSize);
* System.schedule('QB TaxRate Sync', cronExpr, new BatchQBTaxRateSync());
*/
/**
* @class BatchAllQBSFPaymentMethodSync
* @description Pulls all Payment Method records from QuickBooks into Salesforce as
* cm_finance__Quickbooks_Payment_Method__c records.
*
* Follows the same count-first / paginate-in-chunks pattern.
* Records are upserted on Composite_Unique_Key__c
* (QuickbooksId::PaymentMethod::RealmId).
*
* finish() stamps cm_finance__Last_Sync_Config__c on the
* Company record.
*
* @usage
* Database.executeBatch(new BatchAllQBSFPaymentMethodSync(totalCount), batchSize);
* System.schedule('QB Payment Method Sync', cronExpr, new BatchAllQBSFPaymentMethodSync());
*/
/**
* @class BatchRefreshTokenUpdate
* @description Refreshes expired QuickBooks OAuth access tokens for all
* active, connected cm_finance__Quickbooks_Company__c records.
*
* start() queries cm_finance__Quickbooks_Credentials__c records
* whose Company is Active and in 'Successfully Synced' status.
*
* execute() refreshes the token only if the last refresh was
* more than 24 hours ago, then updates
* cm_finance__Refresh_Token__c and
* cm_finance__Refresh_Token_Last_Modified_Date__c on the
* credential record.
*
* Runs with batch size 1 to stay within callout limits.
*
* @usage
* Database.executeBatch(new BatchRefreshTokenUpdate(), 1);
* System.schedule('QB Token Refresh', cronExpr, new BatchRefreshTokenUpdate());
*/
/**
* @class BatchSFQBCustomerSync
* @description Pushes pending cm_finance__Quickbooks_Customer__c records
* from Salesforce to QuickBooks (create or update).
*
* Queries records where Process_via_Batch__c = true and
* Status__c != 'Failed' (configurable via whereClause CMT).
*
* Each execute() chunk (batch size 1) maps the SF record to a
* QBApiDataWrapper.Customer, calls WS_Quickbooks.createCustomer(),
* then updates the SF record with the returned QB Id, SyncToken,
* and status. On failure, stamps Status__c = 'Failed' with the
* error message and clears Process_via_Batch__c.
*
* @usage
* Database.executeBatch(new BatchSFQBCustomerSync(), 1);
* System.schedule('SF→QB Customer Sync', cronExpr, new BatchSFQBCustomerSync());
*/
/**
* @class BatchSFQBInvoiceSync
* @description Pushes pending cm_finance__Quickbooks_Invoice__c records
* from Salesforce to QuickBooks (create or update), including
* all related line items.
*
* Resolves customer, product, class, and custom field lookups
* before calling WS_Quickbooks.createInvoice().
*
* On success, stamps Composite_Unique_Key__c, sets
* Ignore_Webhook__c = true to suppress the round-trip webhook,
* clears Process_via_Batch__c, and upserts line items.
* Orphaned line items are deleted or archived based on the
* deleteExtraLineItems setting.
*
* If GenerateInvoicePDF is enabled, status is set to
* 'Invoice PDF Pending'.
*
* @usage
* Database.executeBatch(new BatchSFQBInvoiceSync(), 1);
* System.schedule('SF→QB Invoice Sync', cronExpr, new BatchSFQBInvoiceSync());
*/
/**
* @class BatchSFQBSalesReceiptSync
* @description Pushes pending cm_finance__Quickbooks_Sales_Receipt__c records
* from Salesforce to QuickBooks (create or update), including
* all related line items.
*
* Resolves customer, product,payment method,account, class, and custom field lookups
* before calling WS_Quickbooks.createSalesReceipt().
*
* On success, stamps Composite_Unique_Key__c, sets
* Ignore_Webhook__c = true to suppress the round-trip webhook,
* clears Process_via_Batch__c, and upserts line items.
* Orphaned line items are deleted or archived based on the
* deleteExtraLineItems setting.
*
* If GenerateSalesReceiptPDF is enabled, status is set to
* 'Sales Receipt PDF Pending'.
*
* @usage
* Database.executeBatch(new BatchSFQBSalesReceiptSync(), 1);
* System.schedule('SF→QB Sales Receipt Sync', cronExpr, new BatchSFQBSalesReceiptSync());
*/
/**
* @class BatchSFQBProductSync
* @description Pushes pending cm_finance__Quickbooks_Product__c records
* from Salesforce to QuickBooks (create or update).
*
* Resolves income, expense, and asset QB Account IDs from
* cm_finance__Quickbooks_Account__c before calling
* WS_Quickbooks.createProduct(). On success, stamps QB Id,
* SyncToken, and clears Process_via_Batch__c.
*
* @usage
* Database.executeBatch(new BatchSFQBProductSync(), 1);
* System.schedule('SF→QB Product Sync', cronExpr, new BatchSFQBProductSync());
*/
/**
* @class BatchSFQBVendorSync
* @description Pushes pending cm_finance__Quickbooks_Vendor__c records
* from Salesforce to QuickBooks (create or update).
*
* Maps all vendor fields (name, address, financial, 1099 flag,
* account number) to a QBApiDataWrapper.Vendor before calling
* WS_Quickbooks.createVendor(). Sets Ignore_Webhook__c = true
* and clears Process_via_Batch__c on success.
*
* @usage
* Database.executeBatch(new BatchSFQBVendorSync(), 1);
* System.schedule('SF→QB Vendor Sync', cronExpr, new BatchSFQBVendorSync());
*/
/**
* @class BatchQBSFAccountWebhook
* @description Processes inbound QuickBooks webhook notifications for Account
* (Chart of Accounts) changes by fetching the updated records
* from the QB API and upserting them into Salesforce.
*
* Receives a list of QB Account IDs and the Realm ID from
* Webhook_QBChangeHandler, then delegates each chunk to
* QuickbooksIntegrationHelper.handleQBAccount().
*
* Duplicate-job guard prevents concurrent runs of this batch.
*
* @usage
* Database.executeBatch(new BatchQBSFAccountWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFCustomerWebhook
* @description Processes inbound QuickBooks webhook notifications for Customer
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Delegates each chunk to
* QuickbooksIntegrationHelper.handleQBCustomer().
*
* @usage
* Database.executeBatch(new BatchQBSFCustomerWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFInvoiceWebhook
* @description Processes inbound QuickBooks webhook notifications for Invoice
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Skips records where Ignore_Webhook__c = true (round-trip
* suppression) and clears the flag afterward.
* Delegates processing to
* QuickbooksIntegrationHelper.handleQBInvoice().
*
* @usage
* Database.executeBatch(new BatchQBSFInvoiceWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFSalesReceiptWebhook
* @description Processes inbound QuickBooks webhook notifications for Sales Receipt
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Skips records where Ignore_Webhook__c = true (round-trip
* suppression) and clears the flag afterward.
* Delegates processing to
* QuickbooksIntegrationHelper.handleQBSalesReceipt().
*
* @usage
* Database.executeBatch(new BatchQBSFSalesReceiptWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFPaymentWebhook
* @description Processes inbound QuickBooks webhook notifications for Payment
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Delegates each chunk to
* QuickbooksIntegrationHelper.handleQBPayment().
*
* @usage
* Database.executeBatch(new BatchQBSFPaymentWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFPaymentMethodWebhook
* @description Processes inbound QuickBooks webhook notifications for Payment Method
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Delegates each chunk to
* QuickbooksIntegrationHelper.handleQBPaymentMethod().
*
* @usage
* Database.executeBatch(new BatchQBSFPaymentMethodWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFProductWebhook
* @description Processes inbound QuickBooks webhook notifications for
* Item (Product/Service) changes by fetching updated records
* from the QB API and upserting them into Salesforce.
*
* Delegates each chunk to
* QuickbooksIntegrationHelper.handleQBProduct().
*
* @usage
* Database.executeBatch(new BatchQBSFProductWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class BatchQBSFVendorWebhook
* @description Processes inbound QuickBooks webhook notifications for Vendor
* changes by fetching updated records from the QB API and
* upserting them into Salesforce.
*
* Delegates each chunk to
* QuickbooksIntegrationHelper.handleQBVendor().
*
* @usage
* Database.executeBatch(new BatchQBSFVendorWebhook(lstQBIds, realmId), batchSize);
*/
/**
* @class InvocableAutomateQBService
* @description Flow-invocable entry point for triggering QuickBooks sync
* operations from Salesforce automation (Flows, Process Builder).
*
* For single-record, non-Product requests, enqueues a
* QueueableAutomateQBService job immediately.
*
* For Product or multi-record requests, resolves field mappings
* from cm_finance__Custom_Field_Mapping__c and processes the
* operation synchronously within the invocable call.
*
* Supported requestType values:
* "Customer" → create/update QB Customer
* "Invoice" → create/update QB Invoice
* "Product" → create/update QB Item
* "SalesReceipt" → create/update QB Sales Receipt
*
* @usage
* // From Flow: "Call QB Service" action, pass QBWrapper list
* InvocableAutomateQBService.callQBService(listRecords);
*/
/**
* @class InvocableCreateQBPayment
* @description Flow-invocable class that creates a Payment record in
* QuickBooks for a given cm_finance__Payment_Transaction__c.
*
* Also implements Queueable to perform the callout
* asynchronously. The @InvocableMethod enqueues one job per
* record Id passed in.
*
* On success, stamps cm_finance__Quickbooks_Id__c,
* cm_finance__Quickbooks_SyncToken__c, and
* cm_finance__Composite_Unique_Key__c on the transaction, and
* sets Ignore_Webhook__c = true.
*
* If the QB response includes a deposit account and the
* transaction has none linked, a new
* cm_finance__Quickbooks_Account__c stub is created via
* UpsertCustomRecords.upsertQBAccountRecords().
*
* @usage
* // From Flow: "Create QB Payment" action, pass Payment Transaction Id list
* InvocableCreateQBPayment.callCreateQBPaymentService(listIds);
*/
/**
* @class InvocableGetQBInvoicePDF
* @description Flow-invocable and Queueable class responsible for retrieving
* Invoice or Sales Receipt PDF documents from QuickBooks and
* updating the corresponding Salesforce transaction record.
*
* The class is invoked from Flow through the
* callQBInvoicePDFService() method. It accepts the Salesforce
* record ID, QuickBooks transaction ID, document number,
* QuickBooks Company record ID, and an optional transaction type.
*
* The transaction type determines whether the class retrieves an
* Invoice PDF or a Sales Receipt PDF. If the transaction type is
* not provided, the transaction is treated as an Invoice.
*
* The Queueable execution performs the following operations:
* 1. Retrieves the active QuickBooks Company configuration.
* 2. Loads the corresponding QuickBooks credentials.
* 3. Refreshes the QuickBooks OAuth access token.
* 4. Calls the appropriate QuickBooks PDF service:
* - Invoice -> WS_Quickbooks.getInvoicePDF()
* - SalesReceipt -> WS_Quickbooks.getSalesReceiptPDF()
* 5. Processes the PDF response and retrieves the generated
* Content Document ID.
* 6. Updates the corresponding Salesforce Invoice or Sales
* Receipt record with the Content Document ID and marks the
* PDF as generated.
* 7. Updates the transaction status to
* 'Awaiting Salesforce Sync' when the PDF is successfully
* retrieved.
*
* If the QuickBooks Company, authorization, API response, PDF
* content, or Content Document ID is unavailable, the class
* updates the corresponding transaction record with a Failed
* status and an appropriate error description.
*
* The class implements Database.AllowsCallouts because PDF
* retrieval is performed through an external HTTP callout.
*
* @usage
* // From Flow: 'Get QB Invoice PDF' action.
* // Pass the required transaction details using InvoiceDataWrapper.
* InvocableGetQBInvoicePDF.callQBInvoicePDFService(listRecords);
*
* // Invoice: transactionType = 'Invoice' or leave it blank.
* // Sales Receipt: transactionType = 'SalesReceipt'.
*/
/**
* @class QueueableAutomateQBService
* @description Queueable that executes the actual QB API callout for
* InvocableAutomateQBService when a single-record,
* non-Product request is submitted.
*
* For requestType = 'Customer', calls createQBCustomer() and
* then optionally chains a new job for requestType = 'Invoice'
* so that a customer-then-invoice sequence can be executed in
* two linked queue slots.
*
* For requestType = 'Invoice', calls createQBInvoice()
* directly.
* For requestType = 'SalesReceipt', calls createQBSalesReceipt()
* directly.
*
* @usage
* System.enqueueJob(new QueueableAutomateQBService(oWrapper));
*/
/**
* @class QueueableGetQBCustomFields
* @description Queueable that fetches custom field definitions from the
* QuickBooks API and upserts them as
* cm_finance__Quickbooks_Custom_Field__c records, keyed on
* Composite_Unique_Key__c (DefinitionId::RealmId).
*
* After the upsert, stamps the 'CustomFields' entry in
* cm_finance__Last_Sync_Config__c on the Company record.
*
* @usage
* QueueableGetQBCustomFields job = new QueueableGetQBCustomFields();
* job.companyId = companyRealmId;
* System.enqueueJob(job);
*/
/**
* @class QueueableGetQBPaymentReceiptPDF
* @description Queueable that retrieves a Payment Receipt PDF from
* QuickBooks and stores it as a ContentDocument linked to the
* cm_finance__Payment_Transaction__c record.
*
* First refreshes the OAuth access token, then calls
* WS_Quickbooks.getPaymentReceiptPDF() and stamps the
* content document Id and sync status on success.
*
* @usage
* System.enqueueJob(
* new QueueableGetQBPaymentReceiptPDF(
* paymentRecordId, transactionNumber, paymentQBId, companyRecordId
* )
* );
*/
/**
* @class QueueableQBPaymentDataFetcher
* @description Queueable that performs a single QB Batch API call to
* refresh the Account, Customer, and Invoice records
* associated with a given Payment Transaction, all in one
* round-trip.
*
* Builds a BatchItemRequest body combining up to three queries
* (Account, Customer, Invoice), dispatches it via
* WS_Quickbooks.batchQBAPI(), then parses the BatchItemResponse
* and upserts each object type via UpsertCustomRecords helpers.
*
* @usage
* System.enqueueJob(new QueueableQBPaymentDataFetcher(paymentRecordId));
*/
/**
* @class Webhook_QBChangeHandler
* @description REST resource (URL: /services/apexrest/getUpdatesQB/) that
* receives QuickBooks webhook event notifications via HTTP POST.
*
* Parses the eventNotifications payload to extract entity names
* and IDs for create/update/void operations, then dispatches
* the appropriate webhook batch class:
* Customer → BatchQBSFCustomerWebhook
* Account → BatchQBSFAccountWebhook
* Invoice → BatchQBSFInvoiceWebhook
* Payment → BatchQBSFPaymentWebhook
* Item → BatchQBSFProductWebhook
* Vendor → BatchQBSFVendorWebhook
* SalesReceipt → BatchQBSFSalesReceiptWebhook
*
* Batch size is read from Quickbooks_Common_Settings__c.BatchSize.
*
* @usage
* POST /services/apexrest/getUpdatesQB/
* Body: QuickBooks webhook JSON payload
*/
/**
* @class iQuickbooksService
* @description Interface that defines the contract for the QuickBooks API
* service layer. Implemented by WS_Quickbooks.
*
* Covers create/query methods for:
* Account, Customer, Product, Invoice, CompanyInfo
*
* Each create-or-query pair returns a QBResponseWrapper; each
* createRecords method maps the raw API response into typed
* Salesforce sObject lists.
*
* @usage
* iQuickbooksService svc = new WS_Quickbooks();
*/
/**
* @class QuickbooksIntegrationHelper
* @description Helper class that processes QB API responses for inbound
* webhook batches (BatchQBSF*Webhook classes).
*
* For each entity type, the corresponding handle* method:
* 1. Checks whether the incoming QB IDs already exist in
* Salesforce with Ignore_Webhook__c = true (round-trip
* suppression) and clears that flag.
* 2. For any remaining IDs, queries the QB API and upserts
* the returned records via WS_Quickbooks helpers.
*
* Supported methods:
* handleQBAccount(), handleQBCustomer(), handleQBInvoice(),
* handleQBProduct(), handleQBVendor(), handleQBPayment(),
* handleQBSalesReceipt()
*
* @usage
* QuickbooksIntegrationHelper helper = new QuickbooksIntegrationHelper();
* helper.lstRecordQBIds = lstQBIds;
* helper.realmId = realmId;
* helper.handleQBInvoice();
*/
/**
* @class AdminServices
* @description Global service class that provides administrative operations
* for the QB Connector backend, called primarily from LWC
* components in the Admin Console.
*
* Key operations:
* updateQBCredentials() → writes Client ID, Client Secret,
* and State URL to a named
* cm_finance__Quickbooks_Credentials__c
* record for Sandbox or Production.
* updateEndpointsFromDefault() → copies all API endpoint fields
* from the 'Default' credential
* record to a company-specific one.
* insertPaymentCredentials() → upserts a
* cm_finance__Gateway_Credential__c
* record for a payment gateway.
*
* @usage
* AdminServices.updateQBCredentials(oWrapper);
* AdminServices.updateEndpointsFromDefault(oWrapper);
*/
/**
* @class AdminSetupController
* @description AuraEnabled controller that backs the Admin Console LWC
* components. Exposes company management, field mapping,
* sync configuration, and permission-set assignment actions.
*
* Key @AuraEnabled methods:
* getConnectedCompanies() → returns all active
* cm_finance__Quickbooks_Company__c
* records, default first.
* updateConnectedCompanies() → persists company record
* changes from the LWC.
* getQBIdApiName() → returns the SF field path
* for cm_finance__Quickbooks_Id__c
* per sObject from field mapping.
* Additional methods cover sync trigger, field mapping CRUD,
* permission set assignment, and company default management.
*
* @usage
* // LWC: @wire or imperative call
* AdminSetupController.getConnectedCompanies();
*/
/**
* @class BulkQBInvoicingService
* @description AuraEnabled service that supports the Bulk Invoice Upload LWC.
*
* Key @AuraEnabled methods:
* generateTemplate() → builds a CSV template from the
* BulkInvoiceFieldConfig CMT record,
* returns it as a base64-encoded string
* for browser download.
* validateHeaders() → compares uploaded CSV header row
* against expected columns and returns
* a list of validation errors.
* processInvoices() → parses validated CSV rows, creates
* cm_finance__Quickbooks_Invoice__c and
* related cm_finance__Quickbooks_Invoice_Item__c
* records, and queues them for QB sync.
*
* @usage
* // LWC: @wire or imperative call
* BulkQBInvoicingService.generateTemplate();
* BulkQBInvoicingService.validateHeaders(csvContent);
*/
/**
* @class CMQBConnectorUtility
* @description Central utility class for the QB Connector. Provides shared
* helpers consumed by all batch, trigger, queueable, and
* controller classes.
*
* Key responsibilities:
* FLS control → enforceFLSSetting() reads GuestUsers CMT,
* returns false for guest/automated contexts.
* Email validation → validateEmail() regex check.
* Date parsing → parseDate() from YYYY-MM-DD string.
* HTTP factory → createHTTPRequest() builds HttpRequest with
* auth headers and content types.
* Record builders → createCustomerRecord(), createInvoiceRecord(),
* createProductRecord(), createVendorRecord(),
* createQBAccountRecord(), createQBPaymentRecord()
* stamp status/description/parent-Id fields.
* Parent lookup → populateParentLookup() resolves
* cm_finance__Parent_Record_Id__c to the
* correct relationship field via Schema describe.
* Reverse mapping → getReverseFieldMapping() fetches
* cm_finance__Custom_Field_Mapping__c and
* returns a keyed map for parent field writes.
* Field sync → qbUpdateParentValues() applies QB field values
* to Salesforce parent records using JSON field maps.
* Settings helpers → getDefaultCompanyId(), getDefaultCompanyRealmId(),
* getWhereClause(), getApiInstance(),
* generateInvoicePDF(), generatePaymentLink(),
* getPaymentReceipt(), deleteExtraLineItemsSetting().
*
* @usage
* Boolean flsOn = CMQBConnectorUtility.enforceFLS;
* cm_finance__Quickbooks_Customer__c stub =
* CMQBConnectorUtility.createCustomerRecord(recordId, '', 'Failed', msg, null);
*/
/**
* @class CustomFieldMapperHandler
* @description AuraEnabled controller for the Custom Field Mapper LWC that
* lets admins map QB Invoice custom fields to Salesforce fields.
*
* Key @AuraEnabled methods:
* getCustomFields() → returns the current QB custom field
* mapping JSON, all available QB custom
* field definitions from
* cm_finance__Quickbooks_Custom_Field__c,
* and the describe fields of the mapped
* sObject (for Invoice mappings only).
* saveMapping() → persists the updated JSON mapping to
* cm_finance__QB_Custom_Fields_Mapping__c
* and optionally updates the reverse mapping.
* getFields() → (public) returns label/value pairs for all
* fields on a given sObject via Schema describe.
*
* @usage
* // LWC: imperative call
* CustomFieldMapperHandler.getCustomFields(recordId);
* CustomFieldMapperHandler.saveMapping(mappingJson, recordId, true);
*/
/**
* @class PostInstallScript
* @description InstallHandler executed once on fresh package installation
* (not upgrades or push upgrades).
*
* Creates the default cm_finance__Quickbooks_Credentials__c
* record ('Default') with all API endpoint templates and
* placeholder client credentials, plus a full set of
* cm_finance__Quickbooks_Common_Settings__c hierarchy custom
* settings (BatchSize, GenerateInvoicePDF, ApiInstance, etc.).
*
* Also provides helper factory methods used in tests:
* createPaymentGateway() → builds a test gateway record.
* createGatewayCredentials() → builds test credential records
* for UnityFi, Fiserv, or Actum.
*
* @usage
* // Invoked automatically by Salesforce on package install.
* // Manual: new PostInstallScript().onInstall(ctx);
*/
/**
* @class QBApiDataWrapper
* @description Global wrapper class that defines all request and response
* data transfer objects (DTOs) for the QuickBooks API layer.
*
* Inner classes:
* QBRequestWrapper → holds typed QB entity objects
* (Customer, Vendor, Product, Invoice,Sales Receipt
* Payment, Payment Method, Account, JournalLedger),
* plus raw query string and batch body.
* QBResponseWrapper → holds API response, typed entity lists,
* company/realm IDs, and CompanyInformation.
* APIResponse → isSuccess, statusCode, body, errorMessage.
* Customer, Vendor, → field-level wrappers with populateField()
* Product, Invoice, → for dynamic property assignment.
* Account, Payment,
* JournalLedger,Payment
* Method, SalesReceipt
* Address, Line, → nested structural wrappers for addresses,
* SalesItemLineDetail, → invoice line items, tax detail, etc.
* AccountReference,
* CustomField
* TaxRate, QBClass, → lightweight read wrappers for
* CompanyInformation, → tax rates, class records, and company info.
* CustomField
*
* @usage
* QBApiDataWrapper.QBRequestWrapper req = new QBApiDataWrapper.QBRequestWrapper();
* req.oCustomer = new QBApiDataWrapper.Customer();
*/
/**
* @class QBAuthController
* @description Visualforce page controller that handles the OAuth 2.0
* authorization code callback from QuickBooks.
*
* On page load (QuickbooksAuthAction()):
* - If redirected back with an auth code: sanitizes and
* validates it, exchanges it for access + refresh tokens
* via WS_Quickbooks.getAccessToken(), creates or upserts a
* cm_finance__Quickbooks_Credentials__c record for the
* Realm ID, fetches company info, and fires a QB_Auth__e
* platform event with isSuccess = true.
* - If redirected with an error: fires QB_Auth__e with
* isSuccess = false and the sanitized error message.
* - If not yet redirected: builds the QB OAuth authorization
* URL and sets authURL for the page to redirect to.
*
* @usage
* // VF page action: action="{!QuickbooksAuthAction}"
*/
/**
* @class QBAccount_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Account__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* updateQBCompanyLookup() → if cm_finance__Quickbooks_Company__c
* is null and a default company exists, stamps the default
* company Id and Realm Id on the record.
*
* @usage
* // Invoked from the Quickbooks_Account trigger.
*/
/**
* @class QBCompany_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Company__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* handleMultipleEmailCheck() → if Email__c contains commas,
* splits it into Email__c (first) and Emails__c (full list).
* handleDefaultCheck() → blocks direct Default__c = true
* changes (must go through Admin Console). Prevents multiple
* defaults in one DML.
*
* AFTER_INSERT / AFTER_UPDATE:
* handleDefaultUpdate() → when Default__c becomes true,
* writes the record Id and Realm Id to DefaultCompanyRecordId
* and DefaultCompanyId hierarchy custom settings.
*
* AFTER_DELETE:
* handleAfterDelete() → if the deleted company was the
* default, resets both settings to 'test'. Also deletes the
* linked cm_finance__Quickbooks_Credentials__c record.
*
* @usage
* // Invoked from the Quickbooks_Company trigger.
*/
/**
* @class QBCustomer_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Customer__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* handleMultipleEmailCheck() → normalizes comma-separated emails.
* updateParentLookup() → resolves Parent_Record_Id__c to
* the correct SF lookup field via
* CMQBConnectorUtility.populateParentLookup().
* Also stamps the default company
* if cm_finance__Quickbooks_Company__c
* is null.
*
* AFTER_INSERT / AFTER_UPDATE:
* updateParentFields() → when Status__c transitions to an
* accepted status and a Parent_Record_Id__c
* exists, reads the reverse field mapping
* and writes QB Customer field values back
* to the parent Salesforce record.
*
* @usage
* // Invoked from the Quickbooks_Customer trigger.
*/
/**
* @class QBInvoice_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Invoice__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* handleMultipleEmailCheck() → normalizes Billing_Email__c.
* updateParentLookup() → resolves Parent_Record_Id__c;
* stamps default company.
*
* AFTER_INSERT / AFTER_UPDATE:
* callPDFService() → if GenerateInvoicePDF is on
* and Status__c = 'Invoice PDF
* Pending', enqueues
* InvocableGetQBInvoicePDF.
* callGeneratePaymentLinkService() → if GeneratePaymentLink is on
* and a new Content Document
* has been attached with a
* non-zero balance, calls
* InvocableGeneratePaymentLink.
* updateParentFields() → writes QB Invoice field values
* back to the Salesforce parent,
* including QB custom field values
* resolved via
* cm_finance__QB_Custom_Fields_Mapping__c.
*
* BEFORE_DELETE:
* deleteInvoiceLineItem() → removes all related
* cm_finance__Quickbooks_Invoice_Item__c
* records.
*
* @usage
* // Invoked from the Quickbooks_Invoice trigger.
*/
/**
* @class QBSalesReceipt_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Sales_Receipt__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* handleMultipleEmailCheck() → normalizes Billing_Email__c.
* updateParentLookup() → resolves Parent_Record_Id__c;
* stamps default company.
*
* AFTER_INSERT / AFTER_UPDATE:
* callPDFService() → if GenerateSalesReceiptPDF is on
* and Status__c = 'Sales Receipt PDF
* Pending', enqueues
* InvocableGetQBSalesReceiptPDF.
* updateParentFields() → writes QB Sales Receipt field values
* back to the Salesforce parent,
* including QB custom field values
* resolved via
* cm_finance__QB_Custom_Fields_Mapping__c.
*
* BEFORE_DELETE:
* deleteSalesReceiptLineItem() → removes all related
* cm_finance__Quickbooks_Sales_Receipt_Item__c
* records.
*
* @usage
* // Invoked from the Quickbooks_Invoice trigger.
*/
/**
* @class QBProduct_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Product__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* updateParentLookup() → resolves Parent_Record_Id__c and
* stamps default company if absent.
*
* AFTER_INSERT / AFTER_UPDATE:
* updateParentFields() → when Status__c is in AcceptedStatusFields
* and Parent_Record_Id__c exists, reads
* the reverse field mapping and writes QB
* Product values back to the Salesforce parent.
*
* @usage
* // Invoked from the Quickbooks_Product trigger.
*/
/**
* @class QBVendor_TriggerHandler
* @description Trigger handler for cm_finance__Quickbooks_Vendor__c.
*
* BEFORE_INSERT / BEFORE_UPDATE:
* handleMultipleEmailCheck() → normalizes comma-separated emails.
* updateParentLookup() → resolves Parent_Record_Id__c and
* stamps default company if absent.
*
* AFTER_INSERT / AFTER_UPDATE:
* updateParentFields() → writes QB Vendor field values back
* to the Salesforce parent record via
* reverse field mapping.
*
* @usage
* // Invoked from the Quickbooks_Vendor trigger.
*/
/**
* @class QBInvoiceController
* @description AuraEnabled controller for the Create/View QB Invoice LWC
* component.
*
* Key @AuraEnabled methods:
* checkExistingInvoice() → checks whether a QB invoice
* already exists for a given
* Salesforce parent record and
* company.
* getTermsOptions() → returns the SalesTermMapping
* JSON from CMT (Net30/60/90
* day values).
* fetchRelatedCustomerFromParent() → resolves the QB Customer
* linked to a parent record
* using the configured
* Source_Invoice_Customer_Lookup_field.
* prefillInvoice() → reads field mappings and
* returns pre-filled invoice
* field values from the parent
* record for the LWC form.
* createInvoice() → creates or updates a
* cm_finance__Quickbooks_Invoice__c
* and its line items, then
* queues it for QB sync via
* InvocableAutomateQBService.
* syncInvoicePDF() → manually enqueues PDF
* generation for an existing
* invoice.
*
* @usage
* // LWC: @wire or imperative call
* QBInvoiceController.checkExistingInvoice(recordId, companyRecordId);
*/
/**
* @class QuickbooksController
* @description AuraEnabled controller that serves the QB data LWC components
* (customer sync, product sync, vendor sync, and company selector).
*
* Key @AuraEnabled methods:
* getListCompanies() → returns active QB companies
* ordered by Default__c DESC.
* syncCustomerFromQuickbooks() → fetches QB Customer records
* by QB Id and upserts them
* into Salesforce.
* syncProductFromQuickbooks() → same pattern for Products.
* syncVendorFromQuickbooks() → same pattern for Vendors.
* getQBCustomers() → returns cm_finance__Quickbooks_Customer__c
* records for a given parent
* record and company.
* Additional methods support QB data search and linking
* workflows from LWC.
*
* @usage
* // LWC: @wire or imperative call
* QuickbooksController.getListCompanies();
*/
**
* @class QBSalesReceiptController
* @description AuraEnabled controller for the Create/View QB Sales Receipt LWC
* component.
*
* Key @AuraEnabled methods:
* checkExistingSalesReceipt() → checks whether a QB Sales
* Receipt already exists for
* a given Salesforce parent
* record and company.
* fetchRelatedCustomerFromParent() → resolves the QB Customer
* linked to a parent record
* using the configured Sales
* Receipt customer lookup
* field mapping.
* checkExistingQBCustomer() → checks the configured
* QuickBooks Customer lookup
* field on the source record
* and returns the related QB
* Customer wrapper.
* createCustomerWrapper() → retrieves a QuickBooks
* Customer record and converts
* it into a Customer wrapper
* for use by the LWC.
* getQuickbooksClasses() → returns active QuickBooks
* Classes for the selected
* QuickBooks company when class
* functionality is enabled.
* sendPrefilledSalesReceipt() → reads configured field
* mappings from the source
* record and returns a pre-filled
* Sales Receipt wrapper.
* Also resolves configured
* custom fields, products,
* payment method, and deposit
* account values.
* createEmptySalesReceiptWrapper() → creates an empty Sales
* Receipt wrapper with default
* values and populates
* configured custom fields.
* createSalesReceiptInQuickbooks() → 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.
* populateParentRecordIdOnSalesReceipt()
* → updates the Parent Record
* ID on an existing QuickBooks
* Sales Receipt record.
*
* The controller 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.
*
* @usage
* // LWC: @wire or imperative call
* QBSalesReceiptController.checkExistingSalesReceipt(recordId, companyRecordId);
*/
/**
* @class UpsertCustomRecords
* @description Without-sharing utility class that centralises all upsert
* operations for QB Connector objects, each using the
* Composite_Unique_Key__c external ID field.
*
* Methods (one per QB object type):
* upsertQBCompanyRecords() → cm_finance__Quickbooks_Company__c
* upsertQBCredentials() → cm_finance__Quickbooks_Credentials__c
* (upserts on Name, FLS off)
* upsertQBCustomerRecords() → cm_finance__Quickbooks_Customer__c
* upsertQBVendorRecords() → cm_finance__Quickbooks_Vendor__c
* upsertQBAccountRecords() → cm_finance__Quickbooks_Account__c
* upsertQBProductRecords() → cm_finance__Quickbooks_Product__c
* upsertQBInvoiceRecords() → cm_finance__Quickbooks_Invoice__c
* upsertQBSalesReceiptRecords() → cm_finance__Quickbooks_Sales_Receipt__c
* upsertQBInvoiceItemRecords() → cm_finance__Quickbooks_Sales_Receipt_Item__c
* upsertQBSalesReceiptItemRecords() → cm_finance__Quickbooks_Invoice_Item__c
* upsertQBPaymentRecords() → cm_finance__Payment_Transaction__c
* updateCredentials() → updates Refresh_Token__c and
* Refresh_Token_Last_Modified_Date__c,
* FLS off.
*
* Declared without sharing so credential upserts succeed
* regardless of the running user's record access.
*
* @usage
* UpsertCustomRecords.upsertQBAccountRecords(lstAccounts);
* UpsertCustomRecords.updateCredentials(oCredentials, newRefreshToken);
*/
/**
* @class UtilityService
* @description Shared utility class for the Payment Guru module.
*
* Key responsibilities:
* enforceFLSSetting() → reads GuestUsers from
* cm_finance__Payment_Guru_Common_Setting__mdt
* and returns false for guest contexts.
* gatewaySetting() → returns cm_finance__Gateway_Credential__c
* for a given gateway name.
* getCommonSettings() → fetches a single
* cm_finance__Payment_Guru_Common_Setting__mdt
* record by DeveloperName.
* getDefaultRelationshipName() → resolves the relationship field
* name between two sObjects via Schema
* describe (shared with CMQBConnectorUtility).
* decryptionMethod() → AES-256 decryption using the
* encryption key from Gateway Credentials.
* mapPaymentCommonSettings → lazy-loaded static map of all
* Payment Guru CMT settings.
*
* @usage
* Boolean enforceFLS = UtilityService.enforceFLSSetting();
* cm_finance__Gateway_Credential__c creds = UtilityService.gatewaySetting('Actum');
*/
/**
* @class WS_Quickbooks
* @description Core QuickBooks API service class. Implements iQuickbooksService.
* All callout logic lives here — every batch, queueable, and
* controller class calls this class to communicate with the QB API.
*
* Authentication:
* getAccessToken() → exchanges OAuth auth code for tokens
* (initial connect flow).
* refreshAccessToken() → refreshes an existing access token
* using the stored refresh token.
*
* Batch API:
* batchQBAPI() → sends a single Batch API request
* with multiple sub-queries.
*
* CRUD + Query per entity (Account, Customer, Vendor, Product,Payment Method
* Invoice,Sales Receipt, Payment, TaxRate, Class, CustomField, CompanyInfo):
* create<Entity>() → POST to QB API; maps request wrapper
* to JSON body.
* query<Entity>() → GET/query to QB API with SOQL-like
* query string.
* query<Entity>Count() → queries totalCount only.
* create<Entity>Records() → maps QB API response JSON into
* typed Salesforce sObject lists.
*
* PDF retrieval:
* getInvoicePDF() → fetches invoice PDF bytes, stores as
* ContentDocument linked to the invoice.
* getSalesReceiptPDF() → fetches sales receipt PDF bytes, stores as
* ContentDocument linked to the invoice.
* getPaymentReceiptPDF() → same pattern for payment receipts.
*
* Company:
* getCompanyInfo() → fetches QB company info and maps to
* cm_finance__Quickbooks_Company__c.
* getCustomFields() → fetches QB invoice custom field
* definitions.
*
* Static fields sCompanyId and oCredentials must be set before
* calling any API method.
*
* @usage
* WS_Quickbooks.sCompanyId = realmId;
* WS_Quickbooks.oCredentials = cm_finance__Quickbooks_Credentials__c.getInstance(realmId);
* WS_Quickbooks svc = new WS_Quickbooks();
* QBApiDataWrapper.QBResponseWrapper resp = svc.queryCustomer(oRequestWrapper);
*/