# Changelog
All notable changes to this project will be documented in this file.

## [4.5.5] - 22-09-2026
 - fixed: the schema migration converges again on WordPress 6.8.x and older (Sentry CARTBOSS-WP-PLUGIN-E, one store, on its first request after updating to 4.5.4). Up to WP 6.8.x, dbDelta() compares the type a schema declares against the lowercase type `DESCRIBE` reports CASE-SENSITIVELY, and every field in every `schema_sql()` was declared uppercase (`VARCHAR(128)`, `BIGINT(20) UNSIGNED`), so an existing table planned one `Changed type of …` ALTER per column — non-additive lines the D5a guard refuses by design. The first unapplied step therefore failed, `cb_schema_version` never moved, and `database_migration_failed` was queued again on every retry. WP 6.9.0 made the comparison case-insensitive, which is why the dev/CI stack (WP 7.1) never reproduced it. All three schemas now declare lowercase field types — WordPress's documented dbDelta() requirement — and a store wedged on that step converges on the next request after updating. The executed DDL, indexes, `PRIMARY KEY` spacing and the parsed column/index names are unchanged; `test_schema_sql_field_types_are_lowercase` pins every schema_sql() field type as lowercase, and the whole `migrate()` was re-run against WP 6.8.2's dbDelta() on a real MariaDB (the old uppercase DDL is refused, the fixed schemas plan nothing, `cb_schema_version` 0 → 3).

## [4.5.4] - 21-09-2026
 - fixed: the shutdown fatal handler no longer reports the plugin's own files being replaced under a live request (Sentry CARTBOSS-WP-PLUGIN-B/-C, one store, 1 s apart). Installing, updating or re-uploading the plugin swaps its files under requests that are already running, and a request that lands mid-copy fatals either on a missing required file (`Failed opening required 'includes/class-cartboss-loader.php'` — `load_dependencies()` builds the path with `require_once`) or on a class that was never included (`Class "Cartboss_Capture_Result" not found` — `cartboss_include()` silently skips missing files, and `class-cartboss-ajax-checkout.php` resolves `Cartboss_Capture_Result::REASON_*` in a `private const` at include time). `cartboss_fatal_mid_update()` reads two facts from disk before `cartboss_handle_fatal()` writes anything: a required file named by the message is missing right now, or the plugin folder, `includes/`, `classes/` or the plugins folder above it changed entries at or after `REQUEST_TIME_FLOAT` minus a 2 s slack for `filemtime`'s whole-second resolution (WordPress's upgrader both deletes-and-re-extracts and renames a new copy into place, so either fact catches the swap). A genuine defect in a settled install fails both and is still recorded and forwarded; nothing else in the record path, the queue or the envelope changes. The fatal itself — one request landing mid-copy — is inherent to PHP plugin updates and cannot be prevented from our side; this only keeps it out of the error feed.

## [4.5.3] - 10-09-2026
 - fixed: the checkout capture endpoint no longer answers `{"success":true}` after its own handler threw. `Cartboss_Ajax_Checkout::handle_request()` caught the `Throwable`, logged it through `Cartboss_Logger`, and then fell through to `wp_send_json_success()`, so a capture that did not complete — the order row and the freshness marker are written BEFORE dispatch, and a post-dispatch hook can throw after delivery, so what it left behind is not knowable at the catch — was indistinguishable in the browser from a clean one — which is exactly how a silent dispatch failure survives a front-end test. It now returns `wp_send_json_error(array('message' => "Capture failed"), 500)`. 500 and not 403 on purpose: `public/js/cartboss-checkout2.js` disables capture for the rest of the page load on 403, and an internal failure is no reason to stop trying on the shopper's later field edits. The log line is unchanged, and every other envelope — admin, nonce, cart, token, phone, origin and admission — is byte-identical to 4.5.2.

## [4.5.2] - 08-09-2026
 - fixed: a legitimate shopper no longer spends the 4.5.1 three-phones-per-hour allowance while typing (#50). A new `public/js/cartboss-phone-dirty.js`, loaded on the checkout page in both checkout modes, marks the phone field as being edited on two tab-scoped carriers — a hidden `cartboss_phone_dirty` input inside `form.checkout` (WooCommerce serialises it into `post_data`) and an `X-CartBoss-Phone-Dirty` header added to every Store API request through a `wp.apiFetch` middleware. While it is set the capture script sends nothing — address changes included, since they carry the half-typed phone — and the intermediate server doors (classic `woocommerce_checkout_update_order_review`, Store API `cart/update-customer` and the checkout `PATCH`) return SKIPPED before identity, nonce or admission, writing nothing at all, not even a log line. The edit settles on focus-out or after ten quiet seconds and then captures once, with the final value. The marker can only ever suppress: it is never read on the wc-ajax client door, the classic checkout POST or the Store API place-order POST, so forging it gains an attacker no admission, no write and no SMS.
 - fixed: the three-phone set is country-aware where that is safe — `040 123 456`, `+386 40 123 456`, `0038640123456` and `38640123456` are now one member instead of up to four, so a browser or plugin that reformats the field on blur no longer costs the shopper a slot. The rewrite runs only for countries that drop the national trunk `0` internationally (`Cartboss_Constants::CB_TRUNK_ZERO_COUNTRIES`, filter `cartboss_phone_trunk_zero_countries`). Italy is deliberately excluded: its leading `0` is part of the number, and rewriting it would hash a landline and a mobile to the same key. Only the hashed set key changes, never the phone that is stored or texted.
 - fixed: on the block checkout the settle now re-pushes the customer itself (#50). WooCommerce pushes customer data on its own ~1.5 s debounce; that push lands while the phone marker is up and is correctly suppressed, and on the block checkout — which dequeues the CartBoss capture script — nothing pushed again unless the shopper edited another field or placed the order, so a shopper who typed the phone LAST and abandoned was not captured at all. `cartboss-phone-dirty.js` now reads `wp.data.select('wc/store/cart').getCustomerData()` once the marker is down and sends one `wp.apiFetch` `POST /wc/store/v1/cart/update-customer` (`billing_address` + `shipping_address`), so WooCommerce's own Nonce and Cart-Token middleware apply and the observer admits it. One push per settle; the route merges over the stored customer, so nothing is blanked. Guarded on `wp.data` and the selector — where either is missing the settle just clears the marker, which is 4.5.1 behaviour.
 - fixed: the client-side length gate counts digits (threshold 8) instead of raw characters (threshold 9). Eight-digit national numbers — Denmark, Norway and others — are captured while the shopper types instead of being silently skipped, and an input mask that reaches nine characters holding six digits no longer claims a phone slot.
 - fixed: a store that upgrades mid-hour keeps its existing phone-set rows (#50). 4.5.1 keyed a member row by the raw digits; 4.5.2 keys it by the country-aware form. Under the identity lock, a live 4.5.1 row for the same destination is now recognised as that member and MIGRATED onto the country-aware key: the raw row is deleted only while it still holds the value that was read, and the new row is inserted in its place, so the member count never moves and the row cannot be matched twice. Without it a shopper's own number could spend a second slot, or be refused outright once the set was full, for a number that was already a member. Both 4.5.1 spellings are looked up, so a shopper who typed the national form before the upgrade and the international form after still lands on their own row. The replacement row is inserted and proven BEFORE the raw row is given up, and the raw row's slot is reserved for it meanwhile, so a write that does not land can never leave a shopper's number tracked nowhere: the raw row is kept and the shopper is admitted on it instead. Only a genuine pre-upgrade row qualifies — one last accepted before the one-time `cartboss_capture_v2_since` stamp, which this site writes at the top of its very first 4.5.2 phone-set decision, before the member rows are read and so before any member is admitted on 4.5.2 — so a raw-digit row that 4.5.2 wrote itself, for Italy or any other country off the trunk-zero list, is its own destination and can never be borrowed to widen the three-phone allowance. That includes a row 4.5.2 merely refreshed: stamping at the member insert alone left a refresh-only first capture unstamped, and the next capture then treated the row it had just refreshed as pre-upgrade. A site whose stamp cannot be written and read back refuses the capture (429, retry in a second) and writes nothing else, rather than reading every row it owns as pre-upgrade.
 - fixed: the country a phone is keyed by comes from that phone's own address side (#50). A shipping phone is read with the shipping country and a billing phone with the billing country; the opposite side is no longer a fallback, so a number shipped to Denmark on a checkout whose billing address says SI is no longer keyed with the Slovenian calling code. The one fallback for a missing country is still the store's base country.
 - fixed: a mid-edit request now writes nothing on the whole door, not just inside capture() (#50). The phone-dirty gate runs before the Store API door assembles its field map — assembly resolves the marketing consent through WooCommerce's semi-internal Blocks container, and on a WooCommerce that cannot supply it that logged through a transient-throttled logger — and before the classic door reports payload drift.
 - fixed: the checkout capture script re-reads the form at send time and listens through the document (#50). WooCommerce replaces the checkout form on `update_checkout` and the block checkout re-renders it; the script used to hold element references from page load, so a replaced input meant a stale value was sent, or the shopper's number was never captured at all. The form is now also re-read INSIDE the three-second send timer, immediately before the request, and the digit gate applies to what is read there — a number that was replaced, rewritten or removed during those three seconds is never the number that reaches the server, and a field that is gone or too short cancels the send outright.
 - changed: the Store API observers now name themselves separately to the `cartboss_server_capture_enabled` kill switch — `store_api_customer`, `store_api_draft` and `store_api_order` — because only the last of the three is the shopper placing their order. **A filter that matched the old single `'store_api'` value must be updated.**

## [4.5.1] - 07-09-2026
 - fixed: abandoned-cart capture is hardened against nonce replay (#50) — the capture nonce is now minted per WooCommerce identity (`cartboss-capture-<identity>`) instead of one shared `ajax-nonce`, every capture door (wc-ajax, the classic order-review fallback, the Store API observers) requires a WooCommerce session the client cannot mint, and one identity may start at most 3 distinct-phone carts per hour (plus 30/min and 60/hour overall). A refused capture writes nothing at all: no transient, no option, no queue entry, no log line. Limits are overridable via the `cartboss_ajax_rate_limit` filter with the scopes `wcs`, `wcs_hour` and `phones`. The capture nonce is also minted on the checkout page only, so a full-page cache plugin storing a product page cannot hand one visitor's nonce to the next; and a phone the plugin could never text (empty or malformed) no longer counts against the three-phone allowance.
 - improved: any page that mints a capture nonce now answers `Cache-Control: no-cache` and sets `DONOTCACHEPAGE`, so a cache plugin cannot store a page carrying a nonce that belongs to one shopper. **After updating, purge your page cache and any CDN cache** — pages cached before the update keep serving the old script until they are purged, and captures from those pages are refused (nothing breaks; capture resumes on the next fresh page load).
 - improved: `cartboss_is_capture_checkout_page` filter — on checkout builders where WooCommerce's own `is_checkout()` is false, return `true` for the builder's page to restore live capture while the shopper types. Even without it, capture still happens when the address changes and when the order is submitted.
 - improved: abandoned phone-set rows are swept in the background — a bounded scan of at most 200 rows at a time, run at most once every five minutes on live traffic plus once a day by cron — so a burst of bot traffic cannot leave rows behind in the options table.

## [4.5.0] - 28-08-2026
 - added: local error reporting — plugin errors are recorded in a dedicated table ({prefix}cb_errors), masked, and flushed to CartBoss once an hour by wp_cron instead of during the shopper's request; the old synchronous /log call is gone (it could hold a checkout for up to 15 s on a slow connection)
 - added: `wp cartboss errors list|flush|purge` and a Site Health check for the error-report table
 - added: opt-out under CartBoss → Settings → "Send error reports to CartBoss" (default on); a fatal inside the plugin's own files is captured by a shutdown handler that needs no plugin class loaded
 - improved: log classification — every reported error carries a stable type/stage so recurring failures group instead of drowning in noise
 - fixed: a dormant duplicate copy of the plugin now declares WooCommerce feature compatibility before bailing out, so it no longer adds WooCommerce's "incompatible plugins" banner on top of our own duplicate notice; the notice itself gained a one-click deactivate button that targets the OLDER of the two copies (folder name alone decides which copy WordPress loads)
 - fixed: build.sh now gates the distributable zip (.github/scripts/verify-package.sh) — a zip without the `cartboss/` root folder makes WordPress install a SECOND plugin folder named after the zip file instead of upgrading the existing one
 - fixed: admin menu-marker selectors are escaped at the echo site so Plugin Check's EscapeOutput sniff is clean (#49)

## [4.4.0] - 20-08-2026
 - added: full support for the WooCommerce Cart & Checkout Blocks — abandoned-cart capture works the same on the block-based and classic (shortcode) checkout, incl. the marketing-consent checkbox (WC 8.9+) and the cart_checkout_blocks compatibility declaration (#1)
 - added: GDPR tooling — abandoned-cart data plugs into WordPress's built-in privacy exporter/eraser (Tools → Export/Erase Personal Data), incl. derived-phone lookup and CartBoss order-meta erasure (#2)
 - added: Site Health checks — API key, cron heartbeat, last-ping and purchase-reconciliation status under Tools → Site Health (#5)
 - added: WP-CLI commands: wp cartboss status|ping|purge (#6)
 - added: purchase reconciliation job — an hourly best-effort sweep (7-day window, batched, capped retries) detects purchases whose WooCommerce hooks never fired and reports them after the fact (#11)
 - added: server-side checkout capture fallback — carts are captured during order-review refresh/order submission even when browser-side capture is blocked (#12)
 - improved: resilient background delivery — API events are queued (Action Scheduler) and retried with backoff (up to 5 attempts) instead of failing on the first hiccup; synchronous fallback when the scheduler is unavailable (#4)
 - improved: logging moved to WooCommerce logs (WooCommerce → Status → Logs, channel "cartboss") with PII-scrubbed messages; no more raw error_log noise (#3)
 - improved: safer database migrations — versioned dbDelta migrations (cb_schema_version); the destructive "drop and recreate on unknown column" self-heal is removed. The 4.4.0 migration is fully additive: existing stored carts and tokens are preserved (#7)
 - improved: runtime compatibility preflight — clean admin notice and graceful no-op instead of a fatal when the WP/PHP/WooCommerce floors aren't met (#10)
 - improved: redesigned the CartBoss settings screen — at-a-glance connection status, clearer grouped options, rewritten admin notices; dropped bundled Bootstrap/jQuery-plugin assets (~62KB lighter)
 - improved: purchase reporting honours the ignored-roles setting — orders owned by an excluded role (staff/test accounts) are no longer reported (#29)
 - improved: a second installed copy of the plugin (e.g. a hand-uploaded zip beside the wp.org install) shows an admin notice instead of a fatal redeclare
 - security: rate limits and payload caps on the public wc-ajax endpoints; stricter session-token and cross-origin checks. Per-IP limits are sized as an abuse backstop (1000/min) so stores behind a proxy that hides the real client IP are never throttled; a denial is logged once per IP per minute to the cartboss WC log (#13)
 - security: hardened Store API request detection against session-fixation via path spoofing
 - security: the public cartboss-reset-session route (an already-used SMS link lands here) now acts only when the link carries a consumed session token; a stray or hostile link no longer empties a visitor's cart
 - fixed: a cb__discount link/cookie on a site with no API key saved no longer risks a front-end fatal (coupon decode bails on an empty secret)
 - improved: the queue's envelope reconciler pages through the whole ledger (ordered, cursor) instead of one unordered batch of 200
 - fixed: uninstall now also removes the 4.4.0 capture/rate-limit transients and queue envelopes (per-order metadata WooCommerce owns is handled via the privacy eraser instead)
 - dev: PHPUnit suite around the security boundaries (670+ tests) and a GitHub Actions pipeline (lint matrix PHP 7.4–8.5, unit matrix, e2e, Plugin Check with zero-ERROR gate, audit, WP/WC/PHP currency detector) run on every change (#8, #9, #14, #21, #22)
 - build: the plugin zip's root folder is now cartboss/ (the wp.org slug), so hand-uploaded zips upgrade in place instead of installing a second copy
 - requirements: unchanged — WordPress 6.0+, PHP 7.4+, WooCommerce 8.0+

## [4.3.0] - 20-07-2026
 - security: hardened entire plugin — strict session-token validation, constant-time coupon signature checks (hash_equals), same-origin checkout redirect validation (scheme/host/port), safe redirects everywhere, HttpOnly + SameSite=Lax cookies, direct-access guards on all files, full input unslash/sanitize + output escaping pass
 - security: environment detection no longer trusts the client Host header
 - fixed: deactivating the plugin no longer deletes settings, stored carts, or the API key — cleanup now happens only on uninstall (multisite-aware)
 - fixed: settings save no longer adds backslashes to quotes; redirect-without-exit; admin assets now load only on the CartBoss screen; update notice uses proper version comparison
 - fixed: cart restore links now work on sites with plain permalinks
 - improved: bundled libraries are namespace-prefixed (CartBoss\Vendor\*) — no more conflicts with other plugins shipping Symfony
 - improved: admin UI fully translatable (text domain: cartboss), translation template shipped
 - improved: WooCommerce dependency guard (clean notice instead of fatal when WooCommerce is missing), Requires Plugins header
 - improved: declared incompatibility with block checkout (classic checkout integration; block support tracked for a future release)
 - deps: updated all bundled dependencies; composer audit clean
 - requirements: now requires WordPress 6.0+, PHP 7.4+, WooCommerce 8.0+

## [4.1.5] - 03-12-2025
 - security improvements: proper output escaping in admin
 - fixed exception leak in AJAX handlers
 - improved input validation

## [4.1.4] - 03-12-2025
 - fixed PHP 8+ compatibility issue with League URI library
 - minimum PHP version now 7.4

## [4.1.3] - 16-04-2025
 - security fixes

## [4.1.2] - 17-12-2024
 - now CartBoss works with Klaviyo and other platforms

## [4.1.0] - 26-08-2024
 - HPOS compatible

## [4.0.6] - 29-02-2024
 - improved session handling


## [4.0.5] - 27-02-2024
 - js fixes
 
## [4.0.4] - 07-12-2023
 - minor fixes
 
## [4.0.2] - 13-06-2023
 - backend event sending fixed

## [4.0.1] - 13-06-2023
 - changed the way we are sending events

## [3.7.0] - 04-05-2023
 - session fix

## [3.6.7] - 04-05-2023
 - cookies fix

## [3.6.5] - 02-03-2023
 - meta data fix

## [3.6.1] - 04-01-2023
 - rewritten session detection
 - better duplicate order detection
 - speed improvements

## [3.5.5] - 19-11-2022
 - limit discount for 1 use

### Changed
- speedy improvements

## [3.5.4] - 19-10-2022

### Changed
- speedy improvements

## [3.5.1] - 11-07-2022

### Changed
- php 7.2+ required

## [3.5.0] - 22-06-2022

### Changed
- improved speed and stability
- url signature check added for better security

## [3.4.9] - 16-06-2022

### Changed
- improved speed and stability
- duplicate order detection added

## [3.4.7] - 21-04-2022

### Changed
- improved speed and stability
- new version checker added
- fix for backend approved orders purchase reporting
- new admin icon :)

## [3.4.2] - 7-01-2022

### Changed
- checkoutWC workarounds


## [3.4.1] - 30-12-2021

### Changed
- exclude admin/woo manager actions from being processed

## [3.3.7] - 3-12-2021

### Changed
- improved phone capturing code

## [3.3.6] - 2-12-2021

### Changed
- various performance improvements

## [3.3.5] - 25-11-2021

### Changed
- fixed cron handled orders
- better phone capturing
- speed improvements :)


## [3.3.3] - 08-11-2021

### Changed
- fix order status check
- don't max_prioritize our hooks
- postcode field capturing fixed


## [3.3.2] - 01-11-2021

### Changed
- checkout JS improvements to work with WC blocks checkout

## [3.3.1] - 24-10-2021

### Changed
- replaced guzzle library with internal solution

## [3.2.8] - 19-10-2021

### Changed
- ping api call fix

## [3.2.7] - 13-10-2021

### Changed
- custom discount support added
- discount creation optimizations
- removed deprecated function calls

## [3.2.6] - 22-09-2021

### Changed
- WPML support added

## [3.2.4] - 21-09-2021

### Changed
- fixed cron scheduling issues

## [3.1.8] - 07-09-2021

### Changed
- complete plugin rewrite
- support for UTM
- support for win-back sms messages
- improved woocommerce hook support


## [2.0.8] - 09-07-2021

### Changed
- cartboss_cb_restore_contact rewrite

## [2.0.7] - 08-07-2021

### Changed
- Added support for Sentry

## [2.0.6] - 07-07-2021

### Changed
- Plugin takes home url instead of site url for base address