QuickBooks Connector

Technical Documentation - Journal Entry creation (manual)


1. Overview

The Journal Entry Creation system provides a standalone form for creating QuickBooks Journal Entries directly from Salesforce without requiring a source record context. It is a purpose-built double-entry bookkeeping UI that enforces accounting balance constraints (debits must equal credits) before any callout is made.

The component, qbJournalLedger, is a standalone LWC surfaced through an Aura wrapper (QBJournalEntryAura) and navigates the user to the created cm_finance__Quickbooks_Journal_Entry__c record on success. The Apex backend (QuickbooksController) deserializes the submitted payload, calls WS_Quickbooks.createJournalLedger(), and persists the resulting Journal Entry and its line items in Salesforce via upsert on cm_finance__Composite_Unique_Key__c.


2. Component Architecture

┌────────────────────────────────────────────────────────────────────┐
│                      Salesforce Page / Tab                         │
│                                                                    │
│  ┌────────────────────────────────────────────────────────────┐   │
│  │              QBJournalEntryAura (Aura wrapper)             │   │
│  │                                                            │   │
│  │  ┌──────────────────────────────────────────────────────┐  │   │
│  │  │            qbJournalLedger  (LWC)                    │  │   │
│  │  │                                                      │  │   │
│  │  │  Header: Company | Journal Date | Memo               │  │   │
│  │  │  Line Items Table (dynamic rows):                    │  │   │
│  │  │    Account (record-picker) → getAccountType()        │  │   │
│  │  │    Type (Debit/Credit)                               │  │   │
│  │  │    Amount | Description                              │  │   │
│  │  │    Customer/Vendor (conditional record-picker)       │  │   │
│  │  │      → getEntityInfo()                               │  │   │
│  │  │  Totals: Debit | Credit | Difference (color-coded)   │  │   │
│  │  │  Actions: Back | Save Journal Entry (guarded)        │  │   │
│  │  └──────────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────────┘   │
│                       @AuraEnabled Apex Calls                      │
│                                                                    │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │                   QuickbooksController                       │  │
│  │  getListCompanies()          getAccountType()                │  │
│  │  getEntityInfo()             createJournalLedgerInQuickbooks()│  │
│  └───────────────────────────┬──────────────────────────────────┘  │
│                              │                                      │
│  ┌───────────────────────────▼──────────────────────────────────┐  │
│  │                  Shared Service Layer                        │  │
│  │  WS_Quickbooks  │  QBApiDataWrapper  │  Data                │  │
│  └───────────────────────────┬──────────────────────────────────┘  │
│                              │                                      │
│                   QuickBooks Online API                             │
│              POST /v3/company/{realmId}/journalentry                │
└────────────────────────────────────────────────────────────────────┘


3. LWC Component — qbJournalLedger

3.1 Component Identity

Property

Value

Component Name

qbJournalLedger

JS Class Name

QbJournalEntry (extends NavigationMixin(LightningElement))

Aura Wrapper

QBJournalEntryAura.cmp

Location

force-app/main/default/lwc/qbJournalLedger/

Apex Imports

getListCompanies, getAccountType, getEntityInfo, createJournalLedgerInQuickbooks from QuickbooksController

3.2 Entity Type Constants

The component uses four constants to represent the AR/AP classification state of each line's selected account. These drive conditional rendering of the Customer/Vendor picker column.

Constant

Value

Meaning

ACCOUNT_TYPE_AR

'Accounts Receivable'

Account type that requires a Customer entity

ACCOUNT_TYPE_AP

'Accounts Payable'

Account type that requires a Vendor entity

ENTITY_NONE

'None'

Account selected — no entity required

ENTITY_CUSTOMER

'Customer'

Account is AR — Customer picker shown

ENTITY_VENDOR

'Vendor'

Account is AP — Vendor picker shown

ENTITY_UNKNOWN

'Unknown'

No account selected yet — placeholder shown

3.3 Tracked State

Property

Type

Description

journalDate

String

Transaction date, defaults to today (ISO substring(0,10))

privateNote

String

Internal memo (maps to QB PrivateNote)

companyOptions

Array

[{label, value}] for company combobox

selectedCompanyId

String

QB Realm ID of the active company

selectedCompanyRecordId

String

SF record ID of the active company

lineItems

Array

Live array of line item objects (see shape below)

totalDebit

Number

Running debit total

totalCredit

Number

Running credit total

isLoading

Boolean

Controls spinner and Save button disabled state

validationError

String

Inline error banner message

companyFound

Boolean

Controls full form vs. no-company empty state

filter

Object

lightning-record-picker filter criteria (scoped to company)

3.4 Line Item Object Shape

Each line in lineItems carries both UI-helper fields and the QB API payload fields in the same object:

{
  // ── Salesforce UI helpers ──────────────────────────
  id               : <auto-increment>,  // unique key for for:each
  accountSfId      : null,              // SF Id of selected QB Account
  entitySfId       : null,              // SF Id of selected Customer/Vendor
  entityType       : ENTITY_UNKNOWN,    // drives entity column rendering
  requiresCustomer : false,             // true for AR accounts
  requiresVendor   : false,             // true for AP accounts
  noEntityRequired : false,             // true for all other account types
  accountNotSelected: true,             // true before any account is selected

  // ── QB API payload fields ──────────────────────────
  postingType  : '',                    // 'Debit' | 'Credit'
  amount       : null,                  // Decimal (≥ 0.01)
  description  : '',                    // optional line description
  accountQbId  : '',                    // QB AccountRef.value (external ID)
  accountName  : '',                    // QB AccountRef.name
  entityQbId   : '',                    // QB Entity.EntityRef.value
  entityName   : ''                     // QB Entity.EntityRef.name
}

3.5 Initialization Flow

connectedCallback()
  ├─ _initLineItems()   → create 2 blank lines (standard QB minimum)
  └─ loadCompanies()
       │
       └─ getConnectedCompanies()
            │
            ├─ result empty → companyFound = false (empty state shown)
            └─ result non-empty:
                 ├─ Build companyOptions []
                 ├─ Auto-select first company (selectedCompanyId, selectedCompanyRecordId)
                 ├─ _updateFilters()  → set record-picker filter by company
                 └─ companyFound = true (form shown)

3.6 Account Selection Flow (per line)

Account selection is the most complex interaction because selecting an account determines whether the line needs a Customer, Vendor, or neither:

User selects account in lightning-record-picker
  │
  ├─ accountId = null (account cleared):
  │     Reset line: accountSfId/QbId/Name = null/'', entityType = ENTITY_UNKNOWN
  │     Clear entity fields, set accountNotSelected = true
  │
  └─ accountId populated:
       │
       └─ getAccountType({ accountId })   [cacheable Apex call]
            │
            └─ result.accountType →
                 ACCOUNT_TYPE_AR ('Accounts Receivable') → entityType = ENTITY_CUSTOMER
                 ACCOUNT_TYPE_AP ('Accounts Payable')    → entityType = ENTITY_VENDOR
                 anything else                           → entityType = ENTITY_NONE
                 │
                 Update line (immutable map pattern):
                   accountSfId, accountQbId (externalId), accountName
                   entityType, requiresCustomer, requiresVendor, noEntityRequired
                   accountNotSelected = false
                   clear previous entity selection

3.7 Entity (Customer / Vendor) Selection Flow (per line)

User selects Customer or Vendor in entity record-picker
  │
  ├─ entitySfId = null (cleared):
  │     Reset: entitySfId = null, entityQbId = '', entityName = ''
  │
  └─ entitySfId populated:
       │
       └─ getEntityInfo({ recordId: entitySfId, objectType: 'Customer'|'Vendor' })
            │
            └─ Update line:
                 entitySfId, entityQbId (externalId), entityName (displayName)

3.8 Line Item Management

Action

Behaviour

Add Line

Appends a new _blankLine() to lineItems

Remove Line (>2 lines)

Removes the line at data-index; _calculateTotals() called

Remove Line (≤2 lines)

Removes the line then immediately appends a blank — minimum 2 lines always maintained

The minimum-2-lines invariant matches standard QuickBooks Journal Entry behaviour (a valid journal entry requires at least one debit and one credit).

3.9 Computed Getters

Getter

Returns

Logic

isCompanySelected

Boolean

!!selectedCompanyRecordId — controls table visibility

formattedTotalDebit

String

Intl.NumberFormat USD currency format of totalDebit

formattedTotalCredit

String

Intl.NumberFormat USD currency format of totalCredit

formattedDifference

String

Currency format of Math.abs(totalDebit - totalCredit)

differenceStyle

String

Green (#2e844a) if abs(debit - credit) < 0.001, red (#c23934) otherwise

isSaveDisabled

Boolean

true when loading, OR totalDebit ≠ totalCredit, OR totalDebit = 0

3.10 Save Flow and Validation

handleSave()
  │
  ├── [1] Header validation:
  │     Company selected?       → fail: 'Please select a company.'
  │     journalDate populated?  → fail: 'Journal Date is required.'
  │
  ├── [2] Line validation (per-line):
  │     accountSfId present?    → fail: 'Line N: Account is required.'
  │     postingType present?    → fail: 'Line N: Type (Debit/Credit) is required.'
  │     amount > 0?             → fail: 'Line N: Amount must be greater than zero.'
  │     requiresCustomer + no entitySfId? → fail: '...Customer name is required for AR lines.'
  │     requiresVendor + no entitySfId?   → fail: '...Vendor name is required for AP lines.'
  │
  ├── [3] Balance validation:
  │     abs(totalDebit - totalCredit) >= 0.01 → fail: 'Total Debits must equal Total Credits.'
  │
  ├── [4] Build QB API payload:
  │     {
  │       TxnDate, PrivateNote,
  │       Line: [ { DetailType, Amount, Description,
  │                 JournalEntryLineDetail: {
  │                   PostingType, AccountRef: {value, name, recordId},
  │                   Entity (AR/AP only): { Type, EntityRef: {value, name, recordId} }
  │                 }
  │               }, ... ]
  │     }
  │
  └── [5] createJournalLedgerInQuickbooks({ reqBody, companyId, companyRecordId })
           │
           ├─ res.isSuccess = true:
           │     _showToast('Success')
           │     _initLineItems()  (reset form)
           │     NavigationMixin.Navigate → recordPage (res.recordId)
           │
           └─ res.isSuccess = false:
                 handleError(res.message) → toast

3.11 Template States

Condition

Rendered UI

isLoading = true

lightning-spinner overlay

companyFound = false

Empty state: pulsing CSS rings, icon, 3-step setup instructions, "Go to Admin Setup" + "Retry" buttons

companyFound = true

Full form: header fields + line items table (only when isCompanySelected)

isCompanySelected = false

Line items table hidden (table requires company-scoped filter)

line.requiresCustomer = true

lightning-record-picker for cm_finance__Quickbooks_Customer__c

line.requiresVendor = true

lightning-record-picker for cm_finance__Quickbooks_Vendor__c

line.noEntityRequired = true

Static text: "Not required"

line.accountNotSelected = true

Static text: "Please select QuickBooks Account first."

validationError not blank

Red alert banner inline above action buttons

3.12 Record Picker Configuration

Account picker (cm_finance__Quickbooks_Account__c):

  • Display: cm_finance__Name__c, cm_finance__Account_Type__c, cm_finance__Account_Sub_Type__c

  • Matching: cm_finance__Name__c, cm_finance__Account_Type__c

  • Filter: cm_finance__Quickbooks_Company__c = selectedCompanyRecordId (set by _updateFilters())

Customer picker (cm_finance__Quickbooks_Customer__c):

  • Display: cm_finance__Fully_Qualified_Name__c, cm_finance__First_Name__c

  • Matching: cm_finance__Fully_Qualified_Name__c, cm_finance__First_Name__c

  • Filter: same company filter

Vendor picker (cm_finance__Quickbooks_Vendor__c):

  • Display: cm_finance__Display_Name__c, cm_finance__Email__c

  • Matching: cm_finance__Display_Name__c, cm_finance__Email__c

  • Filter: same company filter

All three record pickers share the same filter object, ensuring users only see records belonging to the selected company.


4. Apex Controller — QuickbooksController (Journal Ledger Section)

4.1 getAccountType(String accountId)Map<String, String>

Retrieves account metadata for a given cm_finance__Quickbooks_Account__c record. Called by the LWC immediately after account selection to determine whether the line needs a Customer or Vendor entity picker. Marked cacheable=true — the browser caches the result for the same accountId within a component lifecycle.

Query: SELECT Id, cm_finance__Account_Type__c, cm_finance__Account_Sub_Type__c, cm_finance__Name__c, cm_finance__Quickbooks_Id__c FROM cm_finance__Quickbooks_Account__c WHERE Id = :accountId

Response Key

Source Field

Description

accountType

cm_finance__Account_Type__c

Used to set entityType on the line

accountSubType

cm_finance__Account_Sub_Type__c

Returned for context (not currently used in entity logic)

accountName

cm_finance__Name__c

Stored as accountName on the line for the QB payload

externalId

cm_finance__Quickbooks_Id__c

Stored as accountQbId on the line — maps to QB AccountRef.value



Access

@AuraEnabled(cacheable=true)

FLS

Enforced via Data.read()

Error

Throws AuraHandledException on exception


4.2 getEntityInfo(String recordId, String objectType)Map<String, String>

Retrieves the QB external ID and display name for a Customer or Vendor record selected in the entity picker. Also marked cacheable=true. The objectType parameter ('Customer' or 'Vendor') determines which sObject to query.

Customer path:

  • Query: SELECT Id, cm_finance__Quickbooks_Id__c, cm_finance__Fully_Qualified_Name__c FROM cm_finance__Quickbooks_Customer__c WHERE Id = :recordId

  • Returns: {externalId: Quickbooks_Id__c, displayName: Fully_Qualified_Name__c}

Vendor path:

  • Query: SELECT Id, cm_finance__Quickbooks_Id__c, cm_finance__Display_Name__c FROM cm_finance__Quickbooks_Vendor__c WHERE Id = :recordId

  • Returns: {externalId: Quickbooks_Id__c, displayName: Display_Name__c}



Access

@AuraEnabled(cacheable=true)

Returns

{externalId, displayName} — stored as entityQbId / entityName on the line

Error

Throws AuraHandledException on exception


4.3 createJournalLedgerInQuickbooks(String reqBody, String companyId, String companyRecordId)Map<String, Object>

The primary journal entry creation method. Deserializes the LWC-built JSON payload, builds a typed QBApiDataWrapper.JournalLedger, makes the QB API callout, and persists the result as Salesforce records.

Phase 1 — Payload Deserialization:

Parses the raw JSON string reqBody into a QBApiDataWrapper.JournalLedger:

  • oJournal.txnDateTxnDate

  • oJournal.privateNotePrivateNote

  • For each entry in Line[]:

    • Creates QBApiDataWrapper.Line with detailType, amount, description

    • Creates QBApiDataWrapper.JournalEntryLineDetail with postingType

    • Creates QBApiDataWrapper.AccountReference for accountRef (value, name, recordId)

    • If Entity key present: creates QBApiDataWrapper.Entity with entityType and entityRef (value, name, recordId)

SF ID Maps built during deserialization (used for backfill after callout):

accountQBId  : Map<QB_Account_Id → SF_Account_RecordId>
customerQBId : Map<QB_Customer_Id → SF_Customer_RecordId>
vendorQBId   : Map<QB_Vendor_Id → SF_Vendor_RecordId>

These maps are populated from the LWC payload's recordId fields so that, if the QB API response strips or omits recordId values, the method can restore them before persisting.

Phase 2 — API Callout:

WS_Quickbooks.sCompanyId   = oWrapper.companyRealmId;
WS_Quickbooks.oCredentials = cm_finance__Quickbooks_Credentials__c.getInstance(oWrapper.companyRealmId);
QBApiDataWrapper.QBResponseWrapper oResponseWrapper = oClass.createJournalLedger(oWrapper);

Phase 3 — SF ID Backfill (on success):

After the callout, the QB API response is parsed into oResponseWrapper.lstJournalWrapperResponse. For every line in the response:

  • If accountRef.recordId is null but accountRef.value exists: restore from accountQBId map

  • If entityRef.recordId is null:

    • Customer line: restore from customerQBId map

    • Vendor line: restore from vendorQBId map

This ensures SF record IDs are available on the line items before they are persisted, even if the QB API response does not echo them back.

Phase 4 — Record Parsing:

Calls WS_Quickbooks.createJournalLedgerRecords(oResponseWrapper), which returns:

  • lstJournalLedgers: List<cm_finance__Quickbooks_Journal_Entry__c>

  • mapJournalLedgerLineItems: Map<QB_Journal_Id → List<cm_finance__Quickbooks_Journal_Entry_Item__c>>

Phase 5 — Journal Entry Persistence:

For each journal entry:

  • Sets cm_finance__Parent_Record_Id__c = oWrapper.parentRecordId (currently null — component is standalone)

  • Sets cm_finance__Quickbooks_Company__c = companyRecordId

  • Sets cm_finance__Quickbooks_Company_Realm_Id__c = companyId

  • Sets cm_finance__Status__c = 'Successfully Synced'

  • Sets cm_finance__Private_Note__c = privateNote

  • Sets cm_finance__Composite_Unique_Key__c = QB_Id::JournalEntry::RealmId

  • Routes to lstAllUpsertLedgers (if QB_Id present) or lstAllInsertLedgers (fallback)

Upserts lstAllUpsertLedgers on cm_finance__Composite_Unique_Key__c. Inserts lstAllInsertLedgers via data.create(). Records the first created record's Id in mapResponse.put('recordId', ...) for LWC navigation.

Phase 6 — Line Item Persistence:

Builds mapQBIdToLedgerId (QB journal ID → SF journal record ID) from both upsert and insert lists. For each line item:

  • Sets cm_finance__Quickbooks_Journal_Entry__c = parent SF journal record ID (from mapQBIdToLedgerId)

  • Sets cm_finance__Quickbooks_Company__c and cm_finance__Quickbooks_Company_Realm_Id__c

  • Sets cm_finance__Composite_Unique_Key__c = QB_Journal_Id::Line_Id::RealmId

Upserts all line items on cm_finance__Composite_Unique_Key__c.



Access

@AuraEnabled

DML: Journal Entries

Upsert on cm_finance__Composite_Unique_Key__c (or insert fallback)

DML: Line Items

Upsert on cm_finance__Composite_Unique_Key__c

Returns

{isSuccess, message, recordId?, apiResponse?}

No echo suppression

Journal entries are not set with Ignore_Webhook__c — this entity type does not have a webhook batch handler


5. Data Model — QBApiDataWrapper (Journal Classes)

QBRequestWrapper
  └─ oJournal : JournalLedger

QBResponseWrapper
  └─ lstJournalWrapperResponse : List<JournalLedger>

JournalLedger
  ├─ txnDate       : String
  ├─ QB_Id         : String
  ├─ QB_SyncToken  : String
  ├─ adjustment    : Boolean
  ├─ privateNote   : String
  └─ lstJournalItems : List<Line>

Line (extends LineV)
  ├─ detailType          : String    ('JournalEntryLineDetail')
  ├─ amount              : Decimal
  ├─ description         : String
  ├─ lineId              : String
  ├─ lineNumber          : Decimal
  ├─ salesItemDetail     : SalesLineItemDetail   (unused for journals)
  └─ journalEntryDetails : JournalEntryLineDetail

JournalEntryLineDetail
  ├─ postingType : String            ('Debit' | 'Credit')
  ├─ accountRef  : AccountReference  {value: QB_Id, name, recordId: SF_Id}
  └─ entityDetails : Entity          (only for AR/AP lines)

Entity
  ├─ entityType : String             ('Customer' | 'Vendor')
  └─ entityRef  : AccountReference   {value: QB_Id, name, recordId: SF_Id}

AccountReference
  ├─ value    : String               (QB external ID)
  ├─ name     : String               (display name)
  └─ recordId : String               (Salesforce record ID)


6. QB API Payload Structure

The LWC constructs this JSON object and passes it as reqBody to createJournalLedgerInQuickbooks:

{
  "TxnDate"     : "2025-05-03",
  "PrivateNote" : "Adjusting entry for Q1",
  "Line": [
    {
      "DetailType"  : "JournalEntryLineDetail",
      "Amount"      : 500.00,
      "Description" : "Revenue recognition",
      "JournalEntryLineDetail": {
        "PostingType": "Credit",
        "AccountRef" : {
          "value"    : "35",
          "name"     : "Sales of Product Income",
          "recordId" : "a0B5g000001XyZEAA0"
        }
      }
    },
    {
      "DetailType"  : "JournalEntryLineDetail",
      "Amount"      : 500.00,
      "Description" : "AR entry",
      "JournalEntryLineDetail": {
        "PostingType": "Debit",
        "AccountRef" : {
          "value"    : "12",
          "name"     : "Accounts Receivable (A/R)",
          "recordId" : "a0B5g000001AbCDAA0"
        },
        "Entity": {
          "Type"      : "Customer",
          "EntityRef" : {
            "value"    : "58",
            "name"     : "Acme Corp",
            "recordId" : "a0C5g000002KlMNAA0"
          }
        }
      }
    }
  ]
}

The recordId fields inside AccountRef and EntityRef are Salesforce-only additions — they are carried through the Apex deserialization for SF ID backfill and are not sent to the QB API.


7. End-to-End Flow Diagram

User opens qbJournalLedger component
           │
           ▼
connectedCallback()
  ├─ _initLineItems() → 2 blank rows
  └─ loadCompanies() → getListCompanies()
           │
    ┌──────┴───────────────┐
  No companies          Companies found
    │                        │
  Show empty state      Auto-select first company
  (Admin Setup CTA)     _updateFilters()
                        Show form
           │
User fills header (Date, Memo) + selects Company
           │
User selects Account on a line
  └─ getAccountType(accountId)   [cached]
       → Set entityType on line
       → Show: Customer picker | Vendor picker | "Not required"
           │
User selects Customer/Vendor (if AR/AP line)
  └─ getEntityInfo(entitySfId, objectType)   [cached]
       → Set entityQbId, entityName on line
           │
User enters Amount, Type, Description
  └─ handleLineFieldChange() → _calculateTotals()
       → Update totalDebit, totalCredit
       → differenceStyle: green if balanced, red if not
           │
User clicks "Save Journal Entry"
  (Button enabled only when debit = credit AND debit > 0)
           │
handleSave()
  ├─ Validate header
  ├─ Validate each line (account, type, amount, entity)
  ├─ Validate debit = credit (tolerance 0.01)
  ├─ Build QB API payload JSON
  └─ createJournalLedgerInQuickbooks(reqBody, companyId, companyRecordId)
           │
    ┌──────┴────────────────────────┐
  res.isSuccess = true           res.isSuccess = false
    │                                  │
  Toast: Success                  handleError(res.message)
  _initLineItems()                → Toast: Error
  Navigate to res.recordId
  (cm_finance__Quickbooks_Journal_Entry__c record page)


8. Salesforce Object Reference

8.1 cm_finance__Quickbooks_Journal_Entry__c (Header)

Field

Description

cm_finance__QuickBooks_Id__c

QB-assigned journal entry ID

cm_finance__Composite_Unique_Key__c

QB_Id::JournalEntry::RealmId — upsert key

cm_finance__Quickbooks_Company__c

Lookup to cm_finance__Quickbooks_Company__c

cm_finance__Quickbooks_Company_Realm_Id__c

QB Realm ID string

cm_finance__Parent_Record_Id__c

Source SF record ID (null for standalone journal entries)

cm_finance__Status__c

'Successfully Synced' on success

cm_finance__Private_Note__c

Internal memo from the form

8.2 cm_finance__Quickbooks_Journal_Entry_Item__c (Line Items)

Field

Description

cm_finance__Quickbooks_Journal_Entry__c

Parent lookup (resolved via mapQBIdToLedgerId)

cm_finance__Line_Id__c

QB-assigned line sequence ID

cm_finance__Composite_Unique_Key__c

QB_Journal_Id::Line_Id::RealmId — upsert key

cm_finance__Quickbooks_Company__c

Lookup to company

cm_finance__Quickbooks_Company_Realm_Id__c

QB Realm ID string

8.3 Composite_Unique_Key__c Format

Entity

Format

Journal Entry

QB_Journal_Id::JournalEntry::RealmId

Journal Entry Line Item

QB_Journal_Id::QB_Line_Id::RealmId


9. Design Decisions

9.1 Immutable Line Item Updates

Every change to a line item is applied using the JavaScript immutable map pattern (lineItems.map((line, i) => i === index ? {...line, [field]: value} : line)). This ensures LWC reactive property tracking picks up the change without requiring @track on individual properties within the array, and prevents accidental mutation of sibling rows.

9.2 Per-Account-Selection Apex Callout

Rather than loading all account types up front, the component makes a fresh getAccountType() callout each time the user selects an account. The method is cacheable=true, meaning the platform caches identical responses within the component lifecycle. This avoids loading the full chart of accounts into memory while still providing instant feedback on whether a Customer or Vendor picker should appear.

9.3 SF ID Round-Trip via recordId in Payload

The LWC embeds Salesforce record IDs (accountSfId, entitySfId) inside the QB API payload object (AccountRef.recordId, EntityRef.recordId). These fields have no meaning to the QB API but allow the Apex method to build the accountQBId, customerQBId, and vendorQBId reverse-lookup maps during deserialization. If the QB response returns lines without recordIds (which can happen depending on the QB API version), the backfill step restores them before persisting line items — eliminating a separate post-callout query.

9.4 Balance Guard on Save Button

isSaveDisabled is a computed getter that disables the Save button the moment debits and credits diverge, providing instant visual feedback without requiring the user to attempt a save. The accounting constraint (debit = credit) is still re-validated server-side in handleSave() as a defence-in-depth measure.

9.5 Minimum Two Lines Invariant

When the user removes a line that would leave fewer than two rows, removeLine() immediately appends a blank line after the removal. This mirrors standard QB Journal Entry behaviour where the form always shows at least two lines and prevents a state where a user could submit a single-line (inherently unbalanced) entry.

9.6 Standalone Component — No Source Record Context

Unlike the invoice and product flows in cmQBConnectorMaster, qbJournalLedger operates without a source Salesforce record. oWrapper.parentRecordId is explicitly left null in the Apex method. This means journal entries created through this component have cm_finance__Parent_Record_Id__c = null — they are purely QB-originated transactions created from Salesforce, not mapped to any SF source object.

9.7 Aura Wrapper

QBJournalEntryAura.cmp wraps the LWC to enable placement in contexts that require an Aura component (e.g., certain utility bars or page layouts in older org configurations). It contains no logic of its own — all behaviour lives in the LWC.