Act on the IPN event instead of the re-fetched invoice status - #106
Merged
Merged
Conversation
The IPN handler did not use the status that arrives in the notification. It re-fetched the invoice from BitPay's public endpoint and switched on that, which meant any notification arriving while the public endpoint reported a different status was discarded. The handler still answered 200, so BitPay treated it as delivered and never retried. There was also no case for 'confirmed', which is a normal step in the invoice lifecycle, so every invoice_confirmed notification was dropped unconditionally. The switch now runs on the event name, following the same model as the WooCommerce plugin. The re-fetched invoice is still used, but as a check that BitPay is not behind the event being processed. Mismatches are recorded with logTransaction so they appear under Billing > Gateway Log. Payment is applied once: invoice_confirmed applies it, and invoice_completed skips it when confirmed already did. Fixes #96
swlodarski-sumoheavy
requested changes
Sep 23, 2026
A late or retried invoice_paidInFull could move a confirmed transaction back to paid, and a Paid invoice back to Payment Pending. invoice_completed would then apply the payment a second time. Two callbacks arriving together could also both see the old status and both apply it. Each event now moves the row forward only from the states it expects, in a single conditional UPDATE, and the payment is applied only by the callback whose UPDATE changed the row.
swlodarski-sumoheavy
approved these changes
Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Act on the IPN event instead of the re-fetched invoice status
Fixes #96, where the WHMCS invoice never moves from Payment Pending to Paid.
The problem
bitpaycheckout_ipn.php ignored the status that arrives in the notification. It
re-fetched the invoice from BitPay's public endpoint and switched on that:
Two leftovers suggest this was not the intent:
There was no case for 'confirmed', which is a normal step in the lifecycle:
So every invoice_confirmed notification was discarded, and any other
notification arriving while the public endpoint reported a different status
was discarded too. In both cases the handler still returned 200, so BitPay
treated the notification as delivered and never retried. The invoice stays in
Payment Pending permanently.
Reproduced
WHMCS 9.0.7, plugin 5.1.x, sandbox endpoint.
Before the change, a BitPay invoice in 'new' with an invoice_completed
notification posted to the handler:
The change
The switch now runs on the event name, matching what the WooCommerce plugin
already does in class-bitpayipnprocess.php. The re-fetched invoice is kept,
but as a consistency check: BitPay may be ahead of the event being processed,
never behind it.
Payment is applied exactly once. invoice_confirmed applies it and marks the
row confirmed; invoice_completed checks that before applying again. This is
the same caution the WooCommerce settings give about mapping both confirmed
and complete to a paid state.
Mismatched or unhandled events are recorded with logTransaction, so they show
up under Billing > Gateway Log. The plugin had never written to that log
before this change.
Verified
There is no test suite in this repository, so this was verified by hand
against a local WHMCS 9.0.7 with the sandbox endpoint, by posting
notifications to the callback directly.
phpcs passes.
Not in this PR
Found while investigating, each to be filed separately:
overpays the invoice and turns the excess into client credit