Skip to content

FIX ispaid must reflect the real payment status, not the delivery workflow - #19

Open
Pichinov-Jose wants to merge 2 commits into
SplashSync:masterfrom
Pichinov-Jose:fix/ispaid-reflect-real-payment-status
Open

Pichinov-Jose wants to merge 2 commits into
SplashSync:masterfrom
Pichinov-Jose:fix/ispaid-reflect-real-payment-status

Conversation

@Pichinov-Jose

@Pichinov-Jose Pichinov-Jose commented Sep 11, 2026

Copy link
Copy Markdown

Bug

The ispaid flag (Order + Invoice objects, StatusFlagsTrait) is computed as order is validated OR delivered — a delivery-workflow heuristic that says nothing about payment.

Any shop that ships before payment (typical B2B wire-transfer flows, expressed through custom order statuses) gets its delivered-but-unpaid orders announced as paid. On the Dolibarr connector side this flag feeds Invoice::setPaid(), so real unpaid invoices end up classified as paid with zero payment attached.

Fix

Neither WooCommerce signal answers the question on its own, so the fix asks both and lets the status arbitrate.

  • is_paid() reads the current status, and wc_get_is_paid_statuses() excludes refunded. A refunded order is therefore announced as never paid — true of the order, false of the invoice. A refund is a credit note: a separate document, and the original invoice stays paid. Getting this wrong pushes the Dolibarr side to reopen settled invoices.
  • get_date_paid() reads the past, but WooCommerce stamps it on every transition into a paid status (maybe_set_date_paid()), even when no money moved — zero-value orders, and orders switched to processing and cancelled the same day, all carry one.
  • The status arbitrates: a cancelled or failed order never produced a receivable to settle.
$this->out[$fieldName] = $this->object->is_paid()
    || (
        (bool) $this->object->get_date_paid()
        && !in_array(
            $this->object->get_status(),
            array('cancelled', 'failed', 'checkout-draft', 'trash'),
            true
        )
    );

Measured

On a 3 998-order shop, number of orders whose ispaid announcement is wrong:

Predicate Wrong
original workflow heuristic 37 (failed announced paid, credit announced unpaid)
is_paid() alone 136 (all refunded orders)
get_date_paid() alone 87 (81 completed orders carry no payment date, 6 cancelled ones do)
both + status 0

The 81 completed orders without a payment date are why get_date_paid() cannot replace is_paid(), and the 136 refunds are why is_paid() cannot stand alone.

Scope

This fixes what the source announces. It cannot say how much of an invoice was actually received — a shop has one paid/unpaid state where accounting has a running balance, so a cheque schedule or a deposit is announced as paid on day one. That belongs on the consuming side: SplashSync/Dolibarr#30 (never unpay an invoice carrying real payments) and #31 (never close one the payments do not cover).

🤖 Generated with Claude Code

…kflow

The ispaid flag was computed as "order is validated OR delivered", which
says nothing about payment. Any shop that ships before payment (B2B wire
transfers) had its delivered-but-unpaid orders announced as paid — and on
the Dolibarr side this flag drives Invoice::setPaid(), so unpaid invoices
were being classified as paid with zero payment attached.

WooCommerce already knows the truth: $order->is_paid() is driven by
wc_get_is_paid_statuses(), which custom status plugins feed correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
is_paid() reads the CURRENT status, and wc_get_is_paid_statuses() excludes
'refunded'. A refunded order is therefore announced as never paid — true of
the order, false of the invoice, since the reversal is a credit note and the
original invoice stays paid.

get_date_paid() cannot stand alone either: WooCommerce stamps it on every
transition into a paid status, even when no money moved.

Measured on a 3 998-order shop — wrong announcements: 37 with the original
workflow heuristic, 136 with is_paid() alone, 87 with get_date_paid() alone
(81 completed orders carry no payment date), 0 with both plus the status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Pichinov-Jose

Copy link
Copy Markdown
Author

Amended: $order->is_paid() alone was not enough, and in one respect it was worse than the heuristic it replaced.

wc_get_is_paid_statuses() is processing + completed, so refunded is excluded and every refunded order was announced as never paid. That is true of the order and false of the invoice — the reversal is a credit note, a separate document, and the original invoice stays paid. On a shop I checked, that was 136 orders; the original heuristic got those right by accident.

get_date_paid() cannot replace it either: maybe_set_date_paid() stamps the date on every transition into a paid status, even with no movement behind it, and 81 completed orders in the same shop carry no date at all.

So the commit asks both and lets the status arbitrate. Wrong announcements over 3 998 orders: 37 with the original heuristic, 136 with is_paid() alone, 87 with get_date_paid() alone, 0 with the combination.

Worth knowing for anyone running the previous version of this branch: with is_paid() alone, every refunded order tells Dolibarr the invoice is unpaid. SplashSync/Dolibarr#30 stops that from reopening anything, but the announcement itself is wrong until this commit.

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