Add a read-only CreditNote object for WooCommerce refunds - #18
Open
Pichinov-Jose wants to merge 1 commit into
Open
Pichinov-Jose wants to merge 1 commit into
Pichinov-Jose wants to merge 1 commit into
Conversation
WooCommerce refunds are invisible to Splash. The connector exposes seven objects and none of them covers a refund, so a remote server sees an order total change but never learns that a credit note exists, who issued it, why, or which invoice it credits. Unlike Invoice, which is a virtual read-only view of an Order, CreditNote is backed by a real WooCommerce entity: the shop_order_refund post type. Its post_parent is the exact counterpart of the fk_facture_source field that the Dolibarr connector's CreditNote object carries. Read-only for now, like Invoice: creating a refund can trigger a gateway refund, so the write path is left out until it is designed explicitly. Notes on two WooCommerce behaviours this had to accommodate: - Signs. WooCommerce exposes the same money twice: get_total() is negative, get_amount() is positive. Totals are built on get_total() so amounts arrive already negated; get_amount() is exposed separately as 'amount'. This is why no equivalent of the Dolibarr connector's CreditModeTrait is needed here. Order\TotalsTrait::toTotalPrice() guards its VAT computation with $totalTaxExcl > 0, which never holds on a credit note and would report every one of them as 0% VAT, so the rate is computed on absolute values instead. - Full versus partial. The _refund_type meta looks like the answer but is not: it is written by WooCommerce Analytics on the woocommerce_order_(partially| fully)_refunded hooks, so it records the state the ORDER reached when the refund landed rather than the refund's own coverage, and it is never revised when a sibling refund is deleted or the order is edited. On a shop of 448 refunds, 165 of 399 carry 'full' while covering part of their order. The object recomputes is_order_fully_refunded live from the parent instead. Line items are the exception rather than the rule — 1 refund in 400 on that same shop has any — so an amount-only refund is rendered as a single line built from its amount and reason, flagged is_virtual_item so a consumer can tell rebuilt detail from real detail. Hooks commit the refund on woocommerce_refund_created and woocommerce_refund_deleted, and re-commit the parent Invoice, whose outstanding amount changed at the same time.
Pichinov-Jose
marked this pull request as ready for review
September 8, 2026 22:26
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.
Closes #17.
WooCommerce refunds are invisible to Splash. The connector exposes seven objects and none covers a refund, so a remote server sees an order total change but never learns that a credit note exists, who issued it, why, or which invoice it credits.
This adds a read-only
CreditNoteobject. UnlikeInvoice, which is a virtual view of anOrder, it is backed by a real WooCommerce entity: theshop_order_refundpost type. Itspost_parentis the exact counterpart of thefk_facture_sourcefield that the Dolibarr connector'sCreditNotecarries — see SplashSync/Dolibarr#29, which exposes it on that side. The two ends of the mapping now exist.Scope
Read-only, like
Invoice.wc_create_refund()can trigger a gateway refund, so the write path is a money-moving operation that deserves its own design rather than being slipped into a first pass.create()anddelete()refuse explicitly.Two WooCommerce behaviours worth flagging
Signs. WooCommerce exposes the same money twice:
get_total()is negative,get_amount()positive. Totals are built onget_total(), so amounts arrive already negated and no equivalent of the Dolibarr connector'sCore\CreditModeTraitis needed here.get_amount()is exposed separately asamountfor servers that prefer a positive figure.Related:
Order\TotalsTrait::toTotalPrice()guards its VAT computation with$totalTaxExcl > 0, which never holds on a credit note and would report every one of them as 0% VAT. This PR uses a localtoCreditPrice()computing the rate on absolute values, rather than changing the Order path. Happy to fix it there instead if you prefer.Full versus partial. The
_refund_typemeta looks like the answer and is not. It is written by WooCommerce Analytics (Admin\API\Reports\Products\DataStore) on thewoocommerce_order_(partially|fully)_refundedhooks, so it records the state the order reached when the refund landed — not the refund's own coverage — and it is never revised when a sibling refund is deleted or the order is edited. On the shop this was built against, 165 of 399 refunds carryfullwhile covering only part of their order; one carriesfullfor 125.00 on a 250.00 order with no sibling at all. The object exposesis_order_fully_refunded, recomputed live from the parent, instead.Line items are the exception. 1 refund in 400 on that shop has any. An amount-only refund is rendered as a single line built from its amount and reason, flagged
is_virtual_itemso a consumer can tell rebuilt detail from real detail.Fields
parent_idfk_facture_sourcecounterpartparent_order_id_customer_idreference#<order>-R<id>reason,refunded_by,refunded_paymentis_order_fully_refunded,order_total_refundedtotal,total_ht,price_totalamountitems[]is_virtual_itemHooks
woocommerce_refund_createdandwoocommerce_refund_deletedcommit the refund, and re-commit the parentInvoice, whose outstanding amount changed at the same time. Registered from the plugin bootstrap alongside the other objects; the object itself is picked up by the existingsrc/Objectsscan.Testing
Business logic was exercised against 400 real refunds on a production shop, read-only: every getter used here, no exceptions; the sign convention held on all 400; 400 distinct references, no collisions; no orphan refunds, so the invoice link always resolves; 17 orders carry more than one refund, which is what makes a dedicated object necessary rather than a field on the order.
I could not exercise the framework wiring (FieldFactory,
Splash::commit) against a live Splash server, so that part deserves your eye.Open question
The Dolibarr connector's
CreditNotecarriesprotected static bool $disabled = true. It is otherwise complete, down to its own price-inversion trait. Was that a deliberate product decision, or a leftover? The answer probably ought to apply to this object too, and I have left it enabled here pending your view.Opened as a draft: it is a first pass meant to invite your direction on scope before it is polished.