Skip to content

Multiple accounts payable cause bug - #216

Merged
fivetran-catfritz merged 3 commits into
fivetran:release/1.9.1from
bernhardF1984:main
Aug 5, 2026
Merged

Multiple accounts payable cause bug#216
fivetran-catfritz merged 3 commits into
fivetran:release/1.9.1from
bernhardF1984:main

Conversation

@bernhardF1984

@bernhardF1984 bernhardF1984 commented Jul 15, 2026

Copy link
Copy Markdown

Bernhard Feldmann- Twelvegrow

issue with 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:

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.

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: 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 in case of multiple accounts payable
@fivetran-catfritz

Copy link
Copy Markdown
Contributor

Thank you @bernhardF1984 for opening this PR. I created an issue #217 to help our team track progress.

I will review this PR and let you know the next steps.

@bernhardF1984

bernhardF1984 commented Jul 17, 2026 via email

Copy link
Copy Markdown
Author

@fivetran-catfritz
fivetran-catfritz changed the base branch from main to release/1.9.1 July 28, 2026 01:16
@fivetran-catfritz

Copy link
Copy Markdown
Contributor

Hi @bernhardF1984 thanks for opening the support ticket. Were there any updates to be made to this PR?

@fivetran-catfritz
fivetran-catfritz merged commit ed5df58 into fivetran:release/1.9.1 Aug 5, 2026
fivetran-catfritz added a commit that referenced this pull request Aug 6, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants