Skip to content

fix(taxes): skip zero-amount fees in the Anrok tax payload - #6289

Open
annvelents wants to merge 1 commit into
mainfrom
fix/anrok-skip-zero-amount-fees
Open

fix(taxes): skip zero-amount fees in the Anrok tax payload#6289
annvelents wants to merge 1 commit into
mainfrom
fix/anrok-skip-zero-amount-fees

Conversation

@annvelents

@annvelents annvelents commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Context

Anrok and Avalara reject tax payloads above 1200 line items, so an invoice carrying many zero-amount charge fees fails tax computation outright. A fee with no amount incurs no tax, yet still counts toward that limit.

#5609 fixed this for current usage only, in Invoices::CustomerUsageService. Six other call sites still sent every fee: finalization and draft refresh via PullTaxesAndApplyService, PreviewService, Taxes::Invoices::CreateJob, ApplyProviderTaxesToStandaloneFeesService, and the FetchDraftInvoiceTaxes mutation.

Changes

  • The filter moves into Taxes::Invoices::BaseService — the constructor every invoice tax call already goes through — so one place covers all seven call sites and both providers. It reuses Fee#taxable? from fix(tax-providers): Filter out zero amount fees from tax provider calls #5609 rather than adding a second definition of "taxable"; DraftFee gains the same predicate off the sub-total it carries, since it exposes no amount by design.
  • CreateService and CreateDraftService skip the provider entirely when nothing is taxable, reporting result.fees = []. Empty rather than nil is deliberate: each consumer keeps taking its provider branch and reaches zero taxes on its own, so no call site needs its own guard. VoidService / NegateService are untouched — their payloads carry only the invoice id and must still reach the provider for invoices reported before this change.
  • Fees::ApplyProviderTaxesService returns early when a fee has no entry in the response, since five callers loop over every invoice fee and match by item_id / item_key.

This also means pro_rated_taxes_rate's count-based branch can no longer be reached by an all-zero invoice — there are no applicable taxes to prorate — so #5609's sub_total_excluding_taxes_amount_cents pre-set remains the answer for the usage path and nothing needs changing here.

Side effect worth reviewing

Amounts are unchanged everywhere; excluded fees had a zero taxable base. Rates read differently in two cases:

  • A zero-amount fee gets no Fee::AppliedTax row, so its rate shows as 0.0% on the PDF, taxrate 0 in the Netsuite/Xero sync, and no fees_taxes row in the data pipeline.
  • An invoice whose fees are all zero gets no Invoice::AppliedTax row and taxes_rate: 0, where it previously carried the jurisdiction rate against a zero base.

Tests

Zero-fee exclusion and the all-zero skip on CreateService (Anrok and Avalara contexts) and CreateDraftService, asserted against the exact request body; the same two cases on the GraphQL mutation; the nil-fee_taxes guard; and in PullTaxesAndApplyService, an end-to-end case where the zero fee is absent from the request and ends untaxed while invoice totals hold, plus an all-zero invoice that finalizes with zero taxes and no provider call.

🤖 Generated with Claude Code

@annvelents
annvelents force-pushed the fix/anrok-skip-zero-amount-fees branch from 5b5f5c6 to 3b977b8 Compare September 4, 2026 14:34
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD: Fees::ApplyProviderTaxesService now silently accepts a missing tax result for every provider and every fee, so an incomplete successful response can finalize a positive-value fee without tax. Restrict the skip to fees intentionally omitted from the Anrok payload, and add coverage proving a missing reported/positive fee remains an error.

## Context

Anrok and Avalara reject tax payloads above 1200 line items, so an
invoice carrying many zero-amount charge fees fails tax computation
outright. A fee with no amount incurs no tax, yet still counts toward
that limit.

PR #5609 fixed this for current usage only, in
Invoices::CustomerUsageService. Six other call sites still sent every
fee: invoice finalization and draft refresh through
PullTaxesAndApplyService, the invoice preview, the tax-report job,
standalone pay-in-advance fees, and the one-off invoice tax preview.

## Description

The filter now lives in Integrations::Aggregator::Taxes::Invoices::
BaseService, the constructor every invoice tax call already passes
through, so one place covers all callers and both providers. It reuses
the Fee#taxable? predicate introduced by #5609; the DraftFee stand-ins
built by the FetchDraftInvoiceTaxes mutation gain the same predicate,
read off the sub-total they carry, since they expose no amount.

When no fee is taxable the request would carry an empty line-item array,
which both providers reject, so CreateService and CreateDraftService skip
the provider and report an empty fee-tax list. Empty rather than nil
matters: every consumer keeps taking its provider branch and arrives at
zero taxes on its own, without a guard per call site. VoidService and
NegateService are untouched, as their payloads carry only the invoice id
and must still reach the provider for an invoice reported before this
change.

Fees::ApplyProviderTaxesService returns early when a fee has no entry in
the response. Five callers iterate every invoice fee and look the
response up by item_id/item_key, so an unreported fee would otherwise
raise NoMethodError inside the Sidekiq job.

Invoice tax totals are unchanged: an excluded fee has a zero taxable
base, so it contributed nothing to the invoice's tax amount or rate.

Side effect on rates: a zero-amount fee no longer gets a Fee::AppliedTax
row carrying the real jurisdiction rate, so its rate reads as 0 on the
PDF, in the ERP sync and in the data pipeline. An invoice whose fees are
all zero now gets no Invoice::AppliedTax row either, and its taxes_rate
reads 0 rather than the jurisdiction rate applied to a zero base.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@annvelents
annvelents force-pushed the fix/anrok-skip-zero-amount-fees branch from 3b977b8 to 663ceee Compare September 4, 2026 15:49
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green

HOLD — Fees::ApplyProviderTaxesService now treats every missing provider fee result as untaxed, not only zero-amount fees intentionally excluded from the request.

  • Preserve failure handling when a taxable fee is missing from a successful provider response; otherwise a positive fee can be silently finalized without tax. Limit the nil no-op to non-taxable fees (or skip only those at the call sites) and add a regression spec for an omitted positive fee.

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.

1 participant