Skip to content

Bug/ap ar account fanout - #220

Merged
fivetran-avinash merged 14 commits into
mainfrom
bug/ap-ar-account-fanout
Sep 1, 2026
Merged

fivetran-avinash merged 14 commits into
mainfrom
bug/ap-ar-account-fanout

Conversation

@fivetran-catfritz

@fivetran-catfritz fivetran-catfritz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Overview

Package version introduced in this PR:

  • 1.9.2

This PR addresses the following Issue/Feature(s):

  • GA-1033070

Summary of changes:

  • Resolves AP/AR account Fanout

Submission Checklist

  • Alignment meeting with the reviewer (if needed)
    • Timeline and validation requirements discussed
  • Provide validation details:
    • Validation Steps: Check for unintentional effects (e.g., add/run consistency & integrity tests)
    • Testing Instructions: Confirm the change addresses the issue(s)
    • Focus Areas: Complex logic or queries that need extra attention
  • Merge any relevant open PRs into this PR

Changelog

  • Draft changelog for PR
  • Final changelog for release review

bernhardF1984 and others added 8 commits August 5, 2026 17:20
* Bug fix - multiple accounts payable

Bug: payable_account_id is dropped in stg_quickbooks__bill_payment, causing data fanout in int_quickbooks__bill_payment_double_entry when more than one account shares account_type = 'Accounts Payable'
Package
fivetran/dbt_quickbooks (also present in the deprecated fivetran/dbt_quickbooks_source)
Summary
int_quickbooks__bill_payment_double_entry.sql posts the debit side of every Bill Payment to every account whose account_type = 'Accounts Payable', instead of to the one specific AP account each payment actually clears. This silently multiplies real accounts-payable activity onto any other account in the chart of accounts that happens to share that account_type — which QuickBooks allows, even though it isn't the intended one-AP-account-per-currency design the package assumes.

The root cause is that the source bill_payment object does carry a payable_account_id field (confirmed present in the raw Fivetran-synced bill_payment table), but stg_quickbooks__bill_payment.sql never selects it through to the staging layer. Because that field isn't available downstream, int_quickbooks__bill_payment_double_entry.sql has no way to join precisely, and falls back to matching by account_type + currency_id alone.

This is inconsistent with int_quickbooks__bill_double_entry.sql, which handles the equivalent case correctly by using bills.payable_account_id directly for its credit-side entry — that model is not affected by this bug.
Where the bug is
models/staging/stg_quickbooks__bill_payment.sql — final select omits payable_account_id even though it exists on the source table:

final as (
    select 
        cast(id as {{ dbt.type_string() }}) as bill_payment_id,
        cast(check_bank_account_id as {{ dbt.type_string() }}) as check_bank_account_id,
        check_print_status,
        cast(credit_card_account_id as {{ dbt.type_string() }}) as credit_card_account_id,
        exchange_rate,
        currency_id,
        cast(department_id as {{ dbt.type_string() }}) as department_id,
        pay_type,
        total_amount,
        cast( {{ dbt.date_trunc('day', 'transaction_date') }} as date) as transaction_date,
        cast(vendor_id as {{ dbt.type_string() }}) as vendor_id,
        created_at,
        updated_at,
        _fivetran_deleted,
        source_relation
    from fields
)

models/double_entry_transactions/int_quickbooks__bill_payment_double_entry.sql — because payable_account_id isn't available, the join falls back to matching every account of the configured account_type:

ap_accounts as (
    select
        account_id,
        currency_id,
        source_relation
    from accounts
    where account_type = '{{ var('quickbooks__accounts_payable_reference', 'Accounts Payable') }}'
        and is_active
        and not is_sub_account
),

bill_payment_join as (
    select
        ...
        coalesce(bill_payments.credit_card_account_id, bill_payments.check_bank_account_id) as payment_account_id,
        ap_accounts.account_id,
        ...
    from bill_payments
    left join ap_accounts
        on ap_accounts.currency_id = bill_payments.currency_id
        and ap_accounts.source_relation = bill_payments.source_relation
),

Since the join only matches on currency_id + source_relation, every Bill Payment fans out into one debit row per matching AP-type account, not one debit row for the specific account it actually pays down.

The DECISIONLOG does document that a single AP account per currency is a requirement and that multiple AP-typed accounts cause fanout, but since payable_account_id is already present on the source object, this looks like an unintentional oversight rather than an inherent limitation — the fix is available in the data, it's just not being used.
Reproduction
In a company file with 4 accounts sharing account_type = 'Accounts Payable' (one real "Accounts Payable" control account, plus three other liability-type accounts that were misconfigured with that same account_type in QuickBooks), querying quickbooks__general_ledger shows:

account_id
account_name
debit txn count
debit total
131
Accounts Payable (real)
2,769
$19,734,953.91
211
Loan Payable
2,758
$19,726,244.63
286
Loan Payable-Equipment
2,758
$19,726,244.63
178
Payroll Taxes Pay
2,758
$19,726,244.63


