Dette er en demoshop til testformål. Ordrer vil ikke blive gennemført. Luk

Videre til indhold
  • Forside
  • Løsninger
    • Hjemmesider
    • Nyhedsbreve
    • Webshop
    • WordPress
  • Webalive plugin
    • WooCommerce produkt sortering
  • kontakt
  • Forside
  • Løsninger
    • Hjemmesider
    • Nyhedsbreve
    • Webshop
    • WordPress
  • Webalive plugin
    • WooCommerce produkt sortering
  • kontakt

© 2019

Webalive plugin

← Tilbage til alle plugins

Woo Webalive Dinero Sync

Af Webalive

Version 1.0.6 WP 6.0+ Testet 7.1 PHP 7.4+ Opdateret 2026-09-12

Beskrivelse

Woo Webalive Dinero Sync connects a WooCommerce shop to Dinero (https://dinero.dk). It can synchronize:

  • Products (catalog -> Dinero products)
  • Customers (Woo customers -> Dinero contacts)
  • Sales (Woo orders -> Dinero invoices)
  • Credit notes (Woo refunds -> Dinero credit notes)

Designed to integrate with Woo Webalive Order Premium when present, and exposes internal REST endpoints under /wp-json/woo-wa-dinero-sync/v1/ for triggering synchronization tasks.

Ændringslog

1.0.6

  • Marked as tested with WordPress 7.1 and WooCommerce 11.1.0.

1.0.5

  • Admin: The version footer now shows tested-version details only when the active WordPress or WooCommerce version differs from the supported version.

1.0.4

  • Admin: Added a compact version footer on every Dinero Sync tab showing the active WordPress and WooCommerce versions, highlighted when they differ from the tested versions.
  • Compatibility: Declared testing with WordPress 7.1 and WooCommerce 11.0.1.

1.0.3

  • Packaging: Added the missing languages directory so generated translations have the path declared by the plugin header.

1.0.2

  • Fix: Prevented a Composer autoloader class collision when activating Woo Webalive Dinero Sync during migration from the old Webalive Dinero Sync plugin.

1.0.1

  • Packaging: Added WAUM ignore rules that keep development, test, diagnostic and working data out of deployed plugin packages.

1.0

  • Migrated the latest Webalive Dinero Sync runtime to the woo-wa-dinero-sync plugin identity, text domain, namespace and bootstrap while preserving existing wads_* settings, hooks, data and WWOP helper contracts.
  • Credit notes now support generic WWOP deductions as negative gross lines and reconcile exactly to the WooCommerce/QuickPay refund amount.

0.9.54

  • Product metabox: Duplicate SKU no longer hides the whole sync UI. Previously, when a product had a duplicate SKU the metabox took over the entire body — no Import from Dinero picker, no Sync-now button, and (after 0.9.53) only a Detach button when a GUID was stored. Detaching therefore left the operator staring at the duplicate-SKU warning with no way to set up a fresh sync until the SKU conflict was resolved elsewhere. The duplicate-SKU warning is now a top-level notice banner (same visual style as the "Cloned from another product" and "Shared Dinero GUID" banners), and the normal not-synced / synced UI renders below it. The "Sync now" button is disabled while the SKU conflict exists (Sync would fail anyway), but the Import-from-Dinero picker, Verify, and Detach buttons remain available so operators can prepare the product and free stale meta without leaving the edit screen.

0.9.53

  • Product metabox: Detach GUID button now shown when SKU is duplicated. Previously, if a cloned product had BOTH a duplicate SKU AND carried a stored Dinero GUID (typical "Copy to a new draft" scenario), the metabox rendered the "Duplicate SKU" warning but hid every action button — including the Detach GUID button that the accompanying "Cloned from another product" / "Shared Dinero GUID" warnings explicitly tell the operator to click. The operator was locked out of resolving the clone/shared-GUID situation until the SKU conflict was fixed first. Now the duplicate-SKU branch also renders a Detach GUID button whenever a GUID is stored, so operators can safely disconnect the carried-over meta before touching the SKU.

0.9.52

  • Finish the recipient picker started in 0.9.51. Modal + preparation AJAX were half-implemented. This bump ships the complete flow:
    • New AJAX endpoint wads_quote_prepare_send (nonce + manage_woocommerce) fetches the quote's Dinero contact and returns every valid email parsed from the contact's Email field.
    • Clicking a quote-row's Send button now opens an inline modal with one checkbox per candidate email (all pre-checked), a small input to add another email, and Send / Cancel buttons.
    • wads_quote_send accepts recipients[] and validates each entry through is_email() server-side.
    • QuoteConverter::send_accept_link() builds the mail's To: header from the recipient list — all selected addresses go into visible To: (not BCC), so every recipient sees who else got the mail. Display name (Dinero contact name) wraps the first address only; additional recipients are appended plain.

0.9.51

  • Quotes: email picker. Instead of silently picking the first valid email from the Dinero contact's Email field (0.9.50), clicking Send now opens a modal: every parsed candidate address is listed as a checkbox (all pre-checked), plus an "Add another email" input for a one-off recipient. Click Send to selected and every checked / added address receives the accept-link / thank-you email in a single wp_mail() call.
  • New AJAX endpoint wads_quote_prepare_send (nonce + manage_woocommerce) returns the parsed candidate emails + basic tradeoffer metadata.
  • QuoteConverter::send_accept_link() now accepts an optional array $recipients and refuses to send if the list contains any invalid address. Falls back to auto-picking (0.9.50 behaviour) when the caller does not pass recipients — keeps the public API backwards compatible for external callers.

0.9.50

  • Quotes: Dinero contact email splitting. Dinero exposes a single Email string on ContactReadModel, but operators frequently store multiple addresses in that field separated by , / ; / whitespace / |. QuoteConverter::send_accept_link() and apply_contact_to_order() now split the raw Email value on those separators, pick the first entry that passes is_email(), and use it as the recipient / billing email. Falls back to "no valid email address" only when NONE of the entries validates.

0.9.49

  • Quotes: UserAccepted treated as accepted. Follow-up to 0.9.48: both UserAccepted (operator-side accept in Dinero) and CustomerAccepted (customer-side accept via Dinero email) now trigger the thank-you + payment link flow. Only Draft still uses the "please accept your quote" wording. Button label reads "Send thank-you + payment link" on both accepted states.

0.9.48

  • Quotes tab: CustomerAccepted flow + thank-you email. Once a customer accepts a Dinero trade offer in the Dinero portal (Status = CustomerAccepted), it disappeared from our Quotes tab because the filter only allowed Draft and UserAccepted. Extended to include CustomerAccepted so the row stays visible until the quote is invoiced. Additionally:
    • QuoteConverter::send_accept_link() now detects the status and swaps in a thank-you body when the quote is CustomerAccepted: subject "Thank you for your order #… — complete your payment", intro "Thank you for accepting your quote from {site}. Click the button below to place your order in the cart, pick shipping and pay.", button label "Complete order and pay". Same signed accept URL underneath so it still lands in wc_get_checkout_url().
    • QuoteConverter::fill_cart() also allows CustomerAccepted — the public URL works for the follow-up payment step, not only the initial acceptance.
    • On the Quotes tab, CustomerAccepted rows now render Send thank-you + payment link in place of the plain accept-link button so operators visually distinguish the two flows.

0.9.47

  • Import from Dinero: expand - into a line break in the product comment. Operators use the space-hyphen-space separator in Dinero's Comment field to visually delimit bullet-style attributes on one line. Converted verbatim to a Woo short description that looked cramped. ajax_dinero_import now replaces every - in the fetched Comment with \n before passing it to the client, so the short description populates as one line per attribute. The existing TinyMCE-branch of the JS setter already turns \n into <br> for the visible editor, so the multi-line layout carries through both the classic textarea and the visual editor.

0.9.46

  • Absolute rule enforced: never sync anything that is not published. Follow-up to 0.9.45 which locked down products only. Added defensive status guards on every remaining sync entry point so unpublished / auto-draft / trash / pending content can never reach Dinero regardless of which code path triggers the sync (hooks, admin, REST, external do_action calls).
    • Sync\Sales::push_order(): refuses with wads_order_not_eligible when the order status is not in the whitelist completed / pickup / delivered / ready-pickup / partial-shipped. Draft / trash / cancelled / failed / on-hold / pending payment orders cannot create Dinero invoices via any path.
    • Sync\Customers::push_user(): refuses with wads_user_blocked when the WP user is a spam user (multisite), is soft-deleted, or has a role of "none". Only real, active users flow to Dinero.
    • Hooks\WooHooks::on_order_completed() and on_customer_changed(): pre-flight status checks so the sync classes don't even get called on ineligible records.
    • Plugin::sync_order_action() (the wads_sync_order public action hook used by WWOP): same status whitelist so external plugins can't accidentally push a draft/cancelled order.

0.9.45

  • Critical hotfix: stop auto-syncing auto-draft products with placeholder title/SKU. Two long-standing bugs that combined to corrupt Dinero records on the reported site:
    • WooHooks::on_product_saved() fired push_product() on every product save, including WP's auto-draft (created automatically the moment an operator opens the "Add New Product" screen). Now skipped for post_status auto-draft, draft, trash and inherit — only truly published products flow to Dinero via the auto-hook.
    • Sync\Products::map() had a productNumber = 'WC-' . $product->get_id() fallback when SKU was empty, PLUS blindly used the current post title (which for a fresh auto-draft is literally "Auto Draft"). Fallback removed. push_product() now refuses to sync (returns wads_no_sku) when the product has no SKU or when its title is empty / the literal "Auto Draft". No more garbage records created in Dinero.

0.9.44

  • Critical hotfix: Import / Verify / Detach corrupted Dinero records via a hook cascade. All three metabox AJAX handlers persisted metadata via $product->save(), which fires WooCommerce's woocommerce_update_product action. WooHooks::on_product_saved() listens on that action and calls Products::push_product(), which on an auto-draft product (empty title/SKU/price) built a payload with the WP default title Auto Draft and the fallback SKU WC-{id} and PUT it to Dinero. Result: importing 511-HP-SEN5-WS-G into an auto-draft overwrote the real Dinero record with Name = "AUTO-DRAFT" and ProductNumber = "WC-24726". Same class of bug on the Detach and Verify (404) paths.
  • Fixed by switching all three metabox handlers from $product->update_meta_data(...) + $product->save() to direct update_post_meta() / delete_post_meta() calls (which do not fire WooCommerce's save action). write_log() also converted so log entries no longer cascade into a sync.
  • Explicit user-triggered sync (ajax_sync → push_product) is unchanged — that path is supposed to write to Dinero.
  • Note: any Dinero product corrupted by earlier auto-draft imports needs to be recovered manually in Dinero, then reconnected via the metabox Import.

0.9.43

  • Import: reuse search-derived price instead of re-normalising SINGLE GET. Investigating the recurring price mismatches: the GET /v1/{org}/products LIST endpoint and the GET /v1/{org}/products/{guid} SINGLE endpoint on the reported Dinero account return BaseAmountValueInclVat in DIFFERENT scales for the same product (search dropdown showed 62,00 correctly, import filled in 6200). Rather than trying to detect the scale, the search-endpoint's already-normalised price is now piped through the client (as data-price-* on each search hit) and posted back to the import handler, which uses those values verbatim. The SINGLE GET is still called — but only to retrieve fields the LIST endpoint doesn't include (Comment, plus product number as safety net).

0.9.42

  • Revert to unconditional /100 for Dinero product prices. 0.9.41's two-tier heuristic (>100000 → /10000) misinterpreted 780000 as 78 kr when the actual product costs 7800 kr on the reported account. Reverted to the simpler raw / 100 conversion — the reported account consistently uses øre scaling, so 780000 → 7800,00 kr and 620000 → 6200,00 kr. If future accounts turn out to use a different scale, we will revisit with explicit per-account configuration rather than heuristics.

0.9.41

  • Import: two-tier price heuristic for Dinero's 1/10000 precision. Production reported prices coming back at 100× the expected value (62,00 kr shown as 6200). The reported account actually returns prices in units of 1/10000 kr, not øre. normalise_price() now tries three scales: values > 100000 are divided by 10000 (1/10000 precision), values > 100 by 100 (øre), otherwise kept as-is (decimals). Handles all three Dinero API dialects observed in the wild.

= 0.9.40 =* Import: price heuristic + remove already-linked block.

  • Price normalisation swapped from unconditional /100 (0.9.39) to a heuristic: values > 100 are treated as integer øre and divided by 100, values <= 100 are used as-is. Handles both Dinero API dialects (decimals + øre-mode accounts) reasonably. Edge case where a product actually costs 101–999 kr as a decimal will still be miscalculated — the operator can adjust after import.
  • ajax_dinero_import no longer refuses when the current Woo product already carries a wads_dinero_id. Per operator request, multiple Woo products may reference the same Dinero product; the import now simply overwrites the current link with the new {woo_id}:{guid} value. The Settings → Products → Shared Dinero GUIDs review still surfaces multi-linked situations if the operator later wants to unwind them.

0.9.39

  • Hotfix: Import from Dinero — price and comment. Two fixes reported from production:
    • Amount 780000 = 7.800,00. The GET /v1/{org}/products list endpoint on the reported Dinero account returns BaseAmountValue / BaseAmountValueInclVat as integer øre (7800.00 kr stored as 780000), not as the decimal we expected. Both ajax_dinero_search and ajax_dinero_import now divide these two fields by 100 before returning to the browser, so the search dropdown and the pre-filled #_regular_price field show the correct amount.
    • Kommentar til produktet vises ikke. The Dinero product's Comments field is looked up under both Comments and legacy variants Comment / Description (case-insensitive), covering the different property names Dinero returns depending on account age. Additionally, when the Woo short-description editor is initialised as TinyMCE (default for classic Woo editor), setting the textarea's .value alone did not update the visible editor — the JS now calls tinymce.get('excerpt').setContent(...) as well when TinyMCE is present, and dispatches change on the textarea so autosave picks it up.

0.9.38

  • Hotfix: Import search HTTP 400 "Error in property name: Comments". The GET /v1/{org}/products list endpoint only accepts the field whitelist documented in swagger (Name, ProductNumber, Quantity, Unit, AccountNumber, BaseAmountValue, BaseAmountValueInclVat, TotalAmount, TotalAmountInclVat, ExternalReference, CreatedAt, UpdatedAt, DeletedAt) — requesting Comments in fields returns validation code 45. Removed Comments from the search-endpoint fields list; Comments is still fetched via the per-product GET /products/{guid} call inside ajax_dinero_import() so the short-description population still works.

0.9.37

  • Import from Dinero on the product edit screen. New collapsible "Import from Dinero…" panel in the product metabox, shown only when the product has no wads_dinero_id yet (typical empty-varekort case). Debounced (300 ms) AJAX search hits GET /v1/{org}/products?freeTextSearch={q}&fields=Name,ProductNumber,ProductGuid,BaseAmountValue,BaseAmountValueInclVat,Comments,AccountNumber&pageSize=20 and lists matches with SKU + name + price incl. VAT. Clicking a hit runs the import:
    • Woo product title ← Dinero Name
    • Woo SKU (#_sku) ← Dinero ProductNumber
    • Woo short description (#excerpt) ← Dinero Name + Comments (appended to any existing excerpt)
    • Woo regular price (#_regular_price) ← Dinero BaseAmountValueInclVat (price incl. VAT as computed by Dinero itself — no local VAT recalculation)
    • wads_dinero_id postmeta written immediately in the clone-safe {woo_id}:{dinero_guid} format (respects orphan / shared-GUID rules on the next Sync)
    • _wads_dinero_last_sync log records the import so the audit trail is preserved
  • AJAX endpoints: wads_product_meta_dinero_search and wads_product_meta_dinero_import (nonce + manage_woocommerce).
  • Refuses to import when the current product already carries a wads_dinero_id — must Detach first.

0.9.36

  • Quotes tab: translate Dinero API status labels. All strings on the Quotes tab were already wrapped in __() with the correct text domain, but the Status and Mail-out columns rendered Dinero's raw API values (Draft, UserAccepted, Sent, NotSent, etc.) directly — those are not source strings the extractor picks up, so a Danish bookkeeper saw English badges. Added two mapping helpers translate_quote_status() and translate_mailout_status() that pipe every known Dinero value through __() with English source strings, and applied them to the badge + mail-out cells. Unknown/future statuses fall through as-is so no data is hidden.
  • Same fix on the wads_quote_wrong_state and email-sent WP_Error messages: the interpolated status is now the human-readable translated form, not the raw Dinero code.

0.9.35

  • Hotfix: sales sync HTTP 400 "body does not match expected schema" after 0.9.16. Sync\Sales::product_guid_for_item() still read the raw wads_dinero_id postmeta, so every order line for a product whose meta had been rewritten in the clone-safe {woo_id}:{guid} format sent Dinero productGuid = "12345:6ac21fc9-…" — not a UUID. Dinero rejected the whole invoice payload. Fixed by piping the raw value through Products::decode_meta() so only the bare GUID reaches the API. Sales sync should resume immediately after deploy for all affected orders.

0.9.34

  • Quote conversion: SKU exclusion list. Real-world case: a Dinero trade offer had a 400-0001 / Postnord Flex / 40 kr line as a manual shipping charge, but when the customer accepted the quote and landed on Woo checkout, Woo's own shipping methods added their own shipping fee on top — the customer got charged for shipping twice. Fixed by adding a "SKUs to skip when converting quotes" textarea to the Quotes tab: one SKU per line, blank lines and # comments ignored. Matched against the Dinero line's ProductNumber case-insensitively. Skipped lines are dropped entirely before resolve_products() runs — no cart entry, no order line, no auto-created Woo product. Applies to both Push as order and the customer-facing accept link flow. Setting persists in option wads_quote_exclude_skus (array).

0.9.33

  • Send accept link now attaches the Dinero quote PDF. The email produced by "Send accept link" on the Quotes tab now includes the actual Dinero-generated trade offer PDF as an attachment, on top of the existing signed accept-link button. The customer receives the same document Dinero would send, PLUS a single-click checkout URL.
  • PDF is fetched via GET /v1/{org}/tradeoffers/{guid} with Accept: application/octet-stream and cached under wp-content/uploads/wads-pdf-cache/{YYYY}/ — the year folder is derived from the trade offer's Date (business chronology), not from the send date. Cache is content-idempotent: filename is deterministic (Quote_{number}_{YYYY-MM-DD}.pdf, translatable via _x('Quote_%1$d_%2$s', 'Quote PDF filename pattern', ...)) so subsequent sends reuse the cached copy instead of re-downloading.
  • PDF fetch failures are non-fatal — the email still ships with just the accept link, and the failure is logged to error_log for debugging.

0.9.32

  • Send accept link (email) instead of Copy. Replaced the clipboard-copy button on the Quotes tab with a Send accept link button that actually emails the signed URL to the Dinero contact's registered email address via wp_mail(). HTML email with quote number, total incl. VAT, and a prominent button linking to the site's ?wads-quote=...&sig=... accept URL. On success the row shows "Sent to email@example.com"; on failure the AJAX error surfaces (missing contact email, wp_mail SMTP failure, etc.). New AJAX endpoint wads_quote_send (nonce + manage_woocommerce).

0.9.31

  • Quotes: destructive conversion machinery. Buttons on the Quotes tab are now live.
    • Sync\QuoteConverter: fetches a Dinero trade offer, resolves each ProductLine to a Woo product (reverse lookup on wads_dinero_id → fallback to SKU-lookup with adoption → fallback to auto-create a new WC_Product_Simple with SKU=ProductNumber, name from line description, price from BaseAmountValue/BaseAmountValueInclVat per Woo's tax mode, catalog_visibility=hidden so it doesn't appear in the shop until an operator promotes it). New auto-created products immediately store wads_dinero_id = {woo_id}:{product_guid} in the clone-safe format.
    • Push as order button: server-side conversion to a Woo order in status pending payment. Line prices are overridden to match the trade offer's BaseAmountValue (excl. VAT) so the customer sees the same totals as on the quote. Dinero contact is fetched via /contacts/{guid} and applied as billing (email, name, street, postcode, city, country). If the email matches a WP user the order is assigned to that user; otherwise the order is a guest with the Dinero contact's address. Order metadata _wads_dinero_tradeoffer_guid + _wads_dinero_tradeoffer_number links back to the source quote. AJAX response returns the order edit URL + the customer-facing pay-for-order URL for immediate copy/share.
    • Send accept link button: renders a signed public URL (?wads-quote={guid}&sig={hmac}) inline for one-click copy. Signature: HMAC-SHA256 with a per-site secret stored in option wads_quote_secret (auto-generated on first use).
    • Public handler Front\QuoteAccept: hooks init, validates the signature, ensures WC session, empties the current cart, uses QuoteConverter::fill_cart() to add the trade offer's products at their quoted quantities, then wp_safe_redirect( wc_get_checkout_url() ). Customer flows through normal Woo checkout → payment → order lifecycle.
  • AJAX endpoint added: wads_quote_push (nonce-guarded, manage_woocommerce).

0.9.30

  • Hotfix: stale duplicate-SKU count in sync summary. Sync\Products::sync_pending_batch() and sync_stale_batch() had the wrong cache logic: they read the persistent ReviewList option, and only refreshed it if it was empty. Result — once an old publish duplicate was recorded, subsequent sync runs saw stale data indefinitely (even after the operator resolved the duplicate by converting it to a draft or changing the SKU). Sync summaries kept reporting "N dubletter" for products that no longer existed as duplicates. Fixed by always calling refresh_duplicate_review() at the start of every batch (cheap: one JOIN + one option write). The duplicates counter now reflects the CURRENT catalog state, not a historical snapshot.
  • Note: the destructive QuoteConverter + customer-accept-URL feature originally planned for 0.9.30 has moved to 0.9.31 so this hotfix ships alone.

0.9.29

  • New: Dinero Quotes tab (foundation). Added page=wads-settings&tab=quotes (labelled "Quotes"). Reads active trade offers (Dinero "tilbud") from /v1/{org}/tradeoffers and lists them with number, date, contact, status, mail-out status and total incl. VAT. Filtered to Draft + UserAccepted (the states worth acting on) with in-PHP filtering to avoid Dinero's undocumented queryFilter syntax. Paginated 50-per-view. Placeholder "Push as order" and "Send accept link" buttons render in the Actions column but are intentionally disabled and marked Coming in 0.9.30 — the destructive conversion machinery + public URL flow lands in the next bump so this foundation can be verified in production first.
  • Sync\Quotes API wrapper: list_active(int $page, int $page_size) and get(string $guid).
  • Plugin::quotes() factory registered alongside the other sync services.

0.9.28

  • Canonical-per-SKU sync rule. Business reality on the reported site: multiple Woo product entries legitimately share the same SKU (varenummer) and only ONE should ever reach Dinero. Previously Sync\Products blocked every entry in a duplicate-SKU group with wads_duplicate_sku, so no Dinero record was created / updated at all.
    • Products::pick_canonical_for_sku() elects a single winner per SKU group: any entry that already carries wads_dinero_id wins (respects earlier operator decisions); otherwise the lowest Woo post ID wins (deterministic, first-created).
    • refresh_duplicate_review() now blocks only the NON-canonical entries in each SKU group. The canonical entry flows through the runner and is POST'ed / PUT'ed / adopted like a normal product.
    • push_product()'s upfront SKU guard also switched from "block if SKU appears twice" to "block if this product is not the canonical entry" — metabox clicks on a non-canonical entry now return wads_sku_alias (message: "SKU shared with Woo product #X which is the canonical entry synced to Dinero.") instead of the misleading wads_duplicate_sku.
    • Settings → Products → Duplicate SKUs review section now marks the canonical entry with a ★ badge and adds a note explaining the rule.
  • No auto-migration required: existing links stay put; new syncs simply stop wasting slots on the aliases.

0.9.27

  • Adoption: auto-detach stale local owners when Dinero confirms the current product is the rightful owner. After 0.9.25 introduced the wads_adopt_would_conflict guard, production hit dozens of "Refused to adopt … already stored on Woo product(s) #23633" failures where the OTHER Woo product's SKU did not match Dinero's productNumber for that GUID (typical clone-inherited-GUID situation). New tiebreaker in try_adopt_by_product_number():
    • Dinero's own ProductNumber for the target GUID is the source of truth. If it matches the current Woo product's SKU exactly (case-insensitively), the current product is definitively the rightful owner.
    • Any other Woo product that stores the same GUID is by definition holding a stale / clone-inherited value. Their wads_dinero_id postmeta is cleared automatically (Dinero is not touched — the record stays intact for the current product to adopt).
    • A last-sync-style note is written on each detached product (_wads_dinero_last_sync) so the operator sees what happened when they open that product later: Auto-detached by sync: this GUID actually belongs to Woo product #X (matching productNumber "...").
    • Adoption then proceeds normally: PUT + write {woo_id}:{guid} on the current product.
    • The wads_adopt_would_conflict refusal is now reserved for the rare edge case where the tiebreaker cannot decide (e.g. Dinero returned no ProductNumber or none of the SKUs match) — in that case the operator still has to resolve manually via Settings → Products → Shared Dinero GUIDs.

0.9.26

  • Adoption lookup: switched to freeTextSearch + surface adoption failure reason. After 0.9.23 shipped the auto-adopt-on-productNumber-collision path, production still returned identical HTTP 400 uniqueness errors instead of adopting. Two root causes fixed:
    • The lookup used queryFilter=ProductNumber eq '{sku}', whose exact syntax is not documented in Dinero's swagger and returned an empty collection for SKUs like 583-P20B29. Switched to freeTextSearch={sku} (documented as "free text search performed on Name and ProductNumber") plus a strict case-insensitive ProductNumber match in PHP, walking the first two pages of the result set. Now finds records that Dinero's own validator already knows exists.
    • When adoption failed (no match, would-conflict, PUT error) the code silently returned the ORIGINAL POST error, hiding WHY adoption did not succeed — the operator just saw the same "Varekoden skal være unik" error 25 times. Adoption failures are now returned unchanged so the sync log shows either wads_adopt_not_found, wads_adopt_would_conflict (with the conflicting Woo IDs) or the Dinero PUT error, whichever explains the outcome. Actionable per-row diagnostics.

0.9.25

  • Adoption guard: refuse to re-attach a GUID that another Woo product already owns. In 0.9.23 the automatic try_adopt_by_product_number() helper looked up an existing Dinero product by SKU and adopted its GUID. Bug: when the operator had just Detached the same GUID from another Woo product to break a shared-GUID conflict, re-running the sync happily re-attached the very same GUID to the freshly-detached product — recreating the conflict the operator had just fixed. Fixed by extending the adoption path with a wp_postmeta check for other published products carrying that GUID (matches both legacy bare-GUID and clone-safe {id}:{guid} formats). When any other owner is found, adoption is refused with a wads_adopt_would_conflict WP_Error naming the conflicting product ID(s) so the operator sees exactly what happened and can resolve it. Sync no longer creates new shared-GUID conflicts on its own.

0.9.24

  • Shared Dinero GUIDs scan: restricted post_status filter to publish only (was publish, draft, private, pending, future). Drafts are noise — they typically never reach Dinero and would inflate the conflict list with irrelevant rows. This mirrors the rest of the plugin's product sync scope, which also only touches published products.

0.9.23

  • Auto-adopt existing Dinero product on productNumber collision. Bulk sync ("Sync unsynced + legacy products") on production returned dozens of HTTP 400 Validation Errors of the shape {"validationErrors":{"productNumber":"Varekoden skal være unik."}} — Dinero rejects a POST when a product with that productNumber already exists there. Root cause: those products were created directly in Dinero (or by an earlier import) and Woo never learned the GUID. Sync\Products::push_product() now detects Dinero validation code 42 with a productNumber violation and calls the new try_adopt_by_product_number() helper:
    • GET /v1/{org}/products?queryFilter=ProductNumber%20eq%20'{sku}'&fields=Name,ProductNumber,ProductGuid — look up the existing Dinero record.
    • On exact (case-insensitive) match: PUT /v1/{org}/products/{guid} with the Woo payload to reconcile fields, then write wads_dinero_id = {woo_id}:{guid} in the clone-safe format.
    • Turns a HTTP 400 into an OK adoption without operator intervention. Falls back to the original error only when Dinero has zero matches or the PUT itself fails.
  • Result on the reported 25-failure batch: those rows should now sync green.

0.9.22

  • Shared Dinero GUIDs: hide the entire section (heading, description, diagnostic "Scanned N" line and the "No shared Dinero GUIDs detected" placeholder) when the scan finds zero conflict groups. Now that the scan is trusted to work correctly, an all-clean state is signalled by simply not rendering the section at all instead of a noisy "(0)".

0.9.21

  • Targeted "unsynced + legacy" product sync. New third button on Settings → Products next to "Sync ALL" / "Sync pending only": Sync unsynced + legacy products — runs the chunked runner but restricts the worklist to products that either have no wads_dinero_id at all OR carry the pre-0.9.16 bare-GUID format (meta_value NOT LIKE '%:%'). Legacy rows are PUT to Dinero and the meta is automatically rewritten in the clone-safe {woo_id}:{guid} format on success. The button label shows the current backlog count so you know upfront how much work is queued. Fastest path to bring a whole shop up to the new safe format.
  • Sync\Products::sync_stale_batch(): new method that returns the same shape as sync_pending_batch() with an added total_stale counter; used by the new runner mode.
  • SettingsPage::ajax_products_chunk(): accepts new mode stale; whitelisted in assets/products.js alongside all / pending.

0.9.20

  • Shared Dinero GUIDs table: Detach and Verify now update the table inline instead of triggering a full window.location.reload(). Detach fades the affected row out and, if the parent GUID group is left with only one product, collapses the whole group away (no more conflict). Verify shows the result message beside the row and leaves the row in place. Both actions are now genuine per-row AJAX interactions.

0.9.19

  • Removed the window.confirm() prompt on Detach GUID in both the product metabox and the Settings → Products → Shared Dinero GUIDs table. Detach only clears the local wads_dinero_id postmeta; the Dinero record itself is untouched and the last-sync log shows what happened, so the extra click was pure friction.

0.9.18

  • Shared Dinero GUIDs scan: robust rewrite. Live site reported "(0) — No shared Dinero GUIDs detected" even when known duplicates existed. Root cause suspected: MySQL strict ONLY_FULL_GROUP_BY sql_mode silently rejected the previous GROUP BY SUBSTRING_INDEX(…) + aggregate query on some hosts. Rewrote the scan as a plain SELECT post_id, meta_value fetch (any post_status except trash / auto-draft) followed by an in-PHP grouping that decodes the raw meta value once and buckets by the extracted GUID. Works on every MySQL / MariaDB regardless of sql_mode.
  • Section header now shows both the number of shared-GUID groups and the total number of products with a stored wads_dinero_id (diagnostic), so "0" clearly distinguishes "data clean" from "scan matched nothing at all".
  • Post-status filter broadened to include publish, draft, private, pending, future — catches products currently sitting outside publish that still carry a stale GUID from a clone.

0.9.17

  • Settings → Products: new "Shared Dinero GUIDs" review section. Scans wp_postmeta and groups every Woo product / variation that stores the same Dinero ProductGuid (handling both the legacy bare-GUID format and the 0.9.16+ {id}:{guid} format via SUBSTRING_INDEX(meta_value, ':', -1)). For each group the table lists all conflicting products (ID, title, SKU, edit link, stored-woo-id extracted from the meta) and offers per-row Verify in Dinero and Detach GUID buttons that reuse the existing metabox AJAX endpoints (wads_product_meta_verify, wads_product_meta_detach). This is the central recovery workflow for the clone-inherited-GUID class of bugs found on 583-P20B29 / 583-PB555 — no more hopping through individual product edit pages just to see which ones are entangled.

0.9.16

  • Clone-safe wads_dinero_id on products. WordPress's built-in "Copy to a new draft" duplicates all postmeta, so a cloned product carried the parent's Dinero GUID and any Sync now on the clone would PUT its data over the parent's Dinero record. Fixed by encoding the Woo product ID into the stored value:
    • New format: wads_dinero_id = "{woo_product_id}:{dinero_product_guid}" (e.g. "12345:6ac21fc9-57f1-4091-a3cb-58886a298052").
    • Sync\Products::pack_meta() writes the new format on POST / PUT.
    • Sync\Products::decode_meta() reads any stored value and reports: the raw GUID, the Woo ID it was originally bound to, whether the record is legacy (pre-0.9.16 bare-GUID format) and whether it is orphan (the stored Woo ID does not match the current product — clone marker).
    • push_product() refuses to sync an orphan record (returns wads_orphan_guid WP_Error) so the clone can never overwrite the parent's Dinero record. Legacy bare-GUID records still work; they are automatically rewritten in the new format on next sync.
  • Product metabox: new "Cloned from another product" panel shown when an orphan meta is detected. Verify still works (reveals whose Dinero record the GUID belongs to); Detach GUID clears only the local meta and unblocks re-sync. Sync now is disabled in this state.
  • Shared-GUID scan in the metabox updated to also find matches stored in the new {id}:{guid} format — SQL uses an exact match for legacy values PLUS a LIKE %:{guid} predicate.

0.9.15

  • Product metabox: detect + guard against shared/mismatched Dinero GUIDs. In-the-wild finding: two different Woo products (583-P20B29 and 583-PB555) both carried the same wads_dinero_id (postmeta), pointing at the same Dinero record. Clicking Sync now on the wrong Woo product would PUT its data over the Dinero record belonging to the other product. New defenses:
    • Shared-GUID warning: on render the metabox scans wp_postmeta for other published products / variations that carry the same wads_dinero_id and shows a warning listing them (with edit links) so the operator can see exactly which products are entangled.
    • SKU/ProductNumber mismatch on Verify: when GET /products/{guid} returns a ProductNumber that does not match the local SKU (or WC-{id} fallback), the green "Synced" badge is replaced with a red GUID mismatch badge and the log flags the specific mismatch — the stored GUID is preserved (no auto-clear) so the operator can decide whether to detach or overwrite.
    • Detach GUID button: safe recovery path — clears the local wads_dinero_id postmeta only. Does NOT touch Dinero. After detach, Sync now will POST and create a fresh Dinero record with its own GUID. Confirms first.
  • AJAX endpoint added: wads_product_meta_detach (nonce-guarded, manage_woocommerce).

0.9.14

  • Product metabox: real sync status + last-sync log. The green "Synced" badge was optimistic — it only reflected whether the WooCommerce product carried a wads_dinero_id postmeta, not whether that GUID actually resolved to a live Dinero product (e.g. after the product was deleted in Dinero). Added:
    • A Verify in Dinero button (visible when a GUID is stored) that performs GET /v1/{org}/products/{guid}. On 200 the badge stays green and the last-sync log records the live productNumber and name returned by Dinero. On 404 the stored GUID is cleared, the panel flips to "not linked" and the log records the miss — the next Sync now click will re-create the product with a new GUID.
    • A Last sync log block at the bottom of the metabox showing timestamp (local time), action (POST/PUT/GET), HTTP status, and result message from every sync or verify. Persisted in postmeta _wads_dinero_last_sync so it survives page reloads.
    • Sync now now writes the log too, capturing both the successful ProductGuid response and any WP_Error message (duplicate SKU, OAuth failure, HTTP error, etc.).
  • AJAX endpoint added: wads_product_meta_verify (nonce-guarded, manage_woocommerce).

0.9.13

  • New: Dinero product metabox on the WooCommerce product edit screen (side, default). Shows the current Dinero sync status of the product — not linked yet / linked (Dinero ProductGuid + SKU used as productNumber) / duplicate-SKU conflict — and offers a Sync now button that runs Products::push_product() on that single product without leaving the page. First sync POSTs and stores the ProductGuid; subsequent syncs PUT and update name / price / description. AJAX endpoint wads_product_meta_sync, nonce-guarded, capability manage_woocommerce. Product variations are still handled by the batch product sync (variations aren't editable via a metabox screen). Registered in Plugin::boot() under is_admin() alongside the existing order metabox.

0.9.12

  • OAuth invalid_grant UX: when Visma Connect rejects the stored refresh_token as invalid_grant (revoked / rotated / expired after 60 days of inactivity) the error surfaced in the Woo order metabox "View invoice PDF" modal as the raw technical text "Visma-tokenanmodning mislykkedes (HTTP 400): invalid_grant". Replaced with an actionable, translatable message that explains the connection has expired and points the user to Settings → General → "Connect to Dinero" (with the direct admin URL inlined). Same friendlier message now shows anywhere a token refresh fails site-wide (bulk actions, cron sync, single-order actions). Also clears the dead tokens on invalid_grant so the Settings page correctly reflects "not connected" and the Connect button becomes prominent.

0.9.11

  • Send to customer now attaches the Dinero-generated PDF (the exact same one shown in the modal preview), replacing any WooCommerce PDF Invoices & Packing Slips attachment for this specific send. Downloads the PDF from Dinero, saves it to uploads/wads-tmp/Invoice_{n}_order_{o}.pdf, adds a per-request woocommerce_email_attachments filter that swaps the attachment list to just the Dinero PDF, triggers WC_Email_Customer_Invoice, then removes the filter and cleans up the temp file. Order note documents the send with the Dinero invoice number.

0.9.10

  • Moved the "Send to customer" button into the PDF modal header (next to Close), instead of sitting on the metabox. Sending is now a natural next step after previewing the PDF.

0.9.9

  • Order metabox: new "Send to customer" button next to View invoice PDF — fires WooCommerce's standard "Customer invoice / Order details" email (WC_Email_Customer_Invoice). When WooCommerce PDF Invoices & Packing Slips is active the email carries the PDF with the Dinero invoice number (_wcpdf_invoice_number). Confirms first, then adds an order note documenting the send and the recipient address.
  • PDF filename: switched from dinero-invoice-{number}.pdf to the more descriptive Invoice_{number}_order_{order_number}.pdf (i18n translatable via _x('Invoice_%1$s_order_%2$s', 'PDF filename pattern', 'webalive-dinero-sync')). Uses $order->get_order_number() so Woo's display prefix (e.g. SF12345) is honoured.
  • New AJAX endpoint wads_order_meta_send.

0.9.8

  • PDF modal: switched from <iframe> to <embed type="application/pdf"> for the inline preview. Iframe + blob URL was still triggering external download in some browsers (Firefox in particular). <embed> invokes the browser's built-in PDF plugin more reliably. Added a small "Download PDF if it does not appear above" fallback link at the bottom of the modal body for edge cases where the browser refuses to inline the PDF (corporate policies, disabled PDF viewer).

0.9.7

  • PDF modal renders inline again. Dinero returns the invoice PDF with Content-Type application/octet-stream, and browsers treat that as "unknown binary → download" instead of previewing in the iframe. Fixed on the client by re-wrapping the response bytes as new Blob([blob], {type: 'application/pdf'}) before generating the URL.createObjectURL() — the iframe now hands off to the browser's built-in PDF viewer instead of triggering an external download.

0.9.6

  • View invoice PDF now works. Dinero's GET /v1/{org}/invoices/{guid} documentation states the Accept header must be EITHER application/json OR application/octet-stream. The metabox's PDF handler was sending application/pdf, application/octet-stream, application/json which Dinero interpreted as JSON preferred — body was JSON, not PDF, so the magic-bytes check correctly rejected it. Fixed by threading a per-call $accept override through Dinero\Client::get_raw() and forcing application/octet-stream for the PDF endpoint.

0.9.5

  • Order metabox: no more full page reloads. Refresh → deleted and Create new invoice → ok both swap the button set in-place via DOM manipulation. ajax_recreate now returns the fresh pdf_url so the swap can wire the new "View invoice PDF" button without a page load.
  • View invoice PDF fix: the PDF endpoint no longer trusts Dinero's Content-Type header (which can be text/plain or a bare value depending on the account). It now checks the response body's magic bytes (%PDF-) directly. Fixes the false "Dinero returnerede ikke en PDF…" message that was shown even on healthy booked invoices.

0.9.4

  • Order metabox: dropped the credit-note step from the recreate flow — the old "Credit & recreate" was overkill because when Dinero returns DeletedAt, the invoice is already gone from the ledger (nothing to credit). The button is now:
    • Renamed to "Create new invoice" with the intent-matching description "the invoice was removed in Dinero. Create a new one…".
    • Only visible when the order's _wads_dinero_invoice_deleted_at meta is set (populated by a prior Refresh from Dinero that detected DeletedAt).
    • Behaviour: archives old guid/number into wads_dinero_invoice_history, clears the linked-invoice meta + deleted flag + draft-mismatch state, then calls push_order() which creates and books a fresh invoice and writes the new number to wads_dinero_invoice_number + _wcpdf_invoice_number.
    • Refuses to run unless the deleted flag is present (guards against nuking a healthy link).
  • Refresh from Dinero: hidden once the deleted flag is set (it just told us the invoice is gone — nothing more to refresh). When it detects a fresh DeletedAt, it now persists the flag on the order and reloads the page so the button set switches to "Create new invoice".
  • View invoice PDF: also hidden when the deleted flag is set.

0.9.3

  • PDF modal: proper wp_die HTML parsing. Previously the entire error page — including the embedded <style> CSS — was rendered as plain text next to the actual message. Now the JS runs the response through DOMParser, strips <style>/<script>, and extracts the message from .wp-die-message / #error-page p (or falls back to body.textContent).

0.9.2

  • Order metabox: "View invoice PDF" now opens in a modal overlay (iframe, 90vh) instead of a new tab. Fetches the PDF via fetch() and previews as blob url; if the endpoint returns a non-PDF (draft / deleted / error) the modal shows the readable server message instead of a broken PDF viewer.
  • Refresh from Dinero: now inspects DeletedAt on the invoice response. Dinero keeps deleted booked invoices around (Danish law), and their Status still reads "Booked" — which was misleading ("Invoice #46567 (Booked)" even though the invoice was deleted). New behaviour: when DeletedAt is present, report "Invoice #N is DELETED in Dinero (removed …). Use 'Credit & recreate' to make a new one." and don't overwrite the order's _wcpdf_invoice_number with the dead number.
  • PDF endpoint: pre-checks the invoice's JSON before streaming the PDF and returns a specific message for the deleted case (410 Gone) and the draft case (409 Conflict).

0.9.1

  • Order metabox cleanup: removed the GUID + Contact GUID lines from the UI — they were internal identifiers with no value to the user.
  • Recreate button renamed to "Credit & recreate" and now routes through SettingsPage::run_audit_credit_recreate() (made public). Correct behaviour under Danish law: booked invoices cannot simply be deleted — they must be credit-noted. The flow now: 1) deletes any registered payments on the old invoice, 2) generates + books a credit note against it (or re-adopts an existing one), 3) creates a fresh draft invoice for the order, 4) books it, 5) updates wads_dinero_invoice_guid + wads_dinero_invoice_number + _wcpdf_invoice_number on the order. Old guid/number archived to wads_dinero_invoice_history meta.

0.9.0

  • Fix (order metabox): the inline <script> was sitting OUTSIDE .wads-order-meta, so document.currentScript.parentElement pointed at the metabox wrapper instead of our div — data-order-id was null and every AJAX call returned "Invalid order id". Moved the script INSIDE the div.
  • "View in Dinero" replaced with View invoice PDF: the button now hits admin-post.php?action=wads_om_pdf which fetches the invoice PDF from Dinero (GET /v1/{org}/invoices/{guid} with Accept: application/octet-stream) and streams it back inline. Same PDF that Dinero sends to the customer, no more useless login-required app URL. Draft invoices surface a plain message (PDFs are only available for booked invoices per Dinero).

0.8.99

  • New "Dinero" metabox on the WooCommerce order edit screen (legacy CPT post.php?post=X AND HPOS admin.php?page=wc-orders&action=edit&id=X). Actions:
    • Create Dinero invoice — when the order has no linked invoice, pushes it to Dinero via the existing push_order() (idempotent, respects total-mismatch guard).
    • Refresh from Dinero — GETs the invoice; updates wads_dinero_invoice_number + _wcpdf_invoice_number with the latest number, or flags the invoice as "deleted in Dinero" when the GET returns 404.
    • Recreate (invoice deleted) — archives the old guid/number to wads_dinero_invoice_history meta, drops the current link, and re-pushes so Dinero assigns a fresh invoice number that is then saved back on the Woo order.
  • AJAX endpoints: wads_order_meta_create, wads_order_meta_refresh, wads_order_meta_recreate (all require manage_woocommerce + a per-request nonce).

0.8.98

  • Fix: the "Refunded via " line on the Dinero credit note comment is now only added when the refund's _wwop_payment_refunded meta is "1" (i.e. the calling side actually pushed the money back to the customer via the gateway). Previously the line was printed unconditionally from the order's stored payment label, so a WWOP return without "Refund payment via QuickPay" ticked produced a false "Refunded via Visa ****" claim on the credit note even though the customer never got the money back through the card. WWOP 1.0.7 stamps this meta on the refund before wc_create_refund fires the WADS hook.

0.8.97

  • Fix (regression from 0.8.86): the credit note date field was being overwritten by the parent invoice's Date, so the credit note carried the original sale date instead of the refund date. Removed the inv_date inheritance; date now stays at the WooCommerce refund's creation date (WC_Order_Refund::get_date_created()), which is what the credit note should reflect — the day the money is returned to the customer.

0.8.96

  • Fix (based on Dinero swagger SalesCreditNoteReadModel): Credits::recover_existing_credit_note() now uses the DeletedAt timestamp field as the primary "is this note gone?" signal instead of guessing status names. Per Dinero's own docs: Status on a credit note only ever holds Draft or Booked (not Cancelled / Paid / OverPaid — those are on the separate PaymentStatus field on the collection endpoint), and when a shop deletes a booked credit note in the Dinero UI, Status stays Booked (Danish bookkeeping law keeps the credit note number for the audit trail) but DeletedAt is populated with the deletion timestamp. New order of checks: 1) DeletedAt != null → treat as gone, recreate; 2) Status != Booked → not in force, recreate; 3) Status == Booked and DeletedAt == null → in force, return wads_credit_note_exists WP_Error with instructions to delete/void in Dinero UI first. Full Status + DeletedAt is logged to debug.log on every recovery attempt.

0.8.95

  • Fix: Credits::recover_existing_credit_note() in-force status whitelist is now aligned with what the existing WADS admin code actually verifies (SettingsPage::run_audit_recreate() around line 5383): only Booked, Paid and OverPaid are treated as "in force, refuse duplicate". The previous list (Sent, PartiallyPaid, Reminded, DebtorCollection, Interest, ...) was guessed and could have wrongly blocked recreation.
  • Any status outside that whitelist — including Draft, Cancelled, Canceled, empty, or any unknown/future Dinero value — clears the refund meta and lets push_refund() create a fresh credit note. When an unknown status is encountered, the full Dinero response is logged (truncated) so we can extend the whitelist based on real data if needed.

0.8.94

  • Rewrite: Credits::recover_existing_credit_note() now uses a whitelist of "valid, in-force" credit-note statuses (Booked, Sent, Paid, PartiallyPaid, Overpaid, Reminded, DebtorCollection, Interest). Any OTHER Dinero status — Draft, Cancelled, "ikke godkendt", or any unknown/future value — is treated as "note is no longer in force": the refund's wads_dinero_credit_id + wads_dinero_credit_number meta is cleared and push_refund() creates a fresh credit note. Danish bookkeeping law forbids removing credit note numbers, so the old note stays in Dinero for the audit trail but no longer blocks a new one.
  • Removed the API-DELETE fallback for legacy drafts (Dinero doesn't reliably support it and the shop rejected this path). Orphan drafts must be handled in the Dinero UI.
  • When a valid credit note is still in force, push_refund() now returns a WP_Error with code wads_credit_note_exists instead of a silent skipped — the caller (e.g. WWOP repair) can surface a clear "Cancel the existing credit note in the Dinero UI first" message to shop staff instead of falsely reporting success.

0.8.93

  • Fix: Credits::recover_existing_credit_note() now recognises Status = Cancelled (Dinero's state after the shop "deletes" a booked credit note in the UI — Dinero can't truly delete booked accounting documents, it voids them with a reverse entry and marks the credit note Cancelled). Previously this was treated the same as Booked/Paid and returned skipped, so the WWOP repair flow reported "1 already existed" and did not recreate. Now Cancelled clears the refund's wads_dinero_credit_id / wads_dinero_credit_number meta and signals push_refund() to create a fresh credit note.

0.8.92

  • Fix: Credits::recover_existing_credit_note() now treats an HTTP 404 from GET sales/creditnotes/{guid} as "the credit note has been deleted in Dinero" — it clears the refund's wads_dinero_credit_id and wads_dinero_credit_number meta and returns null so push_refund() proceeds to create a fresh credit note. Previously the 404 was surfaced as a hard error, blocking the WWOP repair flow from replacing a credit note the shop had cancelled/deleted directly in Dinero.
  • Also clears wads_dinero_credit_number alongside wads_dinero_credit_id in the existing "draft could not be booked, delete and recreate" recovery path so a subsequent push doesn't carry over the stale credit-note number.

0.8.91

  • Fix (regression from 0.8.86): credit note total was ~20 %% too low. Credits::build_lines() was still sending $item->get_total() (excl. VAT) as baseAmountValue, but 0.8.86 started forwarding the invoice's showLinesInclVat = true onto the credit note. With that flag on, Dinero interprets baseAmountValue as INCL. VAT and back-calculates the net, so a 3.845 DKK refund produced a 3.076 DKK credit note. Now the line amount is always the gross value (get_total() + get_total_tax()) and showLinesInclVat is FORCED to true regardless of the parent invoice — Danish B2C rules require consumer invoices/credit notes to display prices incl. VAT.
  • New: credit note NUMBER is now stored on the refund's wads_dinero_credit_number meta (in addition to the GUID on wads_dinero_credit_id) so callers can build human-readable filenames / references without an extra Dinero GET.
  • Cleanup: removed the "Retur #" line from the credit note comment — the WooCommerce refund post id is an internal number that isn't useful to the customer. The comment now shows Order: <order-number> | Reason: <reason if any> | Refunded via <payment label>.

0.8.90

  • i18n: All strings introduced in the 0.8.83–0.8.89 credit-note work are now properly wrapped in __() with English source text and the webalive-dinero-sync text domain. This covers the Dinero comment field on credit notes ("Order: %s", "Reason: %s", "Return #%d", "Refunded via %s"), the WP_Error messages in Credits::push_refund() / recover_existing_credit_note() / fetch_credit_note_pdf*(), and the wads_push_refund_to_dinero() / wads_get_credit_note_pdf() global helpers. The translation pipeline generates the Danish output as usual — the comment on the credit note will therefore render in Danish on a Danish shop (matching the previous hardcoded text) but is now translatable for other locales.

0.8.89

  • Enhancement: the visible comment on a Dinero credit note now includes a "Refunderet via " line — reusing the QuickPay brand + last4 label (wads_payment_label order meta) that Sales wrote when the invoice was created. Example: Ordre: SF23301 | Retur #24327 | Refunderet via Visa **** 1234. Falls back to the gateway's plain payment method title when the QuickPay label isn't stored.

0.8.88

  • New: Client::get_raw() fetches an endpoint that returns non-JSON data (e.g. application/pdf) and returns the raw response body bytes instead of trying to JSON-decode. Used for the Dinero credit-note PDF endpoint.
  • New: Credits::fetch_credit_note_pdf( WC_Order_Refund ) and Credits::fetch_credit_note_pdf_by_guid( string ) return the raw PDF bytes for a booked Dinero credit note.
  • New: public helper function wads_get_credit_note_pdf( int|WC_Order_Refund ) for external plugins (e.g. WWOP) to grab the credit-note PDF and attach it to a customer email or write it to disk. Returns raw PDF bytes on success, or a WP_Error when the refund has no linked credit note.

0.8.87

  • Enhancement: Credit notes now carry the WooCommerce order number in both machine-readable and human-readable form. The externalReference field is now WC-<orderNumber>-REFUND-<refundId> (was WC-REFUND-<refundId>), and a new comment field is set to "Ordre: <orderNumber> | Ã…rsag: <refund reason if any> | Retur #<refundId>" which Dinero prints on the credit note so shop staff and the customer can see which Woo order it belongs to.

0.8.86

  • Fix: Credits::push_refund() / create_and_book_credit_note() now GET the parent invoice from Dinero and inherit its Currency, Language, Date, ContactGuid and ShowLinesInclVat fields into the credit note payload. Without matching currency / VAT display Dinero refuses to auto-apply the credit against the invoice balance — the credit note was booked but appeared disconnected from the original invoice on the customer account. When the invoice cannot be fetched (or the order has no wads_dinero_invoice_id meta) the credit note is still created but as a stand-alone note on the customer account, and the reason is logged to debug.log. Added wads/credits/push_refund_payload filter and invoiceGuid in the response so callers can verify the linkage.

0.8.85

  • Fix: Credits::push_refund() now recovers pre-existing draft credit notes on refunds that already have a wads_dinero_credit_id meta. Previously the function returned skipped immediately if the meta existed, which left old drafts — created before 0.8.84 attached contactGuid — stuck as un-bookable drafts in Dinero with no way to retry via the WWOP repair flow. The new flow: GET the credit note, if it's already booked return skipped; if it's a draft try to POST /book on it; if booking fails (typically because the draft has no contact), DELETE the draft, clear the refund meta and create a fresh credit note with contactGuid in the same call. The return value gained a recovered: true flag so callers can report "draft booked" vs "newly created" separately.

0.8.84

  • Fix: Credits::push_refund() now attaches contactGuid from the parent order's wads_dinero_id meta (the same ContactGuid the invoice was created against). Without a contact the credit note stayed as a draft in Dinero with no customer name — Dinero refuses to book credit notes that don't identify the debtor.
  • Fix: the book call's result is now checked. Previously a book failure was silently ignored so the credit note remained a draft with no indication back to the caller. The response now includes booked (bool) and, on failure, book_error (string) so WWOP's return flow can surface "created but NOT booked — " in the order note and admin UI.

0.8.83

  • Fix: Credits::push_refund() now posts to sales/creditnotes (and books via sales/creditnotes/{guid}/book) instead of the non-existent bare creditnotes endpoint. The old paths returned HTTP 405 (Method Not Allowed) from Dinero, so refunds silently failed to become credit notes when the woocommerce_order_refunded hook fired or when other plugins called the helper. rawurlencode() is now applied to the GUID in the book URL, and the book request uses Timestamp (capital T, matching the rest of the WADS codebase and Dinero's API convention).

0.8.82

  • New public helper function wads_push_refund_to_dinero( $refund ) that wraps Credits::push_refund() and can be called directly by other plugins (e.g. WWOP return/refund flow) instead of relying on the woocommerce_order_refunded hook. Accepts a WC_Order_Refund object or a refund ID and returns the Dinero API response array or a WP_Error. The credit note is automatically linked to the parent order's Dinero invoice via the existing wads_dinero_invoice_id order meta and booked immediately.

0.8.81

  • push_order() now ALWAYS writes the Dinero invoice number to _wcpdf_invoice_number meta on the Woo order (previously only when the meta already existed). The customer-facing PDF invoice number must always equal the Dinero number, even on orders that never had a PDF generated before. WWOP 0.0.11 calls the wads sync BEFORE update_status('completed', ...) so the email PDF carries the right number.

0.8.80

  • New public action hook wads_sync_order for triggering an immediate single-order sales sync (Woo → Dinero invoice) from other plugins. External code:

    if ( class_exists( '\\Webalive\\DineroSync\\Plugin' ) ) {
      do_action( 'wads_sync_order', (int) $order_id );
    }

    Use after the order is finalised (e.g. successful Quickpay payment in Woo Webalive Order Premium). Idempotent — already-synced orders are skipped via the existing _wads_dinero_invoice_id meta check. Non-fatal: any error is swallowed so the order completion flow isn't broken; the admin sales sync UI can still re-process the order later.

0.8.79

  • Sales sync worklist: unsynced_order_ids_in_range no longer relies on meta_query NOT EXISTS (unreliable across HPOS vs legacy CPT order storage). Instead it fetches the date-range order IDs and filters in PHP by $order->get_meta(wads_dinero_invoice_id) === ''. Fixes runs that processed 214/214 orders with all 214 "skipped" (already synced) — the worklist was including already-synced orders.

0.8.78

  • Picks booking: payment now ALWAYS settles the invoice 100%% in its own currency against Dinero (the invoice's native total), regardless of Wise's actual DKK outflow. The diff between the invoice's DKK and Wise's DKK is fully booked on the bank_fees account as kursdiff/fee — so Wise balances. Strategies per pick:
    • pick.ccy = DKK → Amount = pick.native (bilag's DKK total, e.g. Arpe 98.75 DKK)
    • pick.ccy = Wise's currency → Amount = pick.native × Wise rate
    • mixed/unknown → proportional split of Wise DKK (no leftover by construction)
  • Example: USD card payment for a 98,75 DKK Arpe invoice — invoice marked 100%% paid at 98,75 DKK, the difference between 98,75 DKK and Wise's 92,34+0,27 DKK is booked as kursdiff on bank_fees.

0.8.77

  • Status filter: new "Not posted" option (everything where posted_at IS NULL — ready + missing + error). Also added "Draft sent" so you can see only the kladde rows. Filter dropdown order: All, Not posted, Ready to book, Missing mapping, Error, Posted, Draft sent.

0.8.76

  • "Send to draft" button is now visible on ALL unposted rows (READY / ERROR / MISSING), not just MISSING. Use it on rows where contact is mapped but no bilag matches and you don't want to pick manually — the row goes to Dinero as a draft (kladde) on the transit account, and you finish it in Dinero.

0.8.75

  • Picks booking: when ALL selected bilag are in the same currency as the Wise row, the DKK distribution is now native-rate based (pick.native × Wise rate) instead of proportional. The leftover between Wise's actual DKK and the sum of allocated payment shares is automatically booked as a Wise fee/kursdiff on the bank_fees account — e.g. customer paid 1019.81 EUR for a 1014.81 EUR invoice → invoice marked 100%% paid + 5 EUR×rate booked on bank_fees as income ("Mango fee" case).
  • Mixed-currency picks still use proportional split (no leftover by construction).
  • The Wise FEE-row contribution and the new leftover are combined into a single adjustment voucher.

0.8.74

  • New "Send to draft" button on Wise rows where mapping/bilag-ref is missing (STATUS_MISSING). Creates a DRAFT manuel voucher (kladde) in Dinero with the Wise bank account on one side and the configured transit account (or bank-fees as fallback) as placeholder on the other side. Description includes counterparty + amount + Wise txid + "[DRAFT — needs offset]". The user finishes the draft manually in Dinero, then clicks "Undo draft" in Wise + Book normally.
  • New row status draft_sent (gray badge "Draft sent") so drafts are visually distinct from properly posted rows.
  • New AJAX endpoint wads_wise_send_to_draft + Poster::send_to_draft() method.
  • Undo dispatch handles drafts via the existing legacy manuel-voucher delete branch.

0.8.73

  • Bilag picker now fetches state-of-account LIVE from Dinero (cache bypassed) with hideClosed=true, and strictly filters to status='open'. Closed/settled bilag no longer appear in the picker list.
  • StateOfAccount::for_contact() gained two optional parameters: $only_open (uses hideClosed=true, separate cache key) and $force_refresh (bypasses transient cache).

0.8.72

  • Pick button is now also shown on already-posted rows when the contact is mapped. Lets you adjust the bilag selection after the fact — changes take effect on the next Book (click Undo first to re-book against the new picks).

0.8.71

  • Bilag picker modal: live running total of picked bilag, grouped by currency, with diff vs the Wise transfer amount in the transfer's currency (±0.005 green, ±1 yellow, >1 red). Helps you see at a glance whether your selection matches the transfer (e.g. transfer 366.98 GBP → pick #2272 366.98 GBP → diff 0.00 GBP). Per-currency sums shown when picks span multiple currencies; a red warning appears if none of the picked bilag is in the transfer currency.

0.8.70

  • Parser now recognises Wise "Topped up…" descriptions and assigns counterparty = "Topped up account" instead of leaving it empty. Map this counterparty to your Dinero contact (e.g. Hydro-tech) once in the counterparty editor and all current + future top-up rows match against that contact's state-of-account automatically (±10 day window).
  • Schema v6: backfills counterparty = "Topped up account" for legacy rows where counterparty is empty AND description starts with "Topped up". Migration runs once on next Wise page load.

0.8.69

  • try_register_payment now returns a descriptive WP_Error (instead of silently returning null) when its preconditions are not met: missing Wise balance account for the row's currency ("No Wise balance account configured for EUR…") or missing FX rate. Previously these failures fell through to the manual-voucher path which only knows how to print "No offset account configured for this row" — hiding the real cause.

0.8.68

  • Auto-match payment registration is no longer restricted to kind=CARD — it now fires for ANY non-FEE row (TRANSFER, CONVERT, etc.) when the contact is mapped and a bilag matches in state-of-account. Fixes "No offset account configured for this row" when booking an incoming TRANSFER (e.g. Gaia Ganesh → invoice #2264) where the bilag column showed a match but the booking fell through to the manual-voucher path.

0.8.67

  • Fixed two literal \u escape sequences that didn't decode in PHP single quotes — they were rendered as raw text in the UI: the bilag column tooltip ("Ingen automatisk match \u2014 v\u00e6lg bilag manuelt." → "No automatic match — pick a bilag manually.") and the Sales (non-Woo) account description in Settings ("\u2014" → "—").

0.8.66

  • CRITICAL FIX: "Bilag {guid} fetch returned no Timestamp." error when booking against picked SALES invoices — Dinero's InvoiceReadModel uses TimeStamp (capital S), while PurchaseVoucherReadModelV2 uses Timestamp. The picker booking + the legacy try_register_payment now read both spellings.
  • i18n cleanup: remaining Danish source strings from recent additions are now English ("Manually picked bilag", "No automatic match — pick a bilag manually.", "missing", "Undo", "Undo voucher in Dinero? …", "Reset ONLY the local Wise row …", "Transit reconciliation", "Refresh for bilag details"). Existing labels like "Bogfør saldo til bankgebyrer" and "Renteindtægter" stay as-is until you ask for them.

0.8.65

  • Error rows now show the full post_error text inline under the red "Error" badge (previously only on hover) so booking failures are immediately visible.

0.8.64

  • All user-facing strings in the new bilag picker (modal headers/buttons/messages, table headers, status labels, error notices) and the adjustment-voucher descriptions are now English source strings inside __() / esc_html__() — keeps the plugin's i18n convention (English = source, Danish via .po/.mo). Affected: "Select bilag", "Cancel", "Save selection", "Pick", "Pick bilag manually", "Manually picked bilag", "missing", "No automatic match — pick a bilag manually.", "Loading open bilag…", "Open bilag for this contact — check the ones covered by this payment:", "No open bilag found for this contact.", "Date", "Type", "Amount", "Description", "Error", "Network error", "Wise fee · %s", "Wise fee + FX rounding · Bilag %1$s · %2$s".

0.8.63

  • Manual bilag-vælger (overrider auto-match): hver række med kontakt-mapping har nu en "Vælg"-knap i bilag-kolonnen. Klik Ã¥bner modal med ALLE Ã¥bne bilag for kontakten (uanset valuta og dato) — brugeren afkrydser 1-N bilag der dækkes af betalingen.
  • Bogføring mod picked bilag: hver pick fÃ¥r én payment registrering med AmountInForeignCurrency = bilagets fulde native total (= 100%% udligning i bilagets egen valuta). Wise DKK fordeles forholdsvist mellem picks efter native total. Dinero opretter selv kursdiff-posten for forskellen mellem Wise's DKK og Dineros DKK pÃ¥ fakturaen ("resten pÃ¥ kursdiff" automatisk).
  • Wise FEE-rækker bogføres separat som finansbilag pÃ¥ bank_fees-kontoen.
  • Schema v5: ny voucher_picks TEXT-kolonne med JSON-liste af pick-objekter (guid, number, ccy, native_total, category).
  • Annullér: ved undo af picks-rækken slettes hver enkelt payment + adj-bilag, og voucher_picks ryddes sÃ¥ rækken kan vælges pÃ¥ ny.
  • CARD auto-match flow stadig som default nÃ¥r ingen picks er gemt.

0.8.62

  • State-of-account match now uses the MERCHANT's currency/amount (orig_currency/orig_amount) when present, not the Wise balance currency. Example: Arpe & Company invoices in DKK 98,75 but Wise debits the EUR balance with 13,23 EUR — we now match against the DKK 98,75 bilag in Dinero instead of looking for a non-existent EUR 13,23 bilag. Falls back to row.currency/amount when no orig fields are recorded (Wise balance ccy == merchant ccy, e.g. Mailchimp USD).

0.8.61

  • State-of-account matching is now 100%% FX-free: dropped the DKK fallback in find_best_match. Wise's stored FX rate is used ONLY at booking time (DKK ledger amount on the manual voucher). Matching = same contact + same currency + same native amount (±0.05) + within ±10 days. If currencies don't match, there's simply no match.

0.8.60

  • State-of-account match no longer trusts the entry's Currency field — it was unreliable. Instead we GET the underlying bilag for every candidate inside the date window, read the bilag's REAL currency (Currency / CurrencyKey) and native total (TotalInclVat / sum of Lines[].AmountInclVatValue), and match natively (0.05 unit tolerance). Wise USD 71 → Bilag USD 71 now matches deterministically regardless of Wise/Dinero FX-rate differences. DKK fallback still works when no native match is found.

0.8.59

  • State-of-account cache prefix bumped to wads_soa_v2_ so the new entry shape (with currency field added in 0.8.58) takes effect immediately. Old cached entries without the currency field defaulted to DKK and silently bypassed the USD-vs-USD native match — hence the still-failing Mailchimp “mangler” after 0.8.58.

0.8.58

  • State-of-account match is now currency-aware: when the bilag's Currency equals the Wise transaction's native currency (e.g. both USD), the match compares native-to-native by GETting the bilag and reading its real native total (sum of Lines[].AmountInclVatValue for purchase, TotalInclVat for sales). Tolerance: 0.05 native units (~5 cents). DKK-vs-Wise's DKK is no longer used in that case — the FX-rate mismatch between Wise and Dinero (Wise 6.80 vs Dineros day rate) no longer hides matching bilag. DKK bilag continue to use the 5 DKK / 5%% DKK tolerance.
  • state-of-account entries now also expose Currency so the UI/match can read it.

0.8.57

  • State-of-account match: reverted date window back to ±10 days (per user preference). Amount tolerance stays at max(5 DKK / 5%) from 0.8.56.

0.8.56

  • State-of-account match: amount tolerance widened from max(1 DKK / 2%) to max(5 DKK / 5%), and date window from ±10 to ±20 days. Catches foreign-currency invoices booked in Dinero at a different FX rate than Wise's (e.g. Mailchimp $71 booked at Dinero ≈ 497 DKK vs Wise 482.80 DKK). Both for_contact() fetch range and find_match() default tolerance are updated.

0.8.55

  • Wise counterparty editor: switching from "konto" to "kontakt" (or vice versa) now actually clears the opposite field's value before saving — previously the old value remained in the input (just dimmed) and got persisted on the next save. Picking a Dinero contact wipes any existing account number, and typing/picking an account wipes any existing contact pick.

0.8.54

  • Wise CARD posting: sibling lookup no longer requires a different currency. Same transaction_id means same Wise event — e.g. a real charge + refund pair (both USD, opposite signs) now bookes as ONE finansbilag with two lines (one charge line, one refund line) instead of two separate bilag where only the clicked row was posted. Audit trail is preserved (each leg is its own line on the voucher).

0.8.53

  • Wise tab: the "Book" button now posts via AJAX — no page reload. On success the affected row(s) are updated in place (status badge → Posted, button cell → “✓ #voucher_no”), and a dismissible notice is shown at the top. On error a red notice with the Dinero message is shown and the button re-enables so you can retry. The old non-JS form submit is kept as a fallback.

0.8.52

  • Removed invalid top-level Description from the adjustment voucher payload — ManuelVoucherCreateModel only accepts VoucherDate, Lines, FileGuid, ExternalReference (additionalProperties: false). The line-level Description still carries the readable text "Wise gebyr + kursdiff. · Bilag X · Leverandør".

0.8.51

  • Adjustment voucher description now refers to leverandør + bilagsnummer instead of the internal Wise CARD-ID. Example: "Wise gebyr + kursdiff. · Bilag 1234 · Tubs Parts".

0.8.50

  • Foreign-currency bilag: payment POST now includes AmountInForeignCurrency = the bilag's native total (sum of Lines[].AmountInclVatValue for purchase, TotalInclVat for sales). DKK Amount is taken from the state-of-account entry (always DKK). This settles the invoice 100 %% in its own currency with no Overbetalt/Restbeløb.
  • DKK bilag: AmountInForeignCurrency is omitted (Dinero rejects zero/missing pairing).
  • The remaining Wise-vs-bilag difference (fee + FX rounding) is still posted as a separate adjustment voucher on the bank-fees account.

0.8.49

  • Payment Amount + AmountInForeignCurrency now read from the bilag itself, per Dinero's actual schema:
    • Sales invoice (InvoiceReadModel) → TotalInclVat (DKK) and Currency for foreign.
    • Purchase voucher (PurchaseVoucherReadModelV2) → VoucherTotals[Type=Total].Total (DKK) and CurrencyKey for foreign.
  • For foreign-currency bilag we now POST the exact foreign amount the bilag was issued in as AmountInForeignCurrency (not Wise's actual amount scaled). Settles the invoice 100 % with no Overbetalt/Restbeløb.
  • The Wise vs invoice difference (FX/fee) still books to bank_fees as an adjustment voucher.

0.8.48

  • CRITICAL FIX: payment Amount is now the invoice total (from the state-of-account entry's Amount field, already in DKK), not Wise's cash-out. Previously we tried TotalInclVat / TotalAmountInclVat on the GET response — those keys don't exist on PurchaseVoucherReadModelV2 (only on sales invoices). The code fell through to Wise's amount, so invoices were short-paid by the FX rounding difference (e.g. paid 118.61 on a 118.75 invoice = 0.14 DKK restbeløb).
  • Adjustment voucher math now reconciles correctly: Wise cash-out + FEE = invoice total + adjustment.

0.8.47

  • Wise schema v4: explicit fallback ALTER TABLE ADD voucher_meta when dbDelta missed the column on upgrade. This is why Force reset returned 0 rows — the UPDATE referenced a non-existing column and silently failed. Next page load auto-fixes the schema.

0.8.46

  • Force reset: rewritten to use $wpdb->update() (safer than raw query) + verifies the row is actually reset by re-reading it from the DB after the UPDATE, and reports the verified state in the notice. Previous version reported the value $wpdb->query returned, which could be 0 even on a successful UPDATE depending on MySQL CLIENT_FOUND_ROWS setting.
  • Notice now reads: "Reset N row(s); clicked row is now Ready/Posted/..." so you can see at a glance whether it actually worked.

0.8.45

  • Force reset: reset the clicked row by ID as well as any sibling sharing its voucher_id. Previously when MySQL reported "0 rows changed" on the voucher_id UPDATE (legacy data race), the local row stayed locked in Posted state and the Book button didn't reappear.

0.8.44

  • Wise table: "Force reset" now renders as a proper button (was a text link). Sits below "Annullér" with the same small-button styling for visual consistency.

0.8.43

  • Wise table: the "Force reset" button promised in v0.8.42 is now actually rendered (was wired up server-side only). Shows next to "Annullér" on every posted primary row.
  • Wise notices: removed # from error/success messages because some browsers interpret it as a URL fragment on the redirect, causing the notice to be truncated at the first #. Now reads "row 1" / "voucher 2329" instead of "row #1" / "voucher #2329".

0.8.42

  • Wise: new "Force reset" action on posted rows. Skips the Dinero DELETE call entirely and only resets the local Wise row(s) back to Ready. Use this when you've manually cleaned up the entry in Dinero (e.g. clicked "Slet betaling" on the bilag) and just need the plugin's local state to match.
  • The button shows next to "Annullér" on every posted primary row, with a confirm dialog clarifying that Dinero is NOT touched.

0.8.41

  • Wise: "Annullér" now works for rows booked via invoice/purchase-voucher payment endpoint (not just manuel vouchers). Database adds a voucher_meta JSON column (schema v4) storing the payment kind + payment guid + adjustment voucher guid so undo can call the right Dinero endpoints:
    • Sales payment → DELETE /v1/{org}/invoices/{guid}/payments/{paymentGuid}
    • Purchase payment → DELETE /v1/{org}/purchase-vouchers/{id}/payments/{paymentId}/{timestamp}
    • Plus DELETE on the adjustment manuel voucher when present.
  • Rows booked before v0.8.41 don't have meta — their Annullér returns a clear error telling the user to manually delete the payment in Dinero (via the bilag's "Slet betaling" button) and then re-book.

0.8.40

  • Wise payment against matched bilag: now pays the exact bilag total (read from the GET on the invoice/voucher) so Dinero marks it as fully paid instead of "Overbetalt". Previously we summed all rows in the group (including the Wise FEE row), which caused 0.08 DKK overpayment on a 118.75 DKK invoice.
  • The Wise FEE row plus any FX-rounding difference between Wise's actual cash-out and the bilag total are now posted as a separate adjustment manuel voucher to the bank_fees_account_number so the Wise balance still reconciles 1:1 with the Wise app.
  • All Wise rows (payment + adjustment + FEE) are marked posted under the same voucher_id so Annullér still works.

0.8.39

  • Fix: Bilag column matched the SINGLE row's DKK instead of the row group's total. A split-currency CARD payment (e.g. 45.67 + 72.94 = 118.61 DKK ≈ 118.75 invoice) now correctly matches the bilag instead of falsely showing "âš  mangler" on every leg.
  • Bilag is now only rendered on the primary row of a voucher group — other rows in the group leave the cell empty (mirrors the "Book" button behaviour).

0.8.38

  • Wise booking: when a row (or row group) is mapped to a Dinero contact AND a matching purchase/sales bilag is found in the contact's state-of-account, we now register the payment via Dinero's payment endpoint instead of writing a manuel voucher:
    • Sales invoice → POST /v1/{org}/invoices/{guid}/payments
    • Purchase voucher → POST /v1/{org}/purchase-vouchers/{id}/payments
    • Body: RemainderIsFee = true so any difference (Wise FX/fee) auto-books to the bilag's fee account — invoice/voucher settles 100 %.
    • Amount = sum of Wise DKK across the row group; AmountInForeignCurrency = sum of native amount when single-currency.
    • Timestamp is read by GET-ing the bilag first.
  • Row(s) are marked posted with the bilag's voucher number as voucher_no and the payment guid as voucher_id.
  • Falls back to the existing manuel voucher path when no matching bilag exists (the "âš  mangler" rows from v0.8.37).

0.8.37

  • Wise Bilag column: when the row is mapped to a contact but no matching purchase/sales bilag is found in Dinero, the cell now shows a red "âš  mangler" marker instead of a neutral em-dash. Helps surface payments that need a bilag uploaded/registered in Dinero before they can be settled.

0.8.36

  • Wise table: new "Bilag" column for rows mapped to a Dinero contact. We query GET /v1/{org}/state-of-account/{guid} for each unique contact in the visible rows and match Wise transactions to existing purchase/sales vouchers by date (±10 days) and DKK amount (±1 DKK tolerance, or 2 % — whichever is larger — to absorb FX rounding). The matched voucher number is shown with a tooltip (category/class/status/date). Empty when no match.
  • New helpers: Dinero\StateOfAccount::for_contact(guid, from, to) with 15 min transient cache per contact.

0.8.35

  • Fix: "Annullér"/undo failed because Dinero's DELETE /vouchers/manuel/{guid} requires a Timestamp body (same concurrency token pattern as /book). Poster::undo_row() now does a GET /vouchers/manuel/{guid} first to read the current Timestamp, then sends it with the DELETE.
  • Dinero\Client::delete() extended to accept an optional body array.

0.8.34

  • Wise table: posted rows now have a small "Annullér" button (the primary row of each voucher group). Clicking it calls DELETE /v1/{org}/vouchers/manuel/{guid} which, per Danish law, adds an offset entry rather than removing the original. The Wise rows are reset to Ready so you can re-book them (e.g. after fixing VAT codes on the offset account).
  • All rows tied to the same voucher_id are reset together — picking either leg of a CONVERT pair or any leg of a CARD split-payment undoes the whole voucher.

0.8.33

  • Wise voucher lines: the offset (expense / fee / contact) account no longer forces AccountVatCode = 'none'. It is now omitted from the payload so Dinero uses the account's configured default VAT code (typically I25 for 2000 Vareforbrug, etc.) and the booking is split correctly into ex-VAT + incoming-VAT amounts. The Wise balance (AccountNumber) side stays 'none' because bank accounts have no VAT.

0.8.32

  • Wise table: actually remove the "↑ part of voucher" text from non-primary rows (v0.8.30 only added the CSS but didn't strip the text). Also apply the wads-row-grouped class to every row in a multi-row voucher so the coloured left border actually shows.

0.8.31

  • Fix: Wise voucher booking and transit-reconciliation booking now include the required Timestamp field in the body of POST /vouchers/manuel/{guid}/book. Previously this returned HTTP 400 Validation Error: The Timestamp field is required. and left the voucher as a draft in Dinero.

0.8.30

  • Wise table: replaced the "↑ part of voucher" text on non-primary rows with a subtle indigo left-border on every row that is grouped into a multi-row voucher (primary + non-primary). Non-primary action cell is now empty.

0.8.29

  • Wise table: only one "Bogfør" button per logical voucher. Rows that will be booked together (split-currency CARD pair + their FEE row, or both legs of a CONVERT) now show the button only on the primary row — the others show a small "… del af samme voucher" hint pointing at the primary's Tx ID.

0.8.28

  • Wise booking: a FEE row whose transaction_id starts with FEE- followed by another row's txid (e.g. FEE-CARD-3276119364 for CARD-3276119364) is now included in the same voucher as that CARD payment. Result: a single 3-line voucher for a card payment funded from two Wise balances plus its Wise fee, instead of three separate vouchers. All three rows are marked posted with the same voucher id.

0.8.27

  • Wise tab: removed the duplicate render_notice() call — the global render_query_notice() (in SettingsPage) already shows ?wads_msg= notices for every tab, so success messages like "Valutakurser gemt." were appearing twice.
  • Wise redirect_with_msg(): removed double URL-encoding (rawurlencode + add_query_arg) so messages render cleanly without %20/%25 artifacts.

0.8.26

  • Dinero chart-of-accounts picker: new inline "Reload from Dinero" button in the popover (same pattern as the contact picker). Shown in both the empty-state and the no-match state. Useful when you've just created a new account in Dinero — click reload and the autocomplete matches immediately without leaving the page.
  • New AJAX endpoint: wads_accounts_reload (force-refresh chart of accounts + return updated list).

0.8.25

  • Wise: support any currency (not just DKK/EUR/GBP/USD). Wise balance-account editor and FX-rate editor now show one row per currency detected in imported statements, plus the four standard ones. Useful when a CONVERT row involves CHF, NOK, SEK, etc.
  • Wise\FxRates: relaxed SUPPORTED check so rates can be saved for any 3-letter currency code.
  • Settings::update: accepts dynamic wise_account_<ccy> keys (any 3-letter lowercase suffix) in addition to the defaults.

0.8.24

  • Wise main table status: row is now "Ready" when the counterparty mapping has either a Dinero account OR a Dinero contact (not both required). Matches the counterparty-editor's amber-highlight rule. Mismapped reason updated.

0.8.23

  • Wise counterparty editor: "incomplete" amber highlight now only triggers when both account AND contact are empty. A row with either field filled is considered complete (account → manuel voucher, contact → purchase voucher).

0.8.22

  • Wise counterparty editor: account and contact fields are now mutually exclusive in the UI. When account is set, the contact input is dimmed and disabled (and vice versa). Clearing the active field re-enables both — the cleared value is removed via the existing AJAX save endpoint.

0.8.21

  • Wise counterparty editor: "incomplete" highlight now only triggers when the Dinero account is missing. Contact is treated as optional (a row can be booked without one) so rows with account-but-no-contact are no longer flagged as incomplete.

0.8.20

  • Wise counterparty editor: rows that are missing either the Dinero account or the Dinero contact are now highlighted with an amber background (tr.wads-cp-incomplete). Rows where both are set keep the normal striped look. The AJAX save endpoint returns a complete flag so the highlight clears (or re-applies) immediately after each pick — no page reload needed.

0.8.19

  • New setting: transit_account_number (Settings → General → Finance accounts → "Mellemregning bank/Wise"). Identifies the Dinero account used as bank/Wise transit (typically 55990).
  • Wise tab: new "Transit afstemning" section. Pulls all entries on the transit account from Dinero (default last 90 days), shows the running balance, and lists each entry with date/voucher/amount. The balance reflects what's left after both Wise-side and bank-side bookings.
  • Wise tab: "Bogfør saldo til bankgebyrer" button. Builds one manuel voucher that moves the current transit residual to the bank-fees account so 55990 zeroes out. Requires both transit_account_number and bank_fees_account_number to be configured.

0.8.18

  • Wise CARD split-payment pairing: when Wise funds a single card payment from two different balances (e.g. "20.00 USD" debited as 1.30 USD + 119.07 DKK with the same CARD-NNNNN txid), we now post one voucher with two lines, both debiting the counterparty account but each crediting its respective Wise balance. Both rows are marked posted with the same voucher id so neither leg can be double-booked.
  • Pairing only triggers for CARD rows with identical transaction_id and different currencies. Same-currency duplicates (e.g. a charge + matching refund pair) are intentionally left as two separate vouchers since their counterparty mapping may differ.

0.8.17

  • Wise CONVERT (balance-to-balance) rows: no longer require a counterparty account. The two legs of a Wise conversion (e.g. "Converted 1,278.83 DKK to 200.00 USD") now post as a single manuel voucher: debit the target Wise balance, credit the source Wise balance, amount = source DKK value. Both legs are then marked posted with the same voucher id so the paired row can't be double-booked.
  • Status check: CONVERT rows are Ready as soon as both the source and the target currency's Wise balance accounts are configured.
  • Wise\\PdfStatementParser::extract_convert_currencies(): returns {source_ccy, source_amount, target_ccy, target_amount} from a Wise "Converted X CCY to Y CCY" description.

0.8.16

  • Wise main table: "Missing mapping" badge is no longer cryptic. It now explains exactly what's missing (Wise balance account for the currency, counterparty account, bank-fees account, or interest-income account) both as tooltip and as small text under the badge. Common cause: the row's currency Wise account isn't set under Settings → Wise.
  • Poster::missing_reason(): new helper that returns the human-readable reason for a row's missing-mapping status.

0.8.15

  • Contact picker popover: hover/active highlight no longer turns the empty-state and reload-button row blue. Result rows now use their own .wads-contact-row class for the hover scope.
  • Contact picker popover: "Reload from Dinero" is now a proper WP button (button-secondary) on its own row, not an inline link.

0.8.14

  • Dinero contact picker: when no match is found (or the cache is empty), the popover now shows an inline "Reload from Dinero" link. Clicking it AJAX-refreshes the contacts cache in place and re-runs the search — no page reload, no scrolling to find the "Refresh Dinero contacts" button. Useful when you've just created a new contact in Dinero and want to pair it.
  • New AJAX endpoint: wads_contacts_reload (force-refresh + return updated list).

0.8.13

  • Customer sync: when Dinero returns cannotUpdateDeletedContact on PUT (the stored ContactGuid points to a deleted Dinero contact), we now automatically POST /v1/{org}/contacts/{guid}/restore and retry the PUT. If restore fails, the stored guid is cleared and a fresh contact is POSTed (new GUID stored on the user / order). Applies to push_user, push_guest_from_order re-sync and the bulk re-sync handlers.

0.8.12

  • Wise counterparty editor: account (wads-acct-cp) picks/edits now also AJAX-save via wads_wise_cp_save_one (same status badge as the contact field). The batch "Save counterparty mapping" button stays for convenience but is no longer needed for normal use.
  • Account picker (Accounts::render_datalist): pick() now dispatches a change event in addition to the existing change dispatch so listeners can rely on either event.

0.8.11

  • Wise counterparty editor: picking a Dinero contact from the autocomplete now saves immediately via AJAX (wads_wise_cp_save_one). A small ✓ / ✕ indicator appears next to the input. The "Save counterparty mapping" button is still there for batch edits to the account-number column.

0.8.10

  • Contacts/Accounts cache: empty cached arrays no longer poison the cache for 12h. Both all() methods now treat an empty cached array as a miss and re-fetch from Dinero on next page load.
  • Wise counterparty editor: contact picker auto-loads contacts on page render (the manual "Refresh Dinero contacts" click is no longer needed for first use). The Refresh button is kept for explicit re-load after adding contacts in Dinero.
  • Wise counterparty editor: when contacts are still missing after the auto-load attempt (token expired, API down, etc.), the picker popover shows the actual error message in red instead of a generic "No contacts loaded" hint.
  • Note: Dinero /v1/contacts returns all contacts (customers AND suppliers) in a single list — the customer/supplier split is UI-only and based on transaction history. No separate fetch is needed.

0.8.9

  • Wise counterparty editor: the Dinero-contact autocomplete now shows "No contacts loaded. Click 'Refresh Dinero contacts' at the top of this section." when the cache is empty, instead of silently saying "No matching contact". Mirrors the chart-of-accounts popover.
  • Removed temporary error_log debug lines from handle_refresh_accounts (added in v0.8.8 to diagnose the silent-redirect issue).

0.8.8

  • Refresh accounts/contacts: removed double URL-encoding of the message (rawurlencode + add_query_arg was producing %25 instead of %, making the redirected notice look like garbage / sometimes get dropped by the browser).
  • Added temporary error_log lines in handle_refresh_accounts to confirm whether the handler runs at all — inspect wp-content/debug.log after clicking the button.

0.8.7

  • Settings page: the ?wads_msg=/?wads_ok= query notices were silently dropped (only the Wise tab read them). Now rendered at the top of every tab, so "Refresh Dinero chart of accounts" / "Refresh Dinero contacts" finally show their success/error message.

0.8.6

  • "Refresh Dinero chart of accounts" and "Refresh Dinero contacts" now surface the actual API error in the admin notice when the call fails (previously the error was silently swallowed and you only saw "0 accounts loaded").
  • Both Accounts::refresh() and Contacts::refresh() now return WP_Error on failure so callers can react.

0.8.5

  • Settings → General: new "Finance accounts" section (renteindtægter, renteudgifter, bankgebyrer). Shared by Wise and any future feature that needs to book interest or bank fees.
  • Wise: CASHBACK rows now post to Renteindtægter (interest_income_account_number).
  • Wise: FEE rows now post to Bankgebyrer (bank_fees_account_number), separate from the acquirer (Mobilepay/Clearhaus/Teller) fee account under Misc.
  • Wise counterparty editor FEE/cashback info box: shows which account each row type posts to and links to Settings → General → Finance accounts.

0.8.4

  • Wise: schema bump (v3) adds posted_at, voucher_id, voucher_no, post_error columns to wp_wads_wise_tx for tracking which Wise lines have been booked into Dinero.
  • Wise: per-row status is now derived live from (mapping + posted state) and rendered as a colored badge: Posted (green, with Dinero voucher no. when known), Error (red, with last error tooltip), Ready (blue), Missing mapping (amber). FEE rows are Ready as soon as the Acquirer fee account is configured; CASHBACK is treated identically to FEE.
  • Wise: new status filter on the filter bar (All / Ready / Missing mapping / Error / Posted).
  • Wise: new "Book" action per row — creates and books a manuel voucher in Dinero via POST /v1/{org}/vouchers/manuel + POST .../book. The DKK amount comes from the per-year FX rate (FxRates::rate). For the bank side the row uses the configured Wise balance account (DKK/EUR/GBP/USD from Settings → Wise); for the offset it uses the counterparty-mapped account (or the Acquirer fee account for FEE/CASHBACK). Description includes the counterparty, original amount/CCY when present, and the Wise transaction id.
  • Wise: failed bookings store the Dinero error on the row so it's visible without digging through logs. Click the Error badge to see the message.

0.8.3

  • Wise tab dashboard: use the most recent year that has imported data instead of the calendar year, so the cards aren't empty in January (or before this year's statements have been imported).

0.8.2

  • Wise: parse and store the original purchase amount + currency from card transactions. The Wise PDF often shows e.g. Card transaction of 23.277,46 EUR issued by Spa Parts Vortex — even though the line is settled to the USD account. We now extract that EUR amount so the counterparty editor and main table reflect what was actually purchased, not how the payment was covered between the 4 balances.
  • Wise: schema bump (v2) adds two columns orig_amount + orig_currency. On upgrade the plugin backfills these for existing rows by re-parsing the stored description. Re-importing is not required.
  • Wise main table: new "Original" column showing the source-currency amount when it differs from the settled amount.
  • Wise counterparty editor: new "Net (orig CCY)" column. When all of a counterparty's transactions share the same original currency, the net total is shown in that currency instead of the settled one.
  • Wise: new "Dinero contact" column on the counterparty editor. Pick a contact via autocomplete (cached from GET /v1/{org}/contacts, like the chart-of-accounts picker). Stored alongside the Dinero account in option wads_wise_counterparty_map (shape upgraded from key => '1234' to key => ['account'=>'1234','contact_id'=>'guid','contact_name'=>'...']; old entries are read transparently).
  • Wise main table: new "Contact" column showing the mapped Dinero contact.
  • New Settings → General button: "Refresh Dinero contacts" — clears the contacts cache (12h TTL).

0.8.1

  • Wise tab: dashboard cards at the top now show per-currency overview for the current year (lines + net DKK) instead of the generic Products/Customers/Invoices/Credits cards.

0.8.0

  • Wise tab: new "Counterparty → Dinero account" editor. Lists every distinct counterparty seen in imported Wise statements (excluding FEE rows) with line count, last-seen date and net total. Each row has a small input that uses the chart-of-accounts autocomplete; a "Use NNNNN" suggestion button appears when an obvious match exists (first-word prefix). Mappings are stored in option wads_wise_counterparty_map.
  • Wise tab: FEE transactions are not mapped per counterparty — they are summed into a fixed info box at the top of the editor (per currency: lines + total) and posted to the configured Acquirer fee account (Misc → Acquirer / wallet accounts).
  • Wise tab main table: new "Account" column showing the resolved Dinero account for each row (mapped counterparty, fee account for FEE rows, em-dash + red text for unmapped non-FEE rows).

0.7.9

  • Settings → General: "Misc import integration" row moved below "Wise integration" so the toggles appear in the same order as the tabs.

0.7.8

  • Settings → General: new "Misc import integration" checkbox (mirrors the "Wise integration" toggle). The "Misc import" tab and its admin actions are only registered when the box is ticked.

0.7.7

  • Misc tab: "Acquirer / wallet accounts (used by Misc import posting)" moved out of Settings → General into its own self-contained editor on the Misc tab (collapsible like the Wise accounts editor, with its own save handler and nonce). Auto-collapses once any account is configured. Includes the chart-of-accounts autocomplete on each input.

0.7.6

  • Refresh Dinero chart of accounts: button stays in Settings → General only (it's a global cache, not Wise-specific). Empty-state in the autocomplete now reads "No accounts loaded. Go to Settings → General and click 'Refresh Dinero chart of accounts'."

0.7.5

  • Wise tab: "Refresh Dinero chart of accounts" button is now also rendered inside the Wise accounts editor (previously only on Settings → General), so a fresh install can populate the chart without leaving the Wise tab.

0.7.4

  • Chart of accounts: fixed empty dropdown. Dinero's accounts/entry endpoint returns the account number under the key AccountNumber (not Number like most other endpoints). The parser now accepts both AccountNumber and Number (plus lower-case variants), so account lookups actually populate.
  • Account autocomplete: dropdown now always opens on focus and shows a clear empty-state message ("No accounts loaded. Click Refresh…" or "No matching account.") instead of silently doing nothing when the chart is empty or the typed text doesn't match.

0.7.3

  • Wise tab: "Wise accounts (one per currency)" moved out of Settings → General into its own self-contained editor on the Wise tab (collapsible like the FX editor, with its own save handler and nonce). Auto-collapses once any account is configured.
  • Chart-of-accounts autocomplete: replaced native <datalist> (which only matches on the account number) with a custom dropdown that searches by both number prefix AND case-insensitive name substring — so typing e.g. "wise" now lists all Wise accounts. Keyboard navigation (↑/↓/Enter/Esc) and click-to-pick supported.

0.7.2

  • New: Dinero chart of accounts (kontoplan) is now fetched via the API (GET accounts/entry) and cached for 12 hours in the transient wads_accounts_chart. New helper class Webalive\DineroSync\Dinero\Accounts exposes all(), refresh(), by_number(), search() and resolve() for shared lookup by number or name.
  • Settings → General: every account-number input (bank, MobilePay, Clearhaus, Teller, fees, adjustments, sales-non-Woo, debtor, plus the 4 Wise per-currency accounts) now uses a shared <datalist> autocomplete fed from the live chart, and shows the resolved account name next to the field. New "Refresh Dinero chart of accounts" button in the General tab clears the cache and re-fetches.
  • Wise tab: new summary table at the top of the page showing per-year / per-currency totals (in / out / net / FX→DKK / net DKK) plus a year-total DKK row. Reads the same FX rates configured in the FX editor.

0.7.1

  • Wise tab: FX rate editor is collapsed by default once any rate has been saved (still open the first time you open the tab so it's visible).

0.7.0

  • New optional "Wise" tab. Toggle on/off via Settings → General → "Wise integration". When enabled, lets you import Wise multi-currency account statements (the original .zip with the 4 per-currency PDFs, or individual PDFs). Transactions are stored in their native currency in a new table wp_wads_wise_tx (deduped by currency + Wise transaction id + amount + date). The list view filters by currency and year and converts each amount to DKK using a per-year, per-currency FX rate you maintain in the same tab. Currently no Dinero posting — this is a data layer to power later reconciliation work.

0.6.37

  • Payments tab partial-credit: create endpoint corrected to POST sales/creditnotes (v1). v1.2 is only for the PUT update endpoint, not for create — hence the 405 on the previous attempt.

0.6.36

  • Payments tab partial-credit: switched credit-note endpoints from the legacy creditnotes paths to v1.2/sales/creditnotes (create) and sales/creditnotes/{guid}/book (book). Fixes "Dinero API error (HTTP 405)" when posting the partial credit note.

0.6.35

  • Payments tab partial-credit: credit note line description switched to English: "Adjustment - short paid ()".

0.6.34

  • Payments tab partial-credit: credit note line description changed to "Justering - mindre solgt ()" (was "Justering — mindre salg").

0.6.33

  • Payments tab: handle invoices that are partially paid in the bank. When the linked bank line(s) sum to LESS than the invoice total, the row now shows a yellow "Bank delvis X / Y" badge and a "Send (kreditér diff)" button. Clicking it creates a Dinero credit note for the missing amount on the new adjustment_account_number (Settings → General → "Adjustment / mindre salg account"), books the credit note, and registers the received bank amount as payment on the invoice (one Dinero payment per bank line, marking each bank line as posted). Adds an order note documenting the adjustment. Requires bank_account_number and the new adjustment_account_number to be set.

0.6.32

  • Misc "Send to kladde": the booked finansbilag now uses the matched bank line's txn_date as VoucherDate (instead of the misc row's payout_date). Acquirers (Teller / Clearhaus / MobilePay) often book the payout in the bank one day after the settlement-report date, and Dinero's bank-reconciliation pairs voucher and bank line by date — so using the bank date makes the auto-match work straight out of the gate.

0.6.31

  • Misc: new "Send all postable to kladde" bulk button above the table. Sequentially posts every row that has exactly one bank-line match and is not already posted (MobilePay / Clearhaus / Teller). Reuses the existing per-row AJAX endpoint so each voucher is created and immediately booked, and progress (ok / fail counters) is shown inline.

0.6.30

  • Misc "Send to kladde": now also enabled for Clearhaus and Teller / Nets payouts (previously MobilePay only). The same 3-line booked finansbilag is created — Debet bank (payout amount), Debet acquirer fee (fees), Kredit acquirer holding — using clearhaus_account_number or teller_account_number from Settings → General as the holding account. Button still requires exactly one bank-line match and that the row isn't already posted.

0.6.29

  • Misc "Send to kladde" (MobilePay): the manual voucher is now booked directly after creation. The plugin first creates the draft (POST vouchers/manuel) and then immediately books it (POST vouchers/manuel/{guid}/book) using the Timestamp returned from the create call, so the bilag lands as a fully booked finansbilag in Dinero rather than sitting as a kladde awaiting manual approval. The misc row still records the Guid in dinero_ledger_item_id and now also stores the booked VoucherNumber.

0.6.28

  • Misc "Send to kladde": diagnostic logging. The full Dinero request body and response (or error) is now written to debug.log with prefix [wads-misc] so we can see exactly what was sent and what came back when a kladde upload appears to silently fail.

0.6.27

  • Misc import: "Send to kladde" button is now AJAX. The button shows an inline spinner while the request runs and updates the row in place on success (state → Posted, Guid shown) or shows an inline red error message on failure — no full-page redirect, no need to keep the redirect-notice URL visible.

0.6.26

  • Misc import (MobilePay): new "Send to Dinero kladde" button per row for MobilePay payouts that have exactly ONE bank-line match and are not yet posted. Posts a 3-line manual voucher (finansbilag) as a DRAFT (kladde) to Dinero — never auto-booked. Lines: Debet bank (payout amount), Debet acquirer fee (fees from settlement), Kredit MobilePay holding (payout + fees = brutto). Account numbers are read from Settings → General (bank_account_number, fee_account_number, mobilepay_account_number) — no hardcoded numbers. On success the misc row is marked state=posted, the matched bank line id is stored in bank_line_id, the Dinero voucher Guid is stored in dinero_ledger_item_id, and posted_at is set. The kladde appears in Dinero under Bilagsoversigt for the user to review and book manually. Same flow for Clearhaus / Teller comes in a later release.

0.6.25

  • Misc "Bank match" column: when multiple bank lines have the same amount within ±1 day, prefer the line whose description contains the payout reference (e.g. MobilePay terminal id 02002190600012710254). This resolves the common case where two consecutive MobilePay payouts are the same amount and both terminal ids appear in the bank ledger.

0.6.24

  • Bank import: broadened the default Clearhaus keep-pattern from /Clearhaus\s*AS/i to /Clearhaus/i so newer bank-line formats like CH2012915 2025-05-15//Clearhaus//RE are also kept. Operators who already saved a custom keep-patterns list must click "Reset to defaults" or update the Clearhaus needle manually.
  • Misc import: payouts with amount = 0 (e.g. Nets monthly fee-only settlements) now show "—" in the Bank match column instead of a red "no match", since a zero-amount payout cannot match a real bank line.

0.6.23

  • Misc import: new "Bank match" column in the misc lines table. For each payout the table now shows candidate bank lines with the same amount (±0.01) within ±1 day of the payout date. Read-only — no posting yet. Multiple candidates show a yellow "N candidates" warning so the operator knows to disambiguate. Lookup is one bank-table query per page render, bucketed by date for O(1) per-row matching.

0.6.22

  • Settings (General tab): widened the 7 Dinero account-number inputs (bank + 6 acquirer/wallet fields) so 5-digit numbers like 55200 / 53000 / 7220 fit comfortably with a couple of characters of headroom.

0.6.21

  • Settings (General tab): six new Dinero account fields used by the upcoming Misc-import posting engine — MobilePay holding account, Clearhaus holding account, Teller/Nets holding account, Acquirer fee account, Sales (non-Woo), Debtor account. Saved on the existing single options row; empty by default. No posting wired up yet — these fields just establish the configuration surface so v0.6.22 can implement match + post.

0.6.20

  • Misc import: new MobilePay XLSX importer. Reads the "Afregningsrapport" exported from the MobilePay merchant portal (sheet "Oversigt", table SummaryData at A12:R59). One payout row per wads_misc_lines row (provider = mobilepay). Mapping: Salgssted → merchant_name, MSN/MobilePay-nummer → merchant_id, Bogføringsdato → period_start/period_end, Valuta → currency, Betalinger → summary_sales, Refusioner → summary_refunds, Gebyrer → summary_fees, Andet → summary_other, Udbetaling → payout_amount, Udbetalingsreference → payout_reference, Planlagt udbetalingsdato → payout_date (matches the date the bank statement actually shows the deposit). The totals row ("I alt") is skipped. Uses PHP's built-in ZipArchive + SimpleXMLElement — no extra Composer dependency. Auto-detect picks mobilepay for any .xlsx whose sharedStrings/workbook XML contains "mobilepay" / "afregningsrapport", or whose filename contains those words.

0.6.19

  • Bank import: corrected default keep-patterns to match the actual bank line text observed in production. Clearhaus AS (with space) replaced by the regex /Clearhaus\s*AS/i so it matches both Clearhaus AS and the run-together ClearhausAS form. The MobilePay terminal substring 02002106 replaced by 0200219 (the prefix actually appearing on the bank statement, e.g. 02002190600010601255). Operators who already saved a custom keep-patterns list must click "Reset to defaults" in the Bank tab to pick up the new defaults, or add the two needles manually.

0.6.18

  • Bank import: PDF debug dump now shows EVERY parsed entry (not just the first 20) with a KEEP / SKIP marker and — when kept — the exact pattern that matched. This lets the operator paste the real bank line text into the keep-patterns textarea and verify what would be imported, without re-uploading the PDF.
  • Bank import: keep-pattern check now exposes the matching reason internally so the debug dump can surface it.

0.6.17

  • Bank import: the PDF “keep” filter is no longer hardcoded. There is a new editable “Keep patterns” textarea above the upload form — one needle per line; if the line starts with / it’s treated as a regex, otherwise it’s a case-insensitive substring match. Defaults: Dankort 3388083 settlements, Clearhaus AS, MobilePay terminal id 02002106, Nets/Teller merchant 8863789. The previous hardcoded Dankort 3388083 / acquirer regex still applies as a baseline so existing imports keep working even with an empty option.
  • Misc import: new Teller / Nets settlement CSV importer. Detects files that contain a row beginning with Settlement Information followed by Date (the header), and reads each subsequent Settlement Information row as one payout: payout_date = Date, payout_amount = Settled, summary_fees = Service Fee, summary_chargebacks = Chargeback, payout_reference = Payment Reference, merchant_id = Merchant no.. Monthly Total / Yearly Total rows above the settlement section are ignored.
  • Misc import: stub for MobilePay (will be wired up once the actual settlement file format is confirmed — the screenshot shows a transaction-level export, which is not what we want for bank reconciliation).

0.6.16

  • New “Misc import” tab — phase 1 (foundation). Imports acquirer settlement reports into a new local table wads_misc_lines so the lump-sum payouts that hit the bank statement (Clearhaus, Teller/Nets, MobilePay) can later be reconciled against the bank lines and posted to Dinero. This release ships the Clearhaus CSV importer (the format with the merchant_id, merchant_name, id, settled, currency, period_start, period_end, payout_amount, payout_date, summary_* columns); Teller/Nets and MobilePay parsers will follow when sample files are available. Includes upload form, listing with provider/date filter, dedup on provider|payout_date|payout_amount|merchant_id, and a “Delete all” action. Posting to Dinero is intentionally not in this release.
  • Bank PDF importer: stopped silently dropping non-Dankort lines. Clearhaus, Teller/Nets and MobilePay lump-sum bank deposits are now KEPT (in addition to the existing Dankort 3388083 settlements), so the bank tab can show the same payouts that the Misc import tab will register. The filter still removes everything else (salary, interest, generic transfers) to keep the bank table relevant. Filterable via wads_bank_pdf_keep_line for any future tweaks.

0.6.15

  • Sales sync: fixed TypeError: push_order() expects WC_Order, OrderRefund given that crashed the chunk runner with a generic 500/Netværksfejl. The worklist query (wc_get_orders returning IDs) was matching both shop_order AND shop_order_refund posts, and refund IDs then resolved to WC_Order_Refund objects which fail the WC_Order type hint. Worklist now explicitly filters type => 'shop_order', and sync_ids() skips anything that isn't a WC_Order defensively.

0.6.14

  • Sales sync: the START AJAX call now ONLY builds the worklist of order IDs and returns immediately — it no longer also processes the first order. That keeps the start request very short (a single wc_get_orders IDs query) so it can’t time out even on stores with hundreds of orders or when the first matched order would have been slow to push. The runner then makes a second AJAX call to actually process the first order.
  • Sales runner: the JS now SHOWS the real failure when a chunk request errors. Instead of the generic “Netværksfejl”, the UI now displays the HTTP status and a short snippet of whatever the server actually returned (e.g. nginx 504, PHP fatal). This makes it diagnosable instead of opaque.

0.6.13

  • Sales worklist build: dropped the defensive PHP date filter that called wc_get_order() for every candidate id. On large stores that loop was N+1 (one DB hit per order) and could exceed the proxy timeout on the very first “start” AJAX call — reproducing the network error. Woo’s own date_created filter on the IDs query is sufficient for the strict-range guarantee from 0.6.12.

0.6.12

  • Sales month / range sync now honors the requested window STRICTLY. Previously the chunk runner re-queried wc_get_orders per request with a meta_query NOT EXISTS filter and trusted Woo to also honor date_created — in practice that combination could leak orders from outside the window (e.g. older unsynced orders) and the runner would chew through them before reaching the requested month. Now, when a month/range/all run starts, the worklist of order IDs is computed ONCE up-front (with both Woo’s date filter AND a defensive PHP date check) and stored in the transient. Each chunk pops the next ID from that list and processes EXACTLY that order. The total/processed counts are now meaningful (total = orders in the window, processed counts down from total). It is now mathematically impossible to process an order outside the requested window.

0.6.11

  • Products tab: variations of DRAFT parent products are no longer treated as active. Variation rows always have post_status = 'publish' in WordPress regardless of the parent’s status, so a draft variable product’s variations were leaking into the duplicate-SKU scan and the sync queries — colliding with the live siblings and getting flagged as duplicates. All five product queries (duplicate scan, single-SKU dup check, sync_batch count + page, sync_pending_batch count + page) now require that variations’ parent is also publish.

0.6.10

  • Products tab: full-page-reload chunk runner replaced with the same AJAX runner pattern used on Sales/Payments. “Sync ALL products” and “Sync pending products only” now stream live progress in-place, no flicker. Each AJAX call processes ONE product so a single request can’t exceed any reasonable proxy timeout. Includes 3× retry with backoff and a 90s AbortController-based fetch timeout, mirroring Sales.

0.6.9

  • Sales chunk size set to 1 order per AJAX call — smoother progress feedback and effectively immune to proxy timeouts.

0.6.8

  • Sales chunk size restored to 20 orders per AJAX call (matches the proven setup that ran without network errors before). Retry + AbortController fetch timeout from 0.6.5/0.6.6 are kept as a safety net.

0.6.7

  • Sales chunked sync: each AJAX call now processes EXACTLY ONE order. Even when Dinero is slow and an order needs 5–7 API calls, a single order finishes in a handful of seconds — well below any conceivable proxy timeout. Combined with meta_query NOT EXISTS + always-page=1, every chunk request just picks up the next unsynced order, processes it, and returns. The “Netværksfejl” that surfaced on month-syncs in 0.6.5/0.6.6 was rooted in chunks occasionally exceeding the proxy timeout when several heavy orders landed in the same batch; with chunk=1 that situation can no longer arise.

0.6.6

  • Sales chunked sync hardened against “Netværksfejl” on slow chunks. Even with 0.6.5's retry, when Dinero is having a slow minute every chunk could exceed the proxy timeout and the retries against the SAME 10-order chunk all hit the same wall. Two changes: (1) chunk size reduced from 10 to 5 orders so each AJAX call finishes well under typical 30–60s proxy limits even when each order makes 4–7 Dinero calls; (2) the JS now uses an explicit AbortController with a 90s timeout per request so a stuck connection actually aborts and the retry runs sooner instead of hanging on the browser's default of “forever”. Combined with the existing 3 retries this should get a month-sync through any reasonable transient hiccup.

0.6.5

  • Fix two regressions in the sales chunked runner introduced by 0.5.97 (meta_query NOT EXISTS filter on wads_dinero_invoice_id):
    • Pagination skipping: each successful chunk gave its 10 orders an invoice GUID, so on the next call page=2 no longer pointed at the orders previously at index 11-20 — those orders were silently skipped. The runner now always asks for page 1 because the filter itself advances the window (the next 10 un-synced orders bubble to the top).
    • Browser “Netværksfejl” on month sync: a single slow chunk (Dinero 429 backoff hits 3×8s, plus invoice POST/book) could exceed the proxy/PHP request limit and the AJAX call would surface as “Networking error”, halting the run even though the next chunk would have succeeded. The JS runner now retries a failing chunk up to 3 times with 1.5s/3s backoff before giving up; only a hard error after 3 tries stops the run. Server side: ajax_sales_chunk now bumps set_time_limit(0) and ignore_user_abort(true) so a slow chunk completes even if the browser disconnects.

0.6.4

  • Bank match no longer requires the bank line to fall within ±3 days of the order/invoice date. Real-world example (order #21465): order completed on 2025-07-15 but the card capture didn't actually hit the bank until 2025-12-22 — the previous ±3-day window meant the matcher never saw the line and the row showed “Ingen bankmatch — uafklaret”. The matcher now fetches every bank line whose order_id matches one of the rows OR whose description mentions one of the order-number/SF-ref needles, regardless of date. Amount and link-to-order checks are unchanged so we still don't pick up unrelated lines that just happen to be the same amount.

0.6.3

  • Split bank matches now post ONE Dinero payment per bank line, instead of a single combined payment for the total. Required so Dinero's bank reconciliation can pair each posted payment with its corresponding line on the bank statement (e.g. 500 kr paid as 100 + 250 + 150 → three Dinero payments with the matching dates and amounts). The order note now lists each registered payment, and every participating bank line is marked posted only after its own payment is successfully booked. If one of the partial payments fails, the rest still get registered and the failure is reported per-line.

0.6.2

  • Bank match now supports split payments. Until now attach_bank_matches() required ONE bank line whose amount equaled the invoice total — if a customer paid the same order across two transactions (e.g. partial card auth + remainder, or a wrong-amount payment + adjustment) the row showed “Ingen bankmatch — uafklaret” even though both lines were obviously linked to the order. The matcher now collects every bank line linked to the order (by order_id or by order-number/SF-ref appearing in the description) and, when no single line equals the invoice total, accepts the set as a sum-match if the total of those lines equals the invoice total. The Bank chip shows “×N” for split matches and tooltips it as “split (N linjer)”. When the payment is registered we mark all participating bank lines as posted (previously the SQL LIMIT 1 cleared only one).

0.6.1

  • Fix: Payments tab scan returned “no unpaid invoices” even when many were present. v0.5.99 added TimeStamp to the field list of the Dinero /sales listing call to skip a per-payment timestamp lookup, but TimeStamp is not a valid field on the listing endpoint and Dinero rejected the request — fetch_payments_rows_page() then returned null, the scan finished with 0 rows, and the empty state was displayed. Reverted that field addition. register_payment() still keeps the stale-timestamp retry from 0.5.99, and chunk size remains 20, so the payments runner is still meaningfully faster than pre-0.5.99 even though we now do the per-invoice GET /invoices/{guid} again to fetch a fresh timestamp.

0.6.0

  • Payments tab: “Account mapping” block (default deposit account + mapping rules textarea) is now collapsed inside a <details> panel by default, mirroring how Tools/Advanced sections are presented elsewhere. The Unpaid invoices table is the first thing visible on the tab; click “Account mapping” to expand the editor when you actually need to change the mapping.

0.5.99

  • Payments tab “register all” further optimized: chunk size raised from 10 to 20 (halves the number of HTTP round-trips between browser and WP) and the per-invoice GET /invoices/{guid} timestamp lookup is skipped when the timestamp is already known from the unpaid-list cache. Sales::register_payment() now accepts an optional $timestamp argument and fetch_payments_rows_page() requests TimeStamp in the /sales field list. If Dinero rejects the cached timestamp (409 conflict because the invoice was edited between listing and payment) we automatically retry once with a fresh GET /invoices/{guid}. Net result for a 20-payment chunk: ~22 Dinero API calls instead of ~70, and the AJAX runner finishes the same workload in roughly a third of the time.

0.5.98

  • Payments tab “register all” chunked runner is now ~5–10x faster. Each call to register_payment_for_guid() re-fetched the full list of unpaid Dinero invoices (paged GETs of up to 2000 invoices) just to look up one row — done 10 times per chunk that meant ~50+ wasted API requests per 10 registered payments. Switched to the per-request cached fetcher and stopped busting the cache inside the chunk loop, so each chunk now hits Dinero once for the unpaid list (regardless of chunk size) plus the 2 calls (GET /invoices/{guid} for timestamp + POST .../payments) actually needed per payment. The cache is still discarded between AJAX chunks, so subsequent chunks see a fresh list.

0.5.97

  • Sales sync skips already-synced orders at the query level. Previously sync_range_batch() loaded every order in the date range and only then bailed out via the META_INVOICE_GUID check inside push_order() — e.g. re-running a month with 101 orders where 100 were already invoiced still loaded all 101 from the DB just to skip them. The Woo wc_get_orders() query now adds a meta_query that excludes orders carrying wads_dinero_invoice_id, so the runner only iterates orders that actually need work. The progress "total" now reflects the un-synced count, so re-runs finish almost instantly when nothing is pending.

0.5.96

  • Sales sync performance: ensure_dinero_product_names_match() was hammering Dinero's /products/{guid} endpoint with one GET (and possibly a PUT) per unique product line per order. Syncing a single month (101 orders) could fire several hundred extra API calls, dragging the run out and tripping Dinero's per-minute rate limit (HTTP 429) at the tail of the run. Two-tier cache added: (1) per-PHP-request static cache so each product GUID is only checked once even when many orders share it; (2) persistent product-meta _wads_dinero_name_verified_at storing the WC product modification timestamp at the moment the name was confirmed in sync — subsequent runs skip the GET entirely until the Woo product is edited. After the first full sync the name-check overhead drops to ~zero API calls.

0.5.95

  • Removed the "Reconcile" admin tab — it is no longer used after the new bank import flow took over reconciliation. The underlying ReconcilePage class file is left on disk for now but is no longer instantiated or routed.

0.5.94

  • Sales tab: every sync run — "Sync ALL eligible", date-range, single month, and per-order — is now driven by AJAX with a rotating spinner overlay and live progress (same UX as the Audit and Payments tabs). Replaces the previous POST/redirect/auto-submit chunked runner that round-tripped the full admin page every 1s.

0.5.93

  • Payments tab: bank-match for orders < 20000 (no SF prefix on the Dinero invoice) was failing because the Dankort settlement description concatenates the merchant id 3388083 directly with the order number (e.g. DKSSL3012338808319943). The strict word-boundary lookbehind (?<![0-9]) saw the trailing 3 of 3388083 and rejected the match, so 19xxx-orders showed "Ingen bankmatch — uafklaret" forever. The matcher now also accepts a numeric order-number needle when it is preceded by the Dankort merchant id 3388083.

0.5.92

  • Payments tab: table is now built progressively via AJAX with a rotating loading-spinner overlay (same UX as the Audit tab). Replaces the previous synchronous fetch that loaded all up-to-2000 unpaid invoices in a single request — the page no longer hangs while Dinero's sales endpoint is being paginated.
  • Payments tab: "Register payment for all eligible" now runs as an AJAX loop processing 10 invoices per request, with progress and a Cancel button. Avoids the 504 Gateway Timeout that occurred when many eligible payments were posted in a single PHP request behind nginx/CloudFlare.

0.5.91

  • Payments tab: only Dankort rows require a matched bank line. Mastercard, Visa and other card-acquirer settled payments ("MobilePay (Mastercard ...)", "Online betaling (Mastercard ...)", etc.) are now registered against the mapped acquirer/teller account using the order's date_paid — they're never visible on the bank statement individually because the acquirer settles them as lump sums. Previously the strict "bank match required" policy left every non-Dankort row stuck as "Ingen bankmatch — uafklaret".
  • Payments tab: per-row Account column now reflects the actual account that will be used (bank account for Dankort with a match, mapped acquirer account for everything else) instead of always showing the bank account.

0.5.90

  • Settings page: renamed the "Drafts" tab to "Tools". Internal slug stays drafts so existing bookmarks and admin-post URLs keep working.
  • Settings page: tab labels now use proper i18n wrappers — the previous __( $variable, ... ) call couldn't be picked up by the string extractor, so translations didn't actually take effect. All tab labels are now wrapped individually. Danish translations added for the first three tabs (General, Products, Customers); the rest stay English for now.

0.5.89

  • Audit tab: fix suppression of already-credited mismatches missing the credit note when the source invoice is older. Dinero sorts the credit-notes list by VoucherDate (the ledger date, which equals the source invoice's date), so a credit note for a year-old invoice ends up deep in the list — outside the 5-page (500-item) window the suppression scan covered. Bumped to 10 pages and added per-user transient caching of the credit-notes list (10 min) so the scan only fetches once per audit run instead of repeating for every Dinero invoice page.

0.5.88

  • Audit tab: "Credit & recreate" now retries the recreate step automatically on Dinero rate-limit (HTTP 429) with 10s and 20s back-offs, since the credit-note booking + invoice search + push are all rapid sequential calls that can otherwise burn through the per-request 429 retry budget.
  • Audit tab: when the recreate step still fails after retries, the order is flagged with a _wads_credit_recreate_pending meta and surfaced at the top of the audit table as a "Pending recovery" row with a "Retry recreate" button. This guarantees we can always finish a half-completed credit-and-recreate run, even though v0.5.87's suppression hides the original mismatch row once the credit note is booked.
  • Audit tab: only one row action runs at a time — clicking multiple "Credit & recreate" buttons in quick succession used to fire several full credit/search/push pipelines concurrently and trip Dinero's rate limit. The other buttons are now disabled while one action is in flight.

0.5.87

  • Audit tab: suppress mismatches whose wrong invoice has already been credited. The scanner now looks up recent credit notes (pre-filtered on the auto-generated "Kreditering af faktura NNNNN" description) and fetches detail to confirm CreditNoteFor matches the invoice GUID and the credit note status is Booked/Paid/OverPaid. Already-credited mismatches are silently dropped from the result set so the audit only flags issues that still need attention.
  • Audit tab: AJAX-driven scan loop with a rotating spinner overlay over the results table replaces the previous POST/auto-submit blink loop. The page no longer reloads between Dinero pages — rows stream into the table as each page completes, and a Cancel button stops the scan cleanly.

0.5.86

  • Hotfix: lookup tool fatal error "stripos(): Argument #2 ($needle) must be of type string, int given". PHP auto-casts numeric string array keys to int, so iterating the known-invoice-numbers map produced int keys. Cast to string before passing to stripos().

0.5.85

  • Lookup tool: rewrote credit-note resolution based on observed Dinero behavior. Findings: (1) GET /sales/creditnotes returns 0 rows when a fields query parameter is supplied — must be omitted; (2) the list response only carries Guid/Date/Description/ContactName/Type — NOT CreditNoteFor, ExternalReference, Number, Status or Total; (3) the invoice-detail endpoint does NOT contain any credit-note reference field. New approach: scan recent credit notes without fields, pre-filter by Description containing one of the order's invoice numbers (Dinero auto-generates "Kreditering af faktura NNNNN"), then GET each candidate's detail to populate Number/Status/Total/CreditNoteFor. Removed the now-redundant invoice-detail walker and the temporary debug panel.

0.5.84

  • Lookup tool: temporary debug panel that shows the raw Dinero responses (invoice-detail keys, extracted credit-note GUIDs, first 3 rows of credit-note pages 0+1, and a fields-less probe of credit-notes page 0). Use this to figure out which field actually carries the credit-note reference for newly created credit notes — will be removed once the right field is identified.

0.5.83

  • Lookup tool: deterministic credit-note resolution. For each invoice we found for the order, fetch the Dinero invoice detail and pull credit-note GUIDs out of any field whose name contains "credit" (e.g. CreditNoteGuid, CreditNotes[].Guid). Then GET each credit note directly. This no longer depends on the credit-notes list endpoint surfacing the row in the most-recent 1000 — newly created credit notes that were invisible to the list scan are now found via their source invoice.
  • The previous list-based scan (ExternalReference / CreditNoteFor matching, sorted by VoucherDate descending) is kept as a best-effort fallback to also catch credit notes whose source invoice is no longer linked.

0.5.82

  • Lookup tool + audit credit-note scan: sort credit notes by VoucherDate descending (same as the invoice scan), so the most recently created credit notes are always in the first 10 pages. Without an explicit sort, Dinero's default order can push newly created credit notes far back, making them invisible to the 1000-most-recent client-side scan introduced in 0.5.78/0.5.80.

0.5.81

  • Drafts tab "Lookup" tool: credit-note scan now also matches credit notes whose CreditNoteFor GUID points at one of the order's known invoice GUIDs (every invoice found above + the GUID currently stored in the order's wads_dinero_invoice_id meta), in addition to the existing ExternalReference match. Dinero's generate-creditnote endpoint does NOT copy ExternalReference from the source invoice onto the new credit note, so credit notes created through the audit "Credit & recreate" action were invisible to the lookup tool when filtering on ExternalReference alone.

0.5.80

  • CRITICAL fix: Audit "Credit & recreate" idempotency check (added in 0.5.74) was using Dinero's queryFilter=ExternalReference=... parameter, which silently returns 0 hits for many records (same bug as fixed for the Lookup tool in 0.5.78). The result: an existing credit note for the audited invoice was not detected, AND if Dinero refused the duplicate generate-creditnote call the action skipped credit entirely and proceeded straight to recreate, leaving the wrong invoice un-credited and the books out of balance. Both the credit-note lookup and the existing-invoice lookup now scan up to 1000 most recent records and filter by ExternalReference client-side.

0.5.79

  • Drafts tab "Lookup" tool: each Dinero invoice row now has a "Set as order meta" button that updates the Woo order's wads_dinero_invoice_id and wads_dinero_invoice_number to point at that invoice. The current invoice is highlighted in green and labelled "current". An order note is written recording the previous and new values. Use this to repair an order whose stored invoice meta points at a stale/wrong invoice.

0.5.78

  • Drafts tab "Lookup" tool: stop relying on Dinero's queryFilter=ExternalReference=... parameter — it silently returned 0 hits even for invoices that were created with a matching ExternalReference. The lookup now scans up to 1000 most recent sales invoices and credit notes and filters by ExternalReference client-side (case-insensitive), the same strategy the audit scanner uses, so freshly created invoices like 44575 actually show up.

0.5.77

  • Drafts tab: new "Lookup: Woo order ↔ Dinero invoices / credit notes" tool. Enter a Woo order ID or order number and see (a) which Dinero invoice GUID/number is currently stored on the order, (b) every Dinero sales invoice with that ExternalReference (all statuses), (c) every Dinero credit note with that ExternalReference including which invoice GUID it credits. Use this to figure out exactly which Dinero documents belong to a given order before deciding what to credit.

0.5.76

  • CRITICAL fix: Audit "Credit (unpaid) & recreate" / "Credit & recreate" now credits the EXACT Dinero invoice the audit flagged — not whatever invoice GUID happens to be stored on the Woo order. Previously, if a prior recreate run had already overwritten the order's wads_dinero_invoice_id meta with a newer invoice, the audit action would credit that newer invoice (often the correct one) and leave the original wrong invoice untouched, then create yet another duplicate. The audit row now carries the flagged invoice GUID through the JS click handler (data-invoice-guid / wads_invoice_guid) into both the form-post and AJAX handlers, and the worker uses it as the authoritative target.

0.5.75

  • Audit row action button: when the Dinero invoice status is Overdue or Booked (i.e. unpaid), the button now reads "Credit (unpaid) & recreate" instead of just "Credit & recreate", to make it clear that no payment will be removed — only the booked invoice is being cancelled. Same workflow as before.

0.5.74

  • Audit "Credit & recreate" is now idempotent and safe to re-run on a partially-fixed order:
    • Before generating a new credit note it queries Dinero for any existing credit note that targets the same invoice (CreditNoteFor match via ExternalReference filter). If a Booked credit note already exists it is REUSED — no new credit note is created. If an unbooked draft exists it is reused, its date is set to the original invoice date, and then it is booked.
    • Before recreating the replacement invoice it queries Dinero for another sales invoice with the same ExternalReference (excluding the one just credited). If found, the order's Dinero meta is updated to point to that existing invoice and NO new draft is created. The order note records which invoice was re-adopted. This prevents double invoices and double credit notes when the operation is re-run after a partial failure or after manual cleanup in Dinero.

0.5.73

  • CRITICAL fiscal-year fix: Audit "Credit & recreate" now ALWAYS sets the credit note Date to the original invoice's Date before booking, so credit notes post in the correct fiscal year. Previously the credit note draft was booked with Dinero's default date (today), which could post a credit for an old invoice into the current fiscal year — an accounting violation that cannot be undone in Dinero (booked credit notes are immutable). The fix uses the v1.2 PUT sales/creditnotes/{guid} update endpoint between draft creation and booking to rewrite the Date. If the original invoice has no readable Date, or if the date update fails, the draft is deleted and the operation is refused — never booked.
  • Dinero client: paths may now be prefixed with v1.2/ (or v1.1/) to target newer API versions; the base URL is swapped accordingly.

0.5.72

  • Drafts tab: new "Cleanup: delete payments on a (credited) invoice" tool. Enter a Dinero invoice number or GUID, list all registered payments (date, amount, account, description) and delete each one with a single click. Use this to clean up orphan payment postings (e.g. against account 55000) that were left behind by older Credit & recreate runs before the v0.5.71 fix.

0.5.71

  • Audit "Credit & recreate" now actually deletes the old payment posting too. The Dinero GET invoices/{guid}/payments response is wrapped in a Payments array (PaymentsCollection schema) — the previous code only looked at Collection, so it always reported "0 payment(s) deleted" and the booking against e.g. account 55000 stayed behind. Now the response is parsed correctly and every payment is unregistered before the credit note is issued.
  • New "Drafts" tab: lists every invoice currently in status Draft in Dinero, with per-row buttons for Book (book the draft as-is), Re-push from Woo (delete + recreate from the matching Woo order, when one is found via wads_dinero_invoice_id or external reference) and Delete. Compares each draft's total against the matching Woo order total and highlights any drift.

0.5.70

  • Sales push: switched discount strategy. Per-line % discount on product lines is no longer used — it never matched Woo's fixed-amount cart discounts to the øre. Product lines are now sent at full pre-discount price (incl. VAT), and a single bottom "Rabat" / "Afrunding" line in DKK absorbs the entire difference vs. the Woo order total. Dinero invoice total now equals Woo total exactly, regardless of how the discount is structured in Woo.
  • Removed the duplicated rounding block accidentally left at the end of build_product_lines() after 0.5.69.

0.5.69

  • Sales push: guarantee Woo ↔ Dinero invoice totals match to the øre. After building all product/shipping/fee lines, we sum their incl-VAT amounts the same way Dinero rounds (per line, 2 decimals) and compare to the Woo order total. Any remaining drift is added as an explicit "Afrunding" line on account 1000 so the Dinero total always equals the Woo total exactly.

0.5.68

  • Audit tab: also removed the data-confirm attribute from the "Recreate from Woo" and "Credit & recreate" buttons so no confirm dialog can appear even if a cached old audit.js is still loaded by the browser.

0.5.67

  • Audit tab: removed the JS confirm() prompt on the "Credit & recreate" action button. The action now runs immediately on click — no more double confirmation dialog.

0.5.66

  • Sales push: when the Dinero invoice is booked (godkendt), the invoice number is now also written to the order meta _wcpdf_invoice_number if that meta key already exists on the order. This keeps the WooCommerce PDF Invoices & Packing Slips invoice number in sync with the Dinero invoice number without creating the meta on orders that never had it.

0.5.65

  • Sales push: eliminate 1-øre rounding drift between Woo and Dinero (e.g. Woo 1.759,00 vs Dinero 1.759,01). The invoice payload now sets showLinesInclVat: true and sends every line's baseAmountValue as the incl-moms amount that Woo already stores exactly. No more divide-by-1.25 round-trip on shipping, fees, or product lines, so the Dinero total always matches the Woo total to the øre.

0.5.64

  • Sales push: order fees (Woo get_fees()) are now pushed to Dinero with the same incl-moms→ex-moms conversion as shipping (0.5.63). Any non-Dinero-product line is now consistently treated as gross-in / ex-VAT-out so Dinero's account-1000 default 25% gives the correct customer-facing total. Description-only product fallback lines are unchanged because they already use Woo's ex-VAT calc, which is mathematically identical.

0.5.63

  • Sales push: shipping line was being sent to Dinero as ex-VAT, so Dinero added 25% on top — e.g. Woo shipping 80 (incl. moms) ended up as 100 in Dinero, making the invoice total 20 kr too high. Shipping is now treated as incl. moms: the gross shipping amount (total + total_tax) is divided down to an ex-VAT base before being sent, so the resulting Dinero total matches the Woo total.

0.5.62

  • Audit "Credit & recreate" fix: generate-creditnote was POSTing an empty JSON array [] (PHP empty array json-encodes as array, not object), which Dinero rejected with HTTP 400 "Cannot deserialize ... into type TimestampObject". Now sends an empty JSON object {} instead. Same fix for the book call when no Timestamp is available.

0.5.61

  • Audit "Credit & recreate" fix: the credit note POST was hitting the wrong Dinero endpoint (creditnotes) and returned HTTP 405 Method Not Allowed. Switched to the dedicated POST /v1/{org}/invoices/{guid}/generate-creditnote endpoint, which auto-mirrors the booked invoice as a draft credit note (no manual line-payload reconstruction). Then booked via POST /v1/{org}/sales/creditnotes/{guid}/book.

0.5.60

  • Audit tab: inline JS removed. The AJAX behaviour now lives in assets/audit.js and is properly enqueued via wp_enqueue_script, with wp_localize_script providing the AJAX URL and per-action nonces. Same for the small CSS used by the row buttons (assets/audit.css).
  • Audit AJAX response now reports the NEW Dinero invoice number/GUID created by the recreate, so the success notice tells you exactly which fresh draft replaced the bad one (e.g. "Order #20272 → new draft #6123").

0.5.59

  • Audit tab: "Recreate from Woo" and "Credit & recreate" now run via AJAX instead of full page reloads. The acted-on row is removed in place and the result is shown inline next to the button (or as a top-of-tab notice on success). The cached scan is unchanged so you can power through the whole list without ever leaving the page or re-scanning.

0.5.58

  • Audit tab UX fixes: (1) action results are now shown as a notice at the top of the Audit tab itself (previously the message was set in a transient that no other tab rendered, so clicks on "Credit & recreate" / "Recreate from Woo" looked silent). (2) Acting on a row no longer wipes the full scan cache. Instead, the acted-on order is removed from the cached mismatch list so the rest of the table stays exactly as scanned and you can keep working through the list without re-scanning every time. The "Run audit scan" button is still available when you want a fresh scan.

0.5.57

  • Audit "Credit & recreate" now also handles the case where a wrong payment was already booked against the bad invoice. Before crediting the invoice it lists the invoice's registered payments via GET /invoices/{guid}/payments and deletes each one via DELETE /invoices/{guid}/payments/{paymentGuid}. Then it credits the invoice (referencing it via invoiceGuid), books the credit note, clears the order's wads_dinero_invoice_id / wads_dinero_invoice_number AND wads_dinero_payment_guid / wads_dinero_payment_booked_at meta, and re-pushes via Sales::push_order(). The order can then be re-reconciled normally on the Payments tab against the actual bank line. Order note records both the deleted payment GUIDs and the credit note GUID for traceability.

0.5.56

  • Audit tab: booked / overdue mismatches can now be cleaned up too. The action column shows a "Credit & recreate" button on booked rows that (1) issues a full credit note in Dinero referencing the wrong booked invoice (cancelling it on the books), (2) books that credit note, (3) clears the Woo order's wads_dinero_invoice_id / wads_dinero_invoice_number meta and (4) re-pushes the order via Sales::push_order() to create a fresh correct draft. Confirms before doing anything destructive. Drafts continue to use the lighter "Recreate from Woo" action (no credit note needed).

0.5.55

  • Sales: invoice lines now send the REAL pre-discount unit price as baseAmountValue PLUS the actual per-line discount as a percentage in discount, instead of hardcoding discount: 0 and pushing the already-discounted unit price. This (a) makes the discount visible on the Dinero invoice (so the customer / bookkeeper can see the rebate), and (b) eliminates a per-unit rounding drift that could leave invoice totals 1 øre off when a coupon's discount didn't divide evenly across line quantity.

0.5.54

  • Audit tab: each mismatch row now has an action. For Draft invoices a "Recreate from Woo" button deletes the bad Dinero draft, clears the order's invoice meta and re-pushes the order to Dinero so the new draft matches the current Woo total. For Booked / Overdue invoices the row shows "Fix in Dinero" — booked invoices must be corrected manually (or via a credit note) in Dinero, since the API does not allow editing a booked invoice.

0.5.53

  • Audit tab: scan rewritten to use Dinero's bulk sales list endpoint (100 invoices per API call, all statuses) instead of one GET per order. For ~500 invoices this drops the scan from hundreds of API calls to ~5, so it finishes in seconds and no longer appears to "hang past the total" (the previous total counter was also wrong because it counted postmeta rows directly). Each invoice's ExternalReference is mapped back to the Woo order and totals are compared locally; only mismatches end up in the table.

0.5.52

  • Audit tab: live scan IMPLEMENTED (the 0.5.51 entry below described this prematurely — the action was registered but the handler/UI were not built). Click "Run audit scan" to iterate through every Woo order that has a Dinero invoice GUID, fetch the invoice from Dinero and compare the totals (incl. VAT). The table then shows ONLY mismatches — i.e. invoices that are NOT in agreement with their Woo order, regardless of whether the order was flagged at creation time. The scan is chunked (25 orders per request, auto-continue, with Cancel) and the result is cached in a per-user transient so the page loads instantly after a scan. The old wads_audit_recheck action and handle_audit_recheck method are removed; the new scan supersedes them.

0.5.51

  • Payments tab: bank-match logic also accepts the order number embedded in the bank line description (word-boundary), in addition to the explicit order_id link, so DK Dankort settlement lines and CSV imports without an order_id can still be reconciled correctly when the description carries the order number.
  • Audit tab: groundwork only — admin_post_wads_audit_scan action registered (the live-scan UI/handler ships in 0.5.52).

0.5.50

  • Sales: pre-book guard. After creating a Dinero invoice DRAFT we now compare the Dinero total (incl. VAT) to the Woo order total. If they don't match within 1 øre, the invoice is left as a DRAFT in Dinero (NOT booked). The order is flagged with wads_dinero_invoice_draft_mismatch=1 and an order note is added with both totals. This prevents a wrong invoice from ever reaching the books, which in turn prevents a phantom payment registration on the Payments tab.
  • New "Audit" tab on the Dinero Sync settings page lists all orders flagged with a draft-total mismatch (Woo total, Dinero draft total, diff, invoice number / draft, info). A "Re-check all flagged invoices against Dinero" button re-fetches each flagged invoice from Dinero and clears the flag if the totals now match (an order note is added; booking is left to the operator).

0.5.49

  • Payments tab: hard guard against booking phantom payments. The amount sent to Dinero is now ALWAYS the actual bank line amount, and we explicitly re-verify that bank_amount == invoice_total (±0.005) right before the API call. If they don't match, registration is aborted with a clear message — nothing is booked. This makes it impossible to register a payment that doesn't exist 1:1 in the bank, even if a future code path or stale cache produced a mismatched row. Bank line UPDATE is also now scoped by order_id so the wrong line cannot be marked posted.

0.5.48

  • CRITICAL: Sales sync now sends the per-unit price AFTER coupon/discount on each invoice line (uses Woo get_item_total() instead of get_item_subtotal()). The previous behaviour sent the pre-discount price, so invoices in Dinero were inflated whenever a coupon was applied (e.g. order #20272: invoice 610 vs. actually paid 581.50). New invoices will now match the order total and reconcile correctly against the bank line. Existing inflated invoices need to be corrected manually in Dinero.

0.5.47

  • Payments tab: bank-match logic is now strict on BOTH criteria — the bank line must be tagged with the same Woo order id AND its amount must equal the invoice total (within ±0.005). The previous order-id-only and amount-within-±3-days fallbacks have been removed. Invoices without an exact bank match (order id + amount) are left unresolved and not registered.

0.5.46

  • Payments tab: payment registration is now strictly bank-reconciled. A row is only eligible (and only registered — single button OR bulk page form) when an imported bank line matches the invoice. There is no longer any fallback to payment-method account mapping or to the Woo date_paid: invoices without a bank match are left unresolved with a clear reason. This keeps the bulk page form and the per-row Register button on the exact same procedure.

0.5.45

  • Payments tab: bulk payment registration now runs in chunks of 40 invoices per request to avoid PHP/HTTP timeouts on large selections. The bulk submit seeds a queue (stored in a per-user transient wads_chunk_payments_{user_id}); a status box at the top of the Payments tab shows progress (processed / total — ok / failed) and auto-continues to the next batch after 1 second. A Cancel button stops the run and clears the queue. The single-row 'Register now' button is unchanged.

0.5.44

  • Payments tab: when a row has a bank-line match, payment registration now posts directly to the bank account (default 55000, configurable in General settings as Bank account number) using the bank txn date as PaymentDate. This produces a 1:1 reconciliation entry on the bank account in Dinero.
  • Bank import: matched bank lines are marked posted in the local wads_bank_lines table after a successful payment registration (links order_id, invoice Guid and Dinero payment Guid).
  • Eligibility: rows with a bank match are now eligible even without a payment-method account mapping (the bank account is used directly).

0.5.43

  • Payments tab: new "Bank" column shows a green ✓ Bank badge when an imported bank line matches the invoice (by linked Woo order id, or by amount within ±3 days of date_paid). Tooltip shows bank txn date + amount, and the badge gets a ×N suffix if multiple candidate lines match.

0.5.42

  • Backfill: when an order has no QuickPay transaction (e.g. legacy MobilePay orders), wads_payment_key now falls back to Woo's _payment_method (e.g. mobilepay) so payment mapping still works.

0.5.41

  • Payments tab: backfill missing wads_payment_label / wads_payment_key order meta on the fly when an order has a Dinero invoice but no cached payment metadata. Uses the same logic as the original sync (incl. QuickPay bridge lookup) so values match what sync would have stored.

0.5.40

  • Bank import: imported lines are now listed oldest-first (chronological order matching Dinero's bookkeeping flow).
  • Bank import: new "From / To" date range filter above the table; defaults to the full data range. Filter survives the URL so the view can be bookmarked.
  • Bank import: row count for the current filter is shown above the table.

0.5.39

  • Bank import: PDF importer now keeps only Dankort settlement lines (description starts with DKSSL/DK3DSF and contains the merchant id 3388083). All other lines (Nets batch deposits, Clearhaus, fees, salaries, VISA card transactions, etc.) are skipped to keep the table relevant for Dinero matching.
  • Bank import: skipped lines are reported in the post-import notice (skipped: N).

0.5.38

  • Bank import: fix month being parsed as 00 (the captured DD.MM still contained the dot, so substr picked up .M and cast to int 0). Dates now parse correctly for all months.
  • Bank import: drop continuation lines that look like the standing column header (dato, +, -, Indsat, H{vet, etc.) so they no longer get appended to the previous transaction description.
  • Bank import: action: "Delete all bank lines" — use this to wipe the broken 0.5.37 import before re-importing with 0.5.38.

0.5.37

  • Bank import: PDF parser now matches the actual Danske Bank Business One text-extraction format — description follows date2 with no space, amount and balance use a TAB separator and the sign is attached to the number (530,00+).
  • Bank import: dramatically expanded header/footer filter (page numbers, barcodes, IBAN/CVR/SWIFT lines, garbled Danish characters from the PDF font) so cross-page header noise is no longer appended as continuation text to the previous transaction.
  • Bank import: "Udskriftsperiode" year extraction now tolerates the missing whitespace in the extracted PDF text.

0.5.36

  • Bank import: PDF parser now tolerates the extra whitespace that PDF text extraction commonly inserts inside dates (02. 01) and amounts (1. 229, 68).
  • Bank import: new "Debug: dump first PDF text" upload form that uploads a single PDF, runs it through the parser, and shows the first 80 raw lines + the regex match attempts so we can iterate quickly when a new statement format appears.

0.5.35

  • Bank import: surface importer errors and file count in the admin notice (previously a missing PDF parser library, an unreadable ZIP entry, etc. resulted in a silent "0 rows" notice).
  • Bank import: notice now shows the count of files processed when uploading a ZIP archive.
  • Bank import: errors are persisted in wads_bank_last_errors for one redirect cycle so they survive the post/redirect/get without bloating the URL.

0.5.34

  • Bank import (Phase 1): new "Bank import" tab. Imports raw bank statement lines into a new wads_bank_lines table for later matching against Woo orders and posting to Dinero ledger drafts.
  • Bank import: accepts standard 3-column UTF-8 CSV (date, description, amount) with auto-detected separator and Danish/EU number format.
  • Bank import: accepts Danske Bank "Business One" PDF statements (text-based, single PDF or ZIP of multiple PDFs). Year is taken from the statement period header. Multi-line entry context (Clearhaus REFNO, VISA Beløb/Land, etc.) is concatenated into the description so revision trail is preserved.
  • Bank import: idempotent — re-uploading the same file does not create duplicates (UNIQUE on line_hash).
  • Note: enabling PDF import requires running composer install in the plugin directory once to install smalot/pdfparser.

0.5.33

  • Reconciliation: revert the bogus pagination on /v1/{org}/entries (the endpoint actually returns a flat array, no Collection/Pagination wrapper). Now uses the response array directly with a Collection fallback for safety.
  • Reconciliation: surface errors and counts in an admin notice on the Reconcile tab. After Sync/Backfill the page shows fetched / upserted / skipped + any error message from the Dinero API and the active accounts list. Previously a silent 0-entry result looked identical to a permission/config error.
  • Reconciliation: cache last error in wads_ledger_last_error so the Reconcile stats box can show it persistently.

0.5.32

  • Reconciliation fix: Ledger\Puller now reads the Collection array from Dinero responses (previously iterated the wrapper object and found 0 entries) and paginates with page / pageSize=1000 until the collection is shorter than the page size. Backfill and incremental sync now actually persist rows.

0.5.31

  • Reconciliation (step 3/3): new "Reconcile" tab on Settings (page=wads-settings&tab=reconcile). Lists cached Dankort settlement rows (Description starts with DK and contains 3388083), shows totals + last-sync timestamp, and offers two buttons: "Sync ledger now" (incremental) and "Backfill year" (full year via /v1/{org}/entries).
  • For each settlement row the tab finds Woo orders whose total equals the settlement amount within ±7 days of the entry date and lists them as suggested matches with a deep-link to the order.
  • Manual override: each row has an inline state selector (unmatched / matched / manual / ignored) so the operator can drive the workflow without the Dinero API supporting reconciliation directly. State is persisted on the local row.

0.5.30

  • Reconciliation (step 2/3): new Ledger\Puller. initial_for_year(year, accounts) does a full backfill of one accounting year via GET /v1/{org}/entries; incremental(accounts) does a windowed sync via GET /v1/{org}/entries/changes (capped at the Dinero 31-day limit). Only entries on the configured deposit/clearing accounts are stored; everything else is skipped client-side. Existing rows are upserted without clobbering local match state.
  • Hourly WP-Cron event wads_ledger_cron calls Plugin::run_ledger_cron() which pulls incrementally for the default deposit account + every account referenced in the Payments mapping. Filterable via wads_ledger_accounts. Scheduled on activation, unscheduled on deactivation.
  • Dankort settlement detection: rows whose Description starts with DK AND contains 3388083 are flagged with is_dk_settlement = 1 for fast retrieval in the upcoming reconcile UI.

0.5.29

  • Reconciliation (step 1/3): new local DB table {prefix}wads_ledger_entries for caching Dinero ledger entries (posteringer). Created on activation, auto-upgraded on boot via Ledger\Schema::maybe_upgrade(). Schema is keyed on Dinero's EntryGuid and indexed for the typical reconcile queries: (account_number, entry_date), (is_dk_settlement, match_state), (amount, entry_date), order_id, invoice_guid. No data is pulled yet — puller and UI come in 0.5.30 / 0.5.31.

0.5.28

  • Payments tab: payment Description sent to Dinero now starts with the order number, e.g. SF21174 MobilePay (Dankort **** 0823) instead of Betaling: MobilePay (Dankort **** 0823).

0.5.26

  • Payments mapping help text: removed the non-existing mobilepay-online example. MobilePay Online charges the underlying card via the merchant's normal acquirer (clearhaus/teller/…), so MobilePay payments map by acquirer like any other card.

0.5.25

  • Sales / Payments: prefer the new woo-wa-quickpay bridge plugin for QuickPay transaction lookup. When the bridge is active, brand/acquirer/last4 come from its cached Transaction DTO, the human label is built the same way, and the bridge's normalized key (e.g. dankort, teller, clearhaus, mobilepay-online, applepay) is stored in order meta wads_payment_key.
  • Payments tab: account-mapping rules now match the normalized key first (exact, case-insensitive). Falls back to the existing substring match on the human label. Lets you write clean rules like dankort=58410, teller=58420, clearhaus=58430 instead of brittle substring matches.
  • Falls back transparently to the legacy direct WC_QuickPay_API_Payment lookup when the bridge plugin is not installed/active.

0.5.24

  • New "Payments" tab: lists Dinero invoices that are still unpaid (statusFilter Booked,Overdue) matched against the originating Woo order via ExternalReference. Shows the payment method actually used (QuickPay card brand, MobilePay, BACS, COD, etc.), the Woo date_paid, and the deposit account that will be used.
  • New per-payment-method account mapping (Settings > Payments). One line per match=account (case-insensitive substring match against the payment label, first match wins) plus a default fallback account number.
  • Register payment in Dinero from the Payments tab — single-row button or bulk "Register all". Calls POST /v1/{org}/invoices/{guid}/payments with PaymentDate = Woo date_paid, DepositAccountNumber from the mapping, Amount = invoice TotalInclVat. Filter: only Woo orders that already have date_paid set.

0.5.23

  • Sales (chunked): fix "0 / 0" total/processed. The base args contained return => 'objects' and the override return => 'ids' (used for the cheap count query) was silently dropped because PHP's + operator keeps the left-hand value on duplicate keys. Switched both queries to array_merge so paginate + return => ids actually take effect, and the per-page query gets a clean return => objects override.

0.5.22

  • Sales: ALL sales sync runs are now chunked (10 orders per HTTP request) — month rows, date-range, and the legacy "Sync ALL eligible sales" button no longer time out on large months. New Sales::sync_range_batch() + handle_chunk_sales runner with auto-continue UI on the Sales tab (same pattern as products/customers chunked sync, with Cancel button).
  • Admin: the legacy "Sync now" buttons for products/customers/sales no longer call sync_all() inline (which timed out). They now seed the matching chunked runner and redirect to the tab where auto-continue takes over.

0.5.21

  • Sales: QuickPay payment-detail lookup now uses the correct API class WC_QuickPay_API_Payment (the previous WC_QuickPay_API_Transaction is the abstract base and never hit /payments/{id}, so the comment kept saying just "Online betaling"). Failures are no longer swallowed: missing class, lookup exception, or empty result are written as order notes so the cause is visible. wa-order-premium does not expose card/brand data (only amounts), so QuickPay's own API is the only useful source.

0.5.20

  • Sales: capture the human-readable invoice Number from the booking response (and as fallback via GET /invoices/{guid}). Drafts have no number assigned by Dinero — the number is only generated at booking time, so v0.5.19 saved an empty wads_dinero_invoice_number. Now stored correctly and written to the order note.

0.5.19

  • Sales/Credits: Dinero response Guid/Number/TimeStamp lookup is now case-insensitive (Guid and guid). Earlier versions only checked PascalCase, so when Dinero replied in camelCase nothing was saved on the order — invoices stayed as drafts and no wads_dinero_invoice_id ended up on the order. If no Guid is returned at all the raw response is now logged as an order note for diagnosis.

0.5.18

  • Sales/Credits: invoices and credit notes are now ALWAYS booked (approved) in Dinero immediately after creation. The previous "auto book" toggle was misleading and left invoices as drafts. Booking failures are written as an order note.

0.5.17

  • Sales: Dinero invoice comment now starts with the Woo order number ("Ordre: SF12345") and, when QuickPay is the gateway, includes the actual payment instrument detected (e.g. "Visa 1234", "MobilePay", "Apple Pay (Visa 1234)"). Falls back gracefully to just the gateway title when QuickPay metadata is unavailable.

0.5.16

  • Sales: before pushing an invoice, ensure each referenced Dinero product's name matches the Woo product's name (the Woo order "wins"). Differences only in case, leading/trailing whitespace, NBSP, doubled spaces or ","/"." are ignored — only meaningful changes trigger a Dinero product update. Best-effort; failures don't block the invoice.

0.5.15

  • Sales: when an order line has a known Dinero ProductGuid, no longer send description/comments (Dinero rejects the invoice with "The product line description can not be edited, when a ProductGuid is set."). Only free-text product lines (without a ProductGuid) carry a description.

0.5.14

  • Dinero API error message now also captures Errors, ErrorCode, CorrelationId and the full JSON body (truncated to 1000 chars) so we can see exactly what was rejected.

0.5.13

  • Dinero API errors now include the actual response detail (Message / ModelState / errors) instead of just "HTTP 400". Makes it possible to see what Dinero rejected.

0.5.12

  • Product queries (duplicate scan, sync, dashboard counters, unsynced list) now only consider publish products. Drafts and privately-published items are no longer included in the duplicate check or sync.

0.5.11

  • Products tab: new "Sync pending products only" button — chunked sync (25/batch) of products that have no Dinero GUID yet, excluding duplicate-blocked SKUs and any products that fail in the same run (so it terminates cleanly). Auto-continues like the full sync; Dinero 429 retries are handled by the API client.

0.5.10

  • Dinero API client now handles HTTP 429 (rate limit): respects Retry-After header, otherwise backs off 2/4/8s, up to 3 retries per request.

0.5.9

  • Chunked sync (products + customers) now auto-continues: when the in-progress notice is shown, the next batch is submitted automatically after 1 second. Click Cancel to stop. No more manual "Continue" clicking through hundreds of batches.

0.5.8

  • Products + Customers chunked sync UI: "Sync ALL ..." button is now ALWAYS visible. Previously a stale "in progress" transient (e.g. from a 504-timeout on the first batch) would replace the start button with only a Continue/Cancel notice, leaving no way to restart from scratch. The status notice now appears in addition to the start button.

0.5.7

  • Products tab: removed the automatic duplicate-SKU rescan that ran on every page view (root cause of the 504 timeout). Replaced with a manual "Rescan duplicate SKUs" button. The duplicate list now uses the cached result from the last sync run / manual rescan.

0.5.6

  • Hotfix: restored a missing PHPDoc opener that caused a fatal parse error on SettingsPage.php introduced during the 0.5.5 work.
  • Sales tab: new "Sync sales by date range" form (from / to date pickers) for syncing an arbitrary period.
  • Sales tab: each row in the pending-orders list now has a "Sync" button to push that single order to Dinero.

0.5.5

  • Sales: invoice external reference is now SF<order-number> (kept as-is if the order number already starts with SF). Previously WC-<order-id>.
  • Sales: invoice comment now includes the WooCommerce payment method title (e.g. "Betaling: MobilePay") in addition to the payment reference.
  • Sales: when a registered customer has no billing address on the WP profile, the order is still pushed to Dinero — the contact is created from the order's own billing details (same logic as guests). All orders end up in Dinero.
  • Products tab: "Sync ALL products" is now chunked (25 products per HTTP request, with operator-driven Continue button) to avoid 504 timeouts on large catalogs.

0.5.4

  • Sales status filter changed: now includes wc-completed, wc-pickup, wc-delivered, wc-ready-pickup, wc-partial-shipped. wc-processing is no longer treated as a sale. Applies to dashboard counter, monthly breakdown, unsynced-orders list, full sync, range sync and the WooCommerce status-change auto-sync hooks.

0.5.3

  • Customers dashboard counter "Users in WP" now only counts users with a billing address (street + postcode + city), matching the scope of what is actually synced.

0.5.2

  • "Registered customers not yet synced" list now only includes WP users that have a billing address (street, postcode, city). Users with email only are no longer shown — they are skipped by the sync anyway.

0.5.1

  • Dinero orphan scan ("Scan Dinero for contacts without address") now ignores contacts that have no email address. Such contacts were not created by WooCommerce and must not be deleted by this tool.

0.5.0

  • Sync ALL customers is now chunked (20 users per HTTP request, with server-side redirect between batches) to avoid 504 timeouts on large user bases. Progress is shown on the Customers tab while running.
  • Customers: WP users with no billing address (no street, zip and city) are now skipped — they have no orders and are not interesting for Dinero. Counted as "skipped" in the result.
  • New section on Customers tab: "Dinero contacts without address" — scan Dinero for contacts that have no address fields and bulk-delete them.

0.4.4

  • Customers: phone numbers are normalized before sending to Dinero — the Danish country prefix +45 (and 0045) is stripped, and all whitespace is removed. Empty values are sent as null.
  • Customers: payment condition now defaults to NettoCash (Netto kontant) instead of Dinero's voucher-settings default of Netto 8 days.

0.4.3

  • Customers tab: "Contacts already synced to Dinero" list now has checkboxes and a "Re-sync selected to Dinero" bulk action. Use this to push corrected payloads (person/company, attPerson, etc.) to existing Dinero contacts via PUT — no duplicates created.

0.4.2

  • Fix: company contacts now map correctly to Dinero. Dinero's ContactCreateModel has no companyName field; for companies the company name belongs in Name and the person belongs in AttPerson. We now send name = company name and attPerson = person name when the contact is a company. Private persons are unchanged (name = person, no attPerson).

0.4.1

  • Fix: Dinero ContactGuid was never persisted because the create-response field is named Guid (per CreatedResponseModel), not ContactGuid. Customers were therefore re-created on every sync run. The sync now reads Guid (with fallback to ContactGuid/contactGuid/guid) and stores it as the customer/order address meta wads_dinero_id.

0.4.0

  • Customers: Dinero person/company distinction now respected. A Dinero contact is created as a company only if WooCommerce billing_company is set AND differs from the customer name; otherwise it is created as a private person.
  • Customers: WooCommerce autofill artefact (billing_company == "First Last") is now ignored, treated as a private person.
  • Customers: required Dinero schema fields IsMember and UseCvr are now sent on every contact create/update.
  • Customers tab: new "Synced contacts" list shows all WP users + guest emails that have a Dinero ContactGuid, including type (Person/Company).

0.3.0

  • Sales tab: month-by-month overview from January 2025 to current month, with per-month "Sync this month" button.
  • Sales sync now stores Dinero invoice number on the WooCommerce order (post meta wads_dinero_invoice_number) and adds an order note.
  • Invoice payload now includes the WooCommerce order's payment reference (transaction id) as Dinero paymentConditionPaymentReference.
  • Dinero Client refactored to use OAuth2 Bearer tokens via Visma Connect (was still on stale X-API-KEY).

0.2.0

  • Customers tab: list ALL registered customers (paginated) with sync status and bulk-sync checkboxes.
  • Customers tab: list ALL guest buyers detected from orders (grouped by billing email) with bulk sync to Dinero contacts.
  • Customers sync now also creates Dinero contacts for guests directly from the admin UI (not only as a side-effect of invoice creation).

0.1.0

  • Initial scaffold: bootstrap, PSR-4, settings, Dinero API-key client, REST + WooCommerce hooks.
  • Authentication: API key only (Bearer header) - Dinero does not support OAuth.
© 2026 Webalive - live website