Skip to content

Retry pulling provider taxes for stuck pending invoices - #5987

Open
lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
fix/claude-retry-tax-pending-invoices
Open

Retry pulling provider taxes for stuck pending invoices#5987
lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
fix/claude-retry-tax-pending-invoices

Conversation

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

Adds a daily clock job (Clock::RetryTaxPendingInvoicesJob) that re-enqueues Invoices::ProviderTaxes::PullTaxesAndApplyJob for every invoice stuck in pending status with tax_pending, so invoices whose tax pull never completed recover automatically instead of staying pending.

Root-cause investigation of why the pull leaves invoices pending is out of scope here; this is the nightly-retry fallback.

## Context

Invoices occasionally stay in a pending status with taxes still pending
even though the provider-tax pull job reports success, and the root cause
has not been identified from the logs.

## Description

Add a daily clock job that re-enqueues the provider-tax pull for every
invoice stuck in pending status with a pending tax status, so they get a
recurring chance to complete instead of staying pending indefinitely.

Signed-off-by: lago-claude-ai-agent[bot] <297187938+lago-claude-ai-agent[bot]@users.noreply.github.com>
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

PASS — Adds a daily clock job that re-enqueues the provider-tax pull for pending invoices whose taxes are still pending; verified against the codebase and CI is green.

  • Invoice.pending (status enum) and Invoice.tax_pending (tax_status enum, tax prefix) scopes both exist; the chained filter is correct.
  • PullTaxesAndApplyJob.perform_later(invoice:) matches the job's perform(invoice:) signature; that job is itself unique :until_executed, so nightly re-enqueues are safely deduped.
  • Clock registration mirrors the sibling webhook-cleanup entries (unused 01:30 slot, matching sentry slug/cron format).
  • Test is real: asserts the pull is enqueued only for the pending + tax-pending invoice and not for tax-succeeded or finalized invoices, plus the shared unique-job example.

Non-blocking: the job scans all organizations' invoices via find_each; fine for a batched nightly clock job.

@lago-claude-ai-agent
lago-claude-ai-agent Bot marked this pull request as ready for review July 21, 2026 14:51
@rsempe

rsempe commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

The job re-enqueues every pending + tax_pending invoice with no datetime bound or attempt cap, so an invoice that can't succeed retries forever

What about adding a restriction, for instance something like where("updated_at > ?", 7.days.ago) to avoid retrying silently?


def perform
Invoice
.pending

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

subscription_gated invoices should be included as well?

Skip invoices untouched for over 7 days so unrecoverable ones stop retrying.

Signed-off-by: lago-claude-ai-agent[bot] <297187938+lago-claude-ai-agent[bot]@users.noreply.github.com>
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

Thanks for the review — here's how I addressed each point.

  • Unbounded retry (retries forever): Added a THRESHOLD = -> { 7.days.ago } bound and .where("updated_at > ?", THRESHOLD.call), mirroring Clock::RetryGeneratingSubscriptionInvoicesJob. Invoices untouched for more than 7 days are no longer re-enqueued, so unrecoverable ones stop retrying silently. Added a spec case for a stale invoice being skipped.
  • Should subscription_gated invoices be included too? They're a distinct case: subscription_gated? is open? && subscriptions.any?(&:gated?), so those invoices are in open status awaiting gate release, not pending. This fallback targets invoices stuck in pending + tax_pending. Pulling taxes for gated open invoices would broaden the job into a different lifecycle state (and needs a per-invoice subscription check), so I've left it out — happy to add it as a follow-up if those are also getting stuck on taxes.

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

Held: CI is not green on this PR; not reviewed. Re-run CI, then re-review.

Checks: https://github.com/getlago/lago-api/pull/5987/checks

2 similar comments
@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

Held: CI is not green on this PR; not reviewed. Re-run CI, then re-review.

Checks: https://github.com/getlago/lago-api/pull/5987/checks

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

Held: CI is not green on this PR; not reviewed. Re-run CI, then re-review.

Checks: https://github.com/getlago/lago-api/pull/5987/checks

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor Author

PASS — Nightly clock job re-enqueues the provider-tax pull for invoices stuck in pending + tax_pending, bounded to recently-updated ones; verified against the codebase and CI is green.

  • Scopes are correct: status gives Invoice.pending, tax_status is prefix: :tax so tax_pending is the tax column, not payment status.
  • PullTaxesAndApplyJob.perform_later(invoice:) matches the job signature and mirrors the existing Invoices::RetryService call; that job is unique :until_executed, so nightly re-enqueues dedupe.
  • The updated_at > 7.days.ago bound actually terminates: a successful pull flips tax_status to succeeded and a provider error flips the invoice to failed, so neither stays in scope, and the no-op paths don't touch updated_at.
  • Clock entry, unique/lock_ttl, and the THRESHOLD lambda mirror Clock::RetryGeneratingSubscriptionInvoicesJob; the 01:30 slot is free.
  • Test is real: the stale invoice case fails without the threshold filter, and tax-succeeded / finalized invoices are asserted excluded.

Non-blocking:

  • If a stuck invoice's customer has no tax integration customer, the service returns a not-found failure and call! raises, so it will produce a failed job every night until the 7-day window closes.
  • Invoices stuck past 7 days are silently dropped from the retry — consider whether they warrant an error detail or alert rather than going quiet.
  • The head commit has no DCO Signed-off-by trailer (both earlier commits do).
  • Root cause of the stuck state is still unknown; this is the fallback retry, not a fix.

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