Three unrelated accounts show a nearly identical transaction count and dollar total to the real AP account — because they're all receiving a copy of the same underlying Bill Payment activity. This produced multi-million-dollar phantom balances on quickbooks__balance_sheet for accounts that should be at or near $0.
Impact
Any customer with more than one account carrying account_type = 'Accounts Payable' (which QuickBooks Online permits, even if not best practice) will see their non-AP account(s) silently inflated by the full company-wide AP payment volume, with no error or warning — the totals are close enough to the real AP account's totals that it can look plausible rather than obviously broken.
Suggested fix
Add payable_account_id to the final select in stg_quickbooks__bill_payment.sql:

cast(payable_account_id as {{ dbt.type_string() }}) as payable_account_id,

In int_quickbooks__bill_payment_double_entry.sql, join bill_payment_join to ap_accounts on the specific account instead of (or in addition to) account_type:

left join ap_accounts
    on ap_accounts.account_id = bill_payments.payable_account_id
    and ap_accounts.source_relation = bill_payments.source_relation

This mirrors how int_quickbooks__bill_double_entry.sql already handles the Bill side correctly via bills.payable_account_id.
Workaround
Until this is fixed upstream, we're overriding both models locally (defining models of the same name in our own dbt project, which take precedence over the installed package version) with the change above. We're also correcting the account_type of the misclassified accounts directly in QuickBooks, since that's the proper long-term fix regardless of this package behavior — but the package should not silently fan out data even when a customer's chart of accounts is misconfigured this way.

* bug fix - multiple accounts payable

Bug: payable_account_id is dropped in stg_quickbooks__bill_payment, causing data fanout in int_quickbooks__bill_payment_double_entry when more than one account shares account_type = 'Accounts Payable'
Package
fivetran/dbt_quickbooks (also present in the deprecated fivetran/dbt_quickbooks_source)
Summary
int_quickbooks__bill_payment_double_entry.sql posts the debit side of every Bill Payment to every account whose account_type = 'Accounts Payable', instead of to the one specific AP account each payment actually clears. This silently multiplies real accounts-payable activity onto any other account in the chart of accounts that happens to share that account_type — which QuickBooks allows, even though it isn't the intended one-AP-account-per-currency design the package assumes.

The root cause is that the source bill_payment object does carry a payable_account_id field (confirmed present in the raw Fivetran-synced bill_payment table), but stg_quickbooks__bill_payment.sql never selects it through to the staging layer. Because that field isn't available downstream, int_quickbooks__bill_payment_double_entry.sql has no way to join precisely, and falls back to matching by account_type + currency_id alone.

This is inconsistent with int_quickbooks__bill_double_entry.sql, which handles the equivalent case correctly by using bills.payable_account_id directly for its credit-side entry — that model is not affected by this bug.
Where the bug is
models/staging/stg_quickbooks__bill_payment.sql — final select omits payable_account_id even though it exists on the source table:

final as (
    select 
        cast(id as {{ dbt.type_string() }}) as bill_payment_id,
        cast(check_bank_account_id as {{ dbt.type_string() }}) as check_bank_account_id,
        check_print_status,
        cast(credit_card_account_id as {{ dbt.type_string() }}) as credit_card_account_id,
        exchange_rate,
        currency_id,
        cast(department_id as {{ dbt.type_string() }}) as department_id,
        pay_type,
        total_amount,
        cast( {{ dbt.date_trunc('day', 'transaction_date') }} as date) as transaction_date,
        cast(vendor_id as {{ dbt.type_string() }}) as vendor_id,
        created_at,
        updated_at,
        _fivetran_deleted,
        source_relation
    from fields
)

models/double_entry_transactions/int_quickbooks__bill_payment_double_entry.sql — because payable_account_id isn't available, the join falls back to matching every account of the configured account_type:

ap_accounts as (
    select
        account_id,
        currency_id,
        source_relation
    from accounts
    where account_type = '{{ var('quickbooks__accounts_payable_reference', 'Accounts Payable') }}'
        and is_active
        and not is_sub_account
),

bill_payment_join as (
    select
        ...
        coalesce(bill_payments.credit_card_account_id, bill_payments.check_bank_account_id) as payment_account_id,
        ap_accounts.account_id,
        ...
    from bill_payments
    left join ap_accounts
        on ap_accounts.currency_id = bill_payments.currency_id
        and ap_accounts.source_relation = bill_payments.source_relation
),

Since the join only matches on currency_id + source_relation, every Bill Payment fans out into one debit row per matching AP-type account, not one debit row for the specific account it actually pays down.

The DECISIONLOG does document that a single AP account per currency is a requirement and that multiple AP-typed accounts cause fanout, but since payable_account_id is already present on the source object, this looks like an unintentional oversight rather than an inherent limitation — the fix is available in the data, it's just not being used.
Reproduction
In a company file with 4 accounts sharing account_type = 'Accounts Payable' (one real "Accounts Payable" control account, plus three other liability-type accounts that were misconfigured with that same account_type in QuickBooks), querying quickbooks__general_ledger shows:

account_id
account_name
debit txn count
debit total
131
Accounts Payable (real)
2,769
$19,734,953.91
211
Loan Payable
2,758
$19,726,244.63
286
Loan Payable-Equipment
2,758
$19,726,244.63
178
Payroll Taxes Pay
2,758
$19,726,244.63


Three unrelated accounts show a nearly identical transaction count and dollar total to the real AP account — because they're all receiving a copy of the same underlying Bill Payment activity. This produced multi-million-dollar phantom balances on quickbooks__balance_sheet for accounts that should be at or near $0.
Impact
Any customer with more than one account carrying account_type = 'Accounts Payable' (which QuickBooks Online permits, even if not best practice) will see their non-AP account(s) silently inflated by the full company-wide AP payment volume, with no error or warning — the totals are close enough to the real AP account's totals that it can look plausible rather than obviously broken.
Suggested fix
Add payable_account_id to the final select in stg_quickbooks__bill_payment.sql:

cast(payable_account_id as {{ dbt.type_string() }}) as payable_account_id,

In int_quickbooks__bill_payment_double_entry.sql, join bill_payment_join to ap_accounts on the specific account instead of (or in addition to) account_type:

left join ap_accounts
    on ap_accounts.account_id = bill_payments.payable_account_id
    and ap_accounts.source_relation = bill_payments.source_relation

This mirrors how int_quickbooks__bill_double_entry.sql already handles the Bill side correctly via bills.payable_account_id.
Workaround
Until this is fixed upstream, we're overriding both models locally (defining models of the same name in our own dbt project, which take precedence over the installed package version) with the change above. We're also correcting the account_type of the misclassified accounts directly in QuickBooks, since that's the proper long-term fix regardless of this package behavior — but the package should not silently fan out data even when a customer's chart of accounts is misconfigured this way.

* Fix join condition for ap_accounts in SQL query

bug in case of multiple accounts payable
* fix/je-ar-lookup

* fix cast
The join in #216 matched ap_accounts strictly on payable_account_id,
dropping the account_type/currency fallback entirely. Any bill payment
without a populated payable_account_id (most historical synced records)
lost its AP account_id outright instead of resolving to the single AP
account, silently breaking GL entries for the common case.

Falls back to the account_type/currency match only when
payable_account_id is null, mirroring the AR lookup guard added in #218.
Mirrors the je_ar_lookup added to int_quickbooks__payment_double_entry
in #218. A BillPayment's payable_account_id is null when it's cleared
against a Journal Entry rather than a Bill; the linked JE usually still
carries a line posted to the correct AP account, so resolve it there
before falling back to the generic account_type/currency match.
Same fix as payment_line in #218 - the raw column comes through as an
integer on some warehouses (e.g. Postgres), while
stg_quickbooks__journal_entry_line.journal_entry_id is cast to string.
The new je_ap_lookup join in int_quickbooks__bill_payment_double_entry
compares the two directly, so the types must match.
Seed additions recreate both scenarios this branch fixes:
- a second same-currency Accounts Payable account (20299) and a bill
  payment resolved via payable_account_id (9002), another with no
  payable_account_id resolved via a linked journal entry (9004)
- a second same-currency Accounts Receivable account (92003) and a
  payment resolved via a linked journal entry (9002)

New integrity tests (tags=fivetran_validations, gated behind
fivetran_validation_tests_enabled):
- ap_ar_double_entry_no_fanout: whenever a bill payment/payment is
  resolvable (populated payable_account_id/receivable_account_id, or a
  linked JE posting to an AP/AR account), asserts exactly one AP/AR row
  is produced, not one per matching account_type account
- ap_ar_double_entry_no_null_account: asserts the AP/AR leg is never
  dropped (account_id null) for resolvable transactions - the
  regression introduced by #216's strict id-only join with no fallback
Patch release documenting the JE-based AR/AP account lookup fix for
duplicate account fanout in int_quickbooks__payment_double_entry and
int_quickbooks__bill_payment_double_entry.
@fivetran-avinash fivetran-avinash added the docs:ready Triggers the docs generator workflow. label Aug 25, 2026

@fivetran-avinash fivetran-avinash left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for release review!

@fivetran-avinash fivetran-avinash added docs:ready Triggers the docs generator workflow. and removed docs:ready Triggers the docs generator workflow. labels Aug 28, 2026

@fivetran-savage fivetran-savage left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@fivetran-avinash
fivetran-avinash merged commit b83f44a into main Sep 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs:ready Triggers the docs generator workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants