=== Careback: earlier releases ===

The changes of every release before 0.80.0, newest first. The changelog of later releases is in readme.txt.

= 0.79.0 =
* The deadline warnings became one daily summary. They used to be one email per request and per kind, which on a busy week meant an inbox of single-line letters nobody could triage as a queue. There is now one letter a day, at an hour the shop sets (11:00 by default), grouped overdue → period expired while withheld → due today → due soon, each request separated from the next, with a plain-text alternative for clients that cannot show the formatted one.
* The summary is a snapshot rather than a list of changes. A refund past its date appears in every letter until somebody pays it — previously a request was reported once and then never again, so anything not acted on that day fell out of sight. It also means nothing is lost to a missed scheduler run, a holiday or a site that was down: the next letter is built from the current state.
* The daily letter is now the only thing the deadline watch sends. A refund that turns overdue at 14:00 is reported in the next morning's summary rather than interrupting the day: the desk works this queue once, and nothing is lost by waiting, because an overdue refund stands in every letter until it is settled.
* The scan stays hourly even though the letter is daily. The exchange cycle rides the same hook, and WP-cron fires on traffic, so a single daily slot is a single chance; the gate opens on "the hour has passed", which delays a summary on a quiet morning instead of losing it.
* Fixed: the last-run marker was a transient with a three-day life, so the "deadline warnings are not running" banner switched itself off while the scheduler was still dead — exactly when it mattered. It is now an option, which also makes the uninstall routine's removal of it true for the first time. A second banner covers what that marker cannot see: the beat is landing but the letter is not going out.
* The working-window hours were retired — with one letter a day the send hour is the window. Working days and non-working dates still apply, and a site upgrading from the old window is given a send hour derived from it.

= 0.78.1 =
* The stock-status filter no longer decides a product is out of stock when no reservation exists. It read the stored quantity before checking whether anything was held, so it was not subtracting a hold — it was re-deriving the status from the raw number and substituting that for WooCommerce's own. A stock-managed product sitting at zero or below whose status still read instock (which the CSV importer, the REST API and any stock-syncing extension can all produce, because WooCommerce does not re-derive status from quantity on a plain save) was flipped to outofstock with no hold anywhere in the table, and could not be added to a cart. It now reads the hold first and returns untouched when there is none, which also stops it silently reversing another extension's instock at priority 20. The sibling quantity filter has always had this guard.

= 0.78.0 =
* Reverted the shop-wide switch added in 0.77.0. the carrier publishes no tracking API, so nothing feeds carrier events in, and the switch was a second control over something that does not happen yet — unobservable, and therefore untestable by the person setting it. The per-order tick remains: it also records which service carried a parcel, which is what picks the right tracking link when more than one is in use.
* No stored setting is lost; sites that saved the switch simply stop reading it.

= 0.77.0 =
* A shop-wide switch for whether a delivery service may move a customer's timeline at all. Off by default: on install nothing feeds carrier events in, and a setting that pretends otherwise is a lie waiting to be believed. Both this and the per-order tick have to be on before anything a service reports is used.
* Switching it off never takes the tracking link away — a link is not a status.
* The order panel says so when the shop-wide switch is off, rather than leaving an operator to discover that their tick only produced a link.

= 0.76.0 =
* New Use section on the Statistics screen: how often customers checked an order status, how often they opened the returns portal, how many of them filed a request, and how many lookups found nothing. Two cards, a month-by-month chart drawn without a library, and a filter for either page or both.
* The figures are five integers a month for the whole shop — no visitor key, no order reference, nothing finer than the month. A record of who looked at what is exactly what they are shaped to avoid keeping.
* They are raised live rather than aggregated, so the section answers on the day a page is switched on instead of waiting for the month to end.
* Fixed a latent defect the new counters would have hit: put_month() wrote with REPLACE, which deletes the row and inserts a new one, so closing a month would have erased a month of live counting at the moment it was closed. It now merges. The suite carried this as a documented latent bug; that check is now the other way round.

= 0.75.1 =
* Fixed on staging: the parcel line printed the internal slug of the delivery service ("post-at") instead of its name.

= 0.75.0 =
* Fixed: the timing token was issued only when the returns portal was on, so a shop running the tracking page alone had every lookup refused as a bot, with nothing saying why.
* Security: the per-order allowance no longer answers with its own status code and now counts every submitted number. As it was, a real order eventually started replying 429 while an invented one never did — an order-existence oracle. The bucket is also keyed by a hash rather than by the order number itself.
* Security: a number is matched against the shop's public order number first, and an internal WooCommerce id is accepted only when it is also that number. "abc123xyz" no longer resolves order 123, and an internal id no longer wins over a customer's real number.
* The item count and the name of the delivery service are now drawn on the page; both were computed, published by the endpoint and rendered by nothing.
* The Security screen says which public form each refusal came from — the returns portal or the order status page.
* A delivery service the shop typed by hand is folded into the shipped service publishing the same address, instead of sitting beside it as a duplicate. Shipped addresses are shown rather than offered for editing, with a closed disclosure for the day a carrier changes one.
* Translation catalogues repaired and the whole suite is green again, including the catalogue check that had been red.

= 0.74.0 =
* A Parcel tracking panel on the order screen: the parcel number, which delivery service carried it, and a tick saying it really was handed over. Nothing a carrier reports is used until that tick is set, so a number left on a cancelled label cannot march a customer's timeline forward on its own.
* The delivery-service list ships with ten common services and their published tracking addresses, none of them ticked and every address editable. {postcode} is understood alongside {number}, because several carriers ask for the delivery postcode as a second factor.
* With two or more services in use and no service recorded on the order, no link is shown rather than a guessed one.

= 0.73.0 =
* The one carrier address became a list of delivery services: name, tracking address and a tick per service, with the service recorded on a parcel choosing which address is used. The list ships empty, as it must — the plugin names no carrier.
* The tracking form draws its two fields as one pair, the way the returns portal does, instead of stacking them down the page.
* The fallback contact is rendered as markup again; it was escaped as text, so a merchant's link to the contact page reached customers as visible HTML.
* Every help bubble on the tracking screen is positioned by script and can no longer be clipped by a table or slide under the admin menu; the implementation is now shared rather than copied per screen.
* The forty wording fields are grouped under six headings instead of listed in one run.
* The staging fixture is read as of the day it was captured, so four checks no longer turn red with the calendar rather than with the code.

= 0.72.0 =
* Every Tracking setting now carries a note explaining what it does and what a sane value is, including one per wording field saying where that text appears.
* The customer preview left the settings screen and lives on Setup only: a 640px picture at the top of a form pushed every field the operator came to change below the fold.
* First tests for the module: 84 checks over the two ladders, the status maps, the off-ramp, the timeline, the labels and the URL template.

= 0.71.0 =
* New Tracking module: a public order-tracking page behind the [careback_tracking] shortcode, off by default.
* One timeline for the outbound order and one per return, withdrawal, exchange or warranty claim; a warranty claim can end in the item coming back rather than in money.
* Order statuses are mapped to customer-facing steps on the settings screen, so no shop's own status names live in the plugin.
* Settings and Setup now switch between Careback and Tracking; the tracking form writes only its own keys.
* Optional carrier link built from a URL template.
* The step ladder is a real ordered list: aria-current on the step you are on, a hidden state word on every step, and focus moved to the answer when it arrives.
* Vertical by default and horizontal only from 1024px, so long step labels no longer overflow their column.
* A journey that ended early is drawn as it happened: the steps it passed are solid, the steps it never reached are dimmed, and how it ended is printed below the ladder instead of as one more step.
* Dates follow the site's own date format and timezone, never ISO.
* German and Ukrainian translations for the whole module.

= 0.70.0 =
* Choice-of-remedy notice on warranty requests.

= 0.69.3 =
* The admin interface is translated. Half of it was not: 429 of the 872 operator strings had never reached the German catalogue at all, so a shop switching the admin language got an English screen and no warning anywhere. Labels, buttons, statuses, column headings and the counters on the board are now translated in both shipped languages; long explanatory passages — read once while learning a screen — stay in English by decision, and that decision is written down in languages/untranslated-by-design.txt rather than left as a silent gap.
* The check that was supposed to catch this now can. It only ever looked at strings already in the catalogue with an empty translation, so a string that was never added was invisible to it. It now asks the question the other way round: every operator string the sources export must be in every catalogue, or named in the exceptions file.
* The neutrality scan reads .distignore instead of keeping its own copy of the list. The copy had drifted, and a working note that never leaves the machine was failing the build.

= 0.69.2 =
* Tooltips stay inside the window. The bubble was positioned against its icon, so any ancestor that scrolls or hides its overflow clipped it, and near the right edge it went off the screen entirely — which on a board of cards is most of them. It is now placed at the moment it opens, flipped below the icon when there is no room above, and nudged sideways rather than allowed past either edge. Keyboard focus opens it exactly as hovering does.
* No figure in the refund queue is clipped. A cut-off number reads as a fault in the software rather than a shortage of space, so the label is short — "21 d · withheld" — and the whole sentence is on the row itself, where hovering shows it in full.

= 0.69.1 =
* The queue of refunds reads in three colours instead of five: blue while there is room, amber as it narrows, red on the last day and past it. A withheld request whose period has run out is red too — the money falls due the moment anything arrives — and the label beside it says "withheld", so it cannot be read as a shop already in default.
* The queue no longer scrolls sideways. The bar is the column that gives, and the label beside it is clipped rather than allowed to widen the row.
* Fixed: the "Sort" label appeared after the control it names. WordPress floats every select inside a table navigation bar to the left, which had carried the control past its own label.

= 0.69.0 =
* The refund period runs; only the payment is withheld. The screen used to tell an operator, in so many words, that "no deadline is running yet" while the shop waited for the goods — and that was the one sentence on it that was untrue. The period counts from the day the declaration arrived and does not stop; what the law allows is to refuse the payment until the goods are back or dispatch is proven. A right not to pay, not a pause on the clock. The date was always computed correctly, and is unchanged.
* A request whose period has run out while payment is lawfully withheld is now a state of its own, and visible. It used to be in no count, raise no email and appear in no list; the only thing that grew was a grey "waiting N days". It is amber rather than red — nothing is late while the right to withhold stands — but there is nothing left of the period, and the money falls due the day the goods or the proof arrive. On this plugin's own staging data, six requests were sitting in that state unseen.
* Customers can tell you the parcel is on its way. A tracking or receipt number typed into the portal ends the right to withhold on the day it arrives, exactly as an email would — except the desk can see it. Until now proof of dispatch could only be entered by the shop, so a customer replying with a tracking number left the refund quietly late while the screen said the clock had not started. The field appears only on a statutory withdrawal still waiting, and disappears once the parcel or the proof is recorded.
* The refund period is capped at fourteen days. It was a warning shown once, after saving, and a shop that clicked past it never saw it again. Where the fourteen-day rule applies the period is not the shop's to lengthen, and a larger figure bought no time — it only stopped the screen reporting a late payment as late. The goodwill window a shop offers on top of the statutory one is a different setting and has no limit.
* Fixed: the scan behind the reminder emails did not read `delivery_state`, the same omission repaired in the board a release earlier. A parcel that came back to the shop ends the right to withhold — the board knew, the email did not, and no reminder ever went out on those.

= 0.68.1 =
* The board holds its shape. Each card's help icon was a grid item of its own, so it was counted as a cell and pushed every card after it out of line; it now sits inside the card it explains.
* A queue of the requests with money owed on them sits above the cards: request number, the day the refund is due, and a bar that fills as the period is used up and reddens in steps as the day comes closer — green, amber, orange, then red once it has passed. Ordered soonest first, so it can be worked straight down, and each row opens its request. Requests waiting for goods to come back are deliberately absent: their clock is on hold and has no length to draw.
* Fixed: the query behind the board did not read `delivery_state`, so it could answer differently from the request row underneath it — a parcel that came back to the shop ends the right to withhold payment, and the board was not being told.
* The reservation filter moved out from beside the sort control, where it read as part of it, to its own place after the Filter button.

= 0.68.0 =
* The requests screen opens on the work. What was at the top was a breakdown of why customers return things — ten rows, half the screen, and nothing anyone acts on in the next ten minutes — with the counts that do matter squeezed into a thin line underneath that hid itself whenever they were all zero. The breakdown has moved to Statistics, where the other once-a-month figures live, and in its place is a board of nine cards: what nobody has picked up, refunds overdue and nearly due, requests stalled, requests held for review, both guarantee clocks, and reserved replacement units. Each card is a link to the requests it counts and each carries a tooltip, because a number an operator cannot interpret is a number they learn to ignore.
* The refund deadline column draws a bar. The date and the days left were easy to skim past; a bar that is nearly full is not. It grows into place when the page loads, turns amber on the same three-day threshold the text does, and is drawn only where a clock is actually running — a settled request has nothing counting down, and a suspended one has a length nobody can know.

= 0.67.1 =
* You can see what is reserved, not just how much. A count of units held too long is only useful if the next click says which units, on which products, for which requests — so there is now a list: the replacement and a link to it, the units, the request and its order, how long it has been held, and when the reservation ends. A reservation carrying a statutory withdrawal says plainly that it has no end and why.
* The request list can be narrowed to requests reserving a replacement. Those are the ones where something the shop cannot sell is waiting on a decision.

= 0.67.0 =
* A reserved replacement is released when a request is put back in the queue. Moving a request to `new` — what an operator does when they picked it up by mistake — is neither taking it on nor ending it, and it was in neither list: the unit stayed reserved on a request that then looked untouched, out of the shop, with no clock on it. Every status is now accounted for, and a build check fails the release if a new one is added to none of the three lists.
* Reserved replacements are counted on the attention strip. A reservation on an ordinary exchange releases itself when its time runs out; one carrying a statutory withdrawal cannot, because a statutory right may not be ended on a timer, so it was announced by a single email and then relied on somebody remembering. The strip now names how many units have been reserved longer than a reservation is meant to last, counted in units rather than requests, because units are what cannot be sold.

= 0.66.1 =
* Three reads of a column added in a later release were made defensive. They mattered because of a change in 0.66.0: the migration now runs on an admin load rather than on a visitor's request, so a site whose files are updated and whose admin nobody has opened yet is briefly running new code against a table that has not gained the columns. A bare read printed a warning across the operator's screen; it now does nothing instead. A build check keeps the rest honest.

= 0.66.0 =
* The portal works behind a page cache. It did not: the render-time token sits in the page's HTML, a cache serves that HTML for hours, and every visitor was submitting a token minted long before they arrived. The form answered with the wording used for a wrong order number, and each attempt counted towards the auto-block, so a customer who tried again locked themselves out of a form that had never worked. The script now fetches a fresh token on load, a merely old token is answered with "reload the page" and never counted against the visitor, and the ceiling defaults to a day instead of an hour and can be switched off.
* The withdrawal window and the refund deadline are two settings. One number stood for both, and they are not the same rule: how long a customer may declare may be widened by a shop out of goodwill, how long the shop then has to pay may not. A shop offering a thirty-day window was being told a payment made on day twenty-two was in time when it was eight days late.
* `duplicate` is a closing status everywhere, not in one list of three. A declaration filed twice could be marked as the copy it is, and then sat with the refund clock still counting on it, in the overdue strip, with no status left to reach and no grounds ever asked for.
* A partial settlement asks what was withheld. The status stops the refund clock, and read as "part paid, the rest to follow" it closed the deadline on money still owed. It is now unavailable until a deduction or a return-carriage figure is on the request; paying the whole amount back is unaffected.
* A withdrawal filed before the parcel arrived no longer waits for goods that will never come back. Refused at the door or unclaimed, the parcel returns to the shop, neither date the suspension waits for is ever stamped, and the request sat outside every count indefinitely while the money was owed.
* The database migration runs on an admin load, on cron or under WP-CLI, behind a lock, and queues the statistics instead of aggregating thirteen months inside somebody's page load. An index it cannot add — a unique one on a table whose rows already break it — is recorded and shown, rather than discarded while the upgrade reports success.
* Rotating your site's salts no longer orphans every stored hash. The hashes are how a customer is found again when they ask what you hold or ask you to erase it, and a routine salt rotation quietly made every one of them unmatchable. Existing hashes are unaffected by this release. `CAREBACK_HASH_KEY` is documented under Installation and remains the arrangement to prefer.
* Anonymisation no longer walks past a row whose name is blank but whose email hash is not.
* A blocked source is turned away before anything is written for it, and a throttled one is noted once per window instead of once per attempt.
* Uninstalling clears all three cron hooks, not two, along with the key index, the throttle counters, the ticket claims and the operator's admin language — per site on a network.
* Ticket claims left behind by a request that died mid-submission are swept up by the retention run.
* The Statistics screen no longer prints a return rate it cannot support. Requests are counted in the month they were filed, orders in the month they were placed, so a request against an older order lands in one and not the other. On a young shop that produced "22 of 3 orders", printed as a return rate of 733.3%; the two counts are now given plainly instead, with the reason.
* Parameter types that PHP 8.4 deprecates were made explicitly nullable.

= 0.65.0 =
* A settled request can no longer be walked back into the pipeline. It could be, and the second write cleared the closing date — the date retention ages a closed row from, so the row silently moved years back in the anonymisation queue. Skipping forward is still allowed, because settling on the day a declaration arrives is lawful where the shop offers to collect: the rule is about direction, not distance.
* Money and closure are now decisions rather than facts. An integration may record that a parcel arrived; it may not decide that a refund is owed or that a claim is refused. One automated actor is excepted by name — the exchange clock, ending a service the shop offered on a deadline it set itself.
* The manual-entry form asks the same helper as everything else whether a declaration was in time. It counted in seconds while the rest of the plugin counts in whole days, so a declaration made late on the last day was in time through the portal and late through the form — and which door it came through is not something the customer chose.
* Switching off the completed date as a stand-in for a handover now switches off the order-status list as well. A shop with a delivery integration was being told there was no handover date and that the order was delivered, in the same breath.
* Proof of dispatch is written once. It ends the shop's right to withhold payment and records whether the customer posted in time, and a second save moved it. Correcting it is now a separate act that requires a reason and keeps both dates.
* The statutory window cannot be zero, on save or on read, and the screen warns when it is set below the European fourteen — without compiling that figure in, because the plugin ships outside Europe.
* Guarantee claims past the days you allow yourself now appear in the attention strip. They were in none of the counts: the withdrawal scan filters on the statutory route in SQL, so an overdue claim was visible only by opening it.
* The unique ticket index is replayed on upgrade, so a site from an older schema receives the guard that stops one ticket filing twice.
* Fixed: the local test harness could not mount a plugin path containing a space, so `npm test` failed on any workspace with one in it.

= 0.64.2 =
* Fixed: two tooltips had drifted out of their own row when the fields were regrouped by route, leaving one field with two explanations and two fields with none. A build test now counts them, because neither is visible in a diff and both are obvious on screen.
* The four guarantee-period figures have a tooltip each instead of one over the row. They are four different rules, and one paragraph over four boxes left it to the reader to work out which sentence belonged to which.
* Each of those tooltips now says what the figure changes on a request, with a worked example where the arithmetic is not obvious.

= 0.64.1 =
* Fixed: clearing the counters could not reach anything written before the release that introduced it. The index only holds what it has seen, so on the upgrade that added the button the button had nothing to clear — and said it had worked. The options table is now swept as well, which covers a site whose transients live there; the index covers a site whose transients live in an object cache. Neither source alone is enough.

= 0.64.0 =
* A button on the Security screen clears every rate-limit counter and block the plugin holds. It is for testing your own portal, where a tester locks themselves out and then cannot get back in: the counters are keyed by a hash of the source, which cannot be reversed, so there is no way to clear one visitor and leave the rest.
* The act is written into the same log as the attempts it was protecting against, and the last time it happened is shown next to the button — it appears in neither of the counting tables, so it is asked for by date instead.
* The counters are now tracked in an index rather than found by sweeping the options table. On a site with a persistent object cache transients are not in that table at all, and the sweep would have found nothing while appearing to work.

= 0.63.3 =
* The route radio on step two now draws itself like the rest of the portal instead of in the browser's blue. The checkboxes were given a monochrome treatment and the radio was missed, so it was the one control on the page that still looked like a system widget. It follows the portal's own text colour, so a shop that overrides it gets its own.

= 0.63.2 =
* A route that requires a reason and has none ticked for it no longer requires one. The requirement is set on one screen and the list on another, so the pair was saveable — and it left the dropdown holding nothing but "please choose", with the request unsendable and nothing on either screen saying why. A route told not to ask at all is left alone.

= 0.63.1 =
* Fifteen tests for where the goods are: five order fixtures — nothing recorded, a handover in the meta, the completed date standing in for one, a parcel the carrier brought back, and both events on the same order — plus the rule that decides between them. Every window in the plugin is measured from the handover, and until now the handover was covered by reasoning rather than by data.

= 0.63.0 =
* A status change is now refused by the repository, not only hidden by the screen. A carrier integration, a cron job or anything else reaching the method directly gets the same answer the operator's dropdown gives. The four confirmation gates stay on the screen on purpose: each takes an answer from a person, and a gate that passes silently when nobody is there is worse than no gate.
* Fixed: the status-changed event fired whichever way the write went. A failed update still sent the customer a status email and still released a reservation, for a state the database never held.
* Fixed: the replacement delivery date could be written more than once. The replacement's own return window is counted from it, so a replayed carrier event — or a poll reading the same delivered event on every run — handed the customer extra days each time.
* A parcel the carrier brought back now wins over a delivery when its timestamp is equal or later, and both are read from order meta with the status lists kept as a fallback. An order completed early and then returned used to read as delivered for good.
* The completed date can be switched off as a stand-in for a handover, for shops whose delivery integration sees every outbound shipment.

= 0.62.0 =
* A guarantee claim can be settled. Six outcomes — repaired, replaced, price reduced, sale unwound, no fault accepted, or paid as a goodwill gesture — recorded apart from the status, because a repair and a fault you did not accept both end with the item back at the customer and the status alone cannot tell them apart.
* New status for exactly that: the item has gone back to the customer. Offered on a guarantee claim and on no other route, closes the request, moves no money, and releases any reservation.
* Refusing a guarantee claim now asks why, from a list, with an account of what you found. The panel says plainly that in many countries a fault reported soon after handover is presumed to have been there, and that it is the shop that has to show otherwise.
* Optional step for shops whose law requires it: confirm the customer was told, before the fault was put right, that they choose between a repair and a replacement and that a repair extends the guarantee period. Off by default, and no date or country is compiled in anywhere.
* The request now shows where the report falls in the guarantee period and who carries the proof. A recorded repair extends the period once, counted from the end of it, and a repair recorded on an earlier request for the same line is found — a claim in month thirty on an item repaired in month twenty read as out of time before.
* Never says a claim is out of time on a handover date the shop never recorded.
* A fault report can be recorded by hand, with an optional reason, and takes every line of the order: the customer keeps the item, so there is nothing for it to run out of.
* The request list can be filtered by route, including the retired one that rows still carry.
* Money paid on a guarantee claim no longer counts toward how long a statutory refund takes, and a sale that stood because the item was put right has a figure of its own.
* Database version 0.17.0 — three additive columns on requests, one on the monthly figures.

= 0.61.0 =
* Fixed: a fault could not be reported on an item another request already held. The portal offered it — correctly, the fault is in an item the customer keeps — and the submission then refused it as having no units left. The rule that a guarantee claim takes no unit lived in one class and the capacity check in another.
* Fixed: with the guarantee route switched on, a line every statutory declaration already covered went on offering the statutory route, which was then refused on submit. Such a line used to be skipped whole, which hid the missing test rather than passing it.
* Fixed: the reason table on the statistics screen printed three route headings over four columns, so every figure in the row sat one place to the left. The headings are built from the route list now.
* Fixed: a route that requires a reason no longer collides with the setting that hides the field. A field some route cannot be filed without is shown whatever the toggle says, on the form and on the server, and a route the customer cannot reach requires nothing.
* Fixed: the browser now reads the per-route reason rules it was already being sent. The star, the field and the check before sending follow the route the customer picked, so what they are told is required is what the server insists on.
* Fixed: a replacement payload was normalised to the exchange route after the reason gate had already run, so a request could be filed as an exchange without the reason that route requires.
* The days you allow yourself to put a fault right are now shown on the request, counting down from the day it was reported. Advisory, like every date here: nothing is refused when it runs out — what changes is that the customer can generally ask for money instead of a repair, and which of the two happens stops being your choice.

= 0.60.0 =
* The settings screen opens with a part per route. Everything that belongs to one route is in one place: how long it stays open, whether it is offered at all, which reasons the customer picks from on it, and whether a reason is asked for. Nothing was renamed and nothing was dropped; the sections moved.
* Reasons are offered per route. They are still defined once — one key, one fault flag, one set of figures — and each row is ticked where it should appear. A reason ticked nowhere stops being offered and keeps its key, its history and its figures.
* A reason row written before this release carries no route set and is offered everywhere, so nothing changes until the ticks are used.
* Each route decides whether a reason is asked for and whether it has to be answered — except the statutory withdrawal, which is fixed at optional and offers no setting, because a withdrawal is made without giving reasons.
* A reason posted from a browser holding an older script is accepted even if that route no longer offers it. Refusing it would cost a customer their declaration over a dropdown.

= 0.59.1 =
* Fixed: the guarantee route had no counter of its own in the statistics. A guarantee claim was counted in the total and in no route column, so the route figures stopped adding up to the total, and the reason table dropped its guarantee rows without saying so. Both were shipped in 0.59.0.
* The reason table now reads the route list instead of naming three routes in its own line, and a build test fails if any route lacks an aggregate column — so a fifth route becomes a build error rather than a silently missing column.
* Database version 0.16.0 — one additive column. Periods aggregated before it keep a zero, which is correct: the route did not exist in them.

= 0.59.0 =
* A fourth route: a customer can report a faulty item. Off by default, because it adds an option to the portal that never closes.
* The route has no period. A statutory guarantee runs for years, is often extended by a repair, and is measured from a handover date many shops never record, so a cut-off here would refuse claims the law covers. The dates are shown to an operator as advice and decide nothing.
* A guarantee claim takes no unit of the order with it. Without that a second report on the same item would be impossible — a fault can recur, and can be reported again after a repair — and the statutory routes would close on a line nobody had claimed.
* It starts no refund clock. A fault reported inside the withdrawal window used to have been recorded as a statutory declaration, carrying a refund deadline for money nobody had asked for.
* A reason and a description are required on this route and on no other: a withdrawal is made without giving reasons, and a fault report that does not say what is wrong cannot be answered.
* New setting for how long you allow yourself to put a fault right, counted from the day it was reported. Nothing about it is shown to the customer; it drives your own warnings.

= 0.58.2 =
* Corrected wording that described the goods coming back as the thing that starts a refund clock. § 14 Abs 3 has two ends to the right to withhold — the goods arriving **or** the customer proving they were sent — and the second is usually the earlier. The behaviour was already right and is unchanged; the hint beside the undo, the changelog entry for 0.58.0 and two code comments said only half of it.

= 0.58.1 =
* The manual-entry form states that nothing is sent to the customer from it, before it is filled in rather than after, and the notice after saving repeats it. Recording a declaration starts its deadlines and answers nobody, and an operator who assumed the portal's acknowledgement had gone out left someone waiting on a confirmation that was never coming.

= 0.58.0 =
* Messages that could not be delivered are listed rather than counted: what failed, when, and the request it belongs to, with its order number and a link into it. The screen previously showed a number and nothing that could be opened.
* A notification that never reached the shop's own team is now among them. It was recorded and shown nowhere at all, so a request could sit unseen while its refund deadline ran.
* A failure whose request has since been deleted or anonymised is still listed, marked as no longer on file, rather than dropped from the log.

= 0.57.0 =
* A reason that filed requests already carry is retired onto another reason instead of being deleted. Tick "Retire", pick where its requests go, and save: the requests are moved and the row leaves the list.
* A row ticked for retirement with no target, or with a target that is itself going away in the same save, stays on the list. Dropping the key is what left a request rendering its reason as a machine string and counted under no reason at all.
* Statistics for periods that have already closed keep the figures they were counted with. A period is aggregated once, after it ends, and moving requests today does not restate what was reported then.

= 0.56.0 =
* The day the goods reached you can be corrected. It was stamped as a side effect of one status and then closed for good, so a date entered on the day an operator got round to the parcel stayed on the record as the day it arrived.
* An arrival can be taken back when the parcel turns out to be empty or to hold something else. The date and the origin both go, and the refund clock returns to suspended only where nothing else is running it: payment may be withheld until the goods are back **or** the customer proves they were sent, so a request already carrying proof of dispatch keeps its clock. Until now a mistaken arrival started a clock that nothing could stop, on a request where nothing had been returned.
* Taking an arrival back requires an account of what was in the parcel, and leaves the status where the operator put it: what arrived is a fact about the parcel, where the request stands is their decision.

= 0.55.0 =
* The product list now carries a red badge under the stock cell naming the units this plugin is holding and linking to the requests holding them, so a reserve is visible to anyone reconciling stock by eye. The merchant's own number is left exactly as WooCommerce states it.
* The badge is shown whether or not held units are subtracted, and says so where a product WooCommerce keeps no number for: the reserve exists on the request and the shop goes on selling the unit.
* An exchange whose replacement has already been dispatched can now be settled in money. It previously had nowhere left to go — money is refused on an exchange, and the type could not be changed after dispatch — so the amount had to be recorded against a second request invented for the purpose.
* Releasing money on a dispatched exchange asks what became of the replacement and records the answer. Nothing is refused; two things leaving the shop for one request is written down rather than stopped.

= 0.54.0 =
* Closing a statutory declaration now asks which of four grounds applies, and the ground decides the refund clock: a duplicate of another request, a declaration the customer took back, and a contract that never carried the right all end the obligation and stop the clock. Refusing a withdrawal does not, because the law counts its fourteen days from the declaration arriving and gives a trader no way to end them by saying no.
* That last ground is the one selected by default, so a panel clicked through without being read leaves the deadline in place rather than clearing it. A duplicate has to name another request on the same order; a number that names none clears nothing.
* Fixed: cancelling a request stopped the refund clock on its own, which made it the quiet way to silence a deadline that was still owed. It now takes a ground like any other closure. Requests closed before this release carry no ground and keep the behaviour they were closed under.
* Database version 0.15.0 — two additive columns; rows written before it read as carrying no ground.

= 0.53.0 =
* A declaration an operator records by hand is now marked against the requests already holding the units it covers, and those requests are marked back. Until now only the portal did this, so a withdrawal that arrived by email could be paid out beside an exchange covering the same item, with no warning on either screen.
* Lines that could not go into a manually recorded request, because earlier requests hold every unit on them, are named after the save and written into the request history. They used to be dropped in silence, so a declaration about the whole order could be recorded against one cheap line of it.
* The payout gate takes a tick instead of a typed request number. The number was printed on the panel directly above the field, so typing it copied what was already on screen; the entry now names every overlapping request rather than the one that was typed.

= 0.52.0 =
* The request filter now sits directly above the table it filters, as a standard WordPress table-nav row: search and sort on the left, the status filter at the right edge over the Status column. It used to stand half a screen away from the results.
* Fixed: the multi-status filter was a button opened by an inline script, so with JavaScript off the checkboxes could not be reached at all and the filter silently offered only one status. It is now a native disclosure that needs no script, and the screen carries no inline script any more.
* The settings screen is grouped into five parts with a sticky index: portal and form, deadlines and reminders, texts the customer reads, emails, and safeguards. Fifteen sections used to run down one column with no way to find anything. Still one form and one Save; the index scrolls, it does not reload, so nothing unsaved is lost. Every text group has its own anchor, which answers "where do I change this wording".

= 0.51.0 =
* An exchange can now hold the replacement. When an operator moves the request into a working status, one unit of the requested item is reserved and taken out of what the storefront offers, so the replacement is still there when the customer's parcel arrives. The merchant's own stock number is never written to: the hold is state on the request, subtracted on read, and the products screen shows the number as entered with a line naming the units held.
* Reserved exchanges run a closed cycle: a reminder to the customer on day three, a second on day five, and closure on day seven, which releases the hold and frees the item. All three days are settings, and the three emails are editable subject and HTML body like every other email.
* A request carrying a statutory declaration is never closed by the cycle and its hold is taken without an end date. Such a request is flagged and mailed to the operator instead, because a declaration already made cannot be cancelled by a timer.
* Off by default: the hold changes what a shop offers for sale, so it is switched on deliberately. A second switch records and shows holds without touching availability.
* Database version 0.14.0 — additive columns and three indexes; rows written before it read as never reserved.

= 0.50.1 =
* Reworded the four notices about a parcel the carrier brought back. They said return carriage may not be withheld and stopped there, which reads as "the shop bears every cost of an uncollected parcel". They now say what the rule actually is: the leg back ran on the shop's own carrier contract, not on a return shipment by the customer, so it is not a deduction from this refund — and what the failed delivery cost is a separate claim rather than nothing at all.

= 0.50.0 =
* Marking a parcel as received now asks where it came from: the customer posted it, the carrier brought it back uncollected, or it never left the warehouse. A parcel the carrier returned was never posted by the customer, so return carriage may not be withheld from their refund — the screen says so, and entering an amount anyway is recorded in the request history rather than silently accepted.
* The recorded origin is shown in the request detail, not only in the history, and the actual arrival date is kept as entered rather than replaced by today.
* Database version 0.13.0 adds the origin column; rows written before it keep working and simply carry no answer.

= 0.49.0 =
* Dispute watching is now a setting, on by default. Switching it off stops the flags, the emails and the extra question; flags already recorded are kept, because they record a dispute that was open.
* New operator playbook shipped with the source: twenty-six real situations, each written as what you see, what the plugin does by itself, what you do, what you must not do, and where to look. It also lists the gaps found while tracing each situation against the code, rather than describing behaviour the code does not have.

= 0.48.0 =
* A request whose payment is being disputed with the payment provider is now marked as such: a badge in the list, a plate in the detail panel, and an email to the operator when the dispute opens. Releasing money on it asks what was checked, and the answer is written into the request history with its author. Nothing is blocked — the statutory refund deadline has an exhaustive list of grounds for withholding payment, and a dispute is not one of them.
* The dispute state is a neutral concept in the core with two actions any payment integration can call. The bridge to a specific gateway is one file that registers itself only when that gateway is present, so the core carries no vendor name.
* The mark is cleared when the dispute closes, and a webhook arriving twice writes one history entry and sends one email.

= 0.47.0 =
* The order screen now warns, under each line still inside its statutory withdrawal period, that replacing the item on the line itself closes the withdrawal route in the portal — and names the date the period ends. Adding the replacement as its own line keeps the route open. The warning does not block anything; it appears while the period runs, whether or not a request has been filed.

= 0.46.0 =
* Return reasons are configured in one place. The six built-in labels had a second set of fields under Settings → Texts, and a shop that had written its own list saw those fields do nothing at all. The built-in entries are now written into the list itself on install, where they are edited like any other row, and the duplicate group is gone. A label already typed into the old fields is carried across; keys are untouched, so filed requests and closed periods keep matching.
* Reasons can be reordered. The order is the order the customer reads in the dropdown. Rows are dragged with the mouse or numbered by hand — the number is what is saved, so the screen works without JavaScript.
* The Reasons section states how many filed requests carry each reason, warns only when at least one of them is actually in use, and asks for confirmation before a reason that requests carry is removed. Rewording is safe; removing a key is not.
* Database version 0.12.0 — no schema change; the bump runs the one-off move of the built-in reasons into the list.

= 0.45.1 =
* The payout confirmation now names the number to type and says it is not the number of the request being settled. The previous wording read as "which request are you paying out", so the natural first answer was refused.

= 0.45.0 =
* Releasing money on a request whose items another request also holds now asks for confirmation naming that other request, and the number has to be typed in. Nothing is forbidden — an order with an exchange on one item and a return on another goes through — but a double payout can no longer happen by clicking past a notice. The confirmation is recorded in the request history with its author.
* The acknowledgement email is chosen by the route the customer took, not by the reason they picked. A statutory withdrawal filed with a fault reason used to receive the fault letter, which told the customer the right of withdrawal "may still be open to you" while the database recorded the withdrawal and the refund clock was already running. A fault reason now adds a paragraph instead of replacing the letter, and the withdrawal letter states that the withdrawal is effective and the refund period has started.
* Closing a request that carries a statutory withdrawal as not accepted or cancelled now warns regardless of the refund clock, and requires written grounds before the status is saved. The clock of a fresh withdrawal is suspended while the goods travel, so the previous warning never fired in the most common case.

= 0.44.0 =
* Fixed: a statutory withdrawal could not be filed for an item already held by an earlier exchange or return, although the portal offered the route and the shop had told the customer in writing that the right was unaffected. A withdrawal filed while the statutory window is open now counts only statutory claims against a line, so the item stays selectable. Voluntary routes are refused over an occupied unit exactly as before.
* Both requests are flagged and cross-referenced in the admin with a badge, a note and a history entry: the earlier one keeps its status and its items, and an operator settles the two against each other. Nothing is cancelled automatically, because a parcel in transit does not stop being in transit.
* Statistics count such an item once and its value once, while both requests are still counted as requests.

= 0.43.1 =
* Fixed: a reason key holding a hyphen was rewritten on the next save of the settings, because the two places that derive a key disagreed on that one character. Requests filed under the old key lost their label and the frozen yearly reason figures split into two series — the exact failure 0.43.0 was released to prevent. Reachable on a fresh install, not only on upgrade.

= 0.43.0 =
* Request status names shown to an operator are plain English literals now. They render a stored value, so switching the admin language must not change them; only static interface wording follows the switcher. The customer still sees the wording set under Settings, resolved by the same stored key.
* Static admin wording that happened to match a visitor-facing default — column headings such as Status, Order, Reason — now carries a translation context, so the admin interface can be translated without the customer side inheriting it.
* Return reasons are configured as a key, a customer-facing label and a fault checkbox instead of one line of text. The key was previously derived from the label, so editing the wording changed the key: existing requests lost their label and the frozen yearly reason figures split in two. Existing lists are migrated with their current keys preserved.

= 0.42.0 =
* Settings → Texts now holds visitor-facing wording only. Eight operator-only strings — the three timeliness badges and the five Statistics messages — were listed there and had to be typed in by hand although no customer ever sees them. They follow the admin language switcher like the rest of the admin interface instead.
* Removed three settings fields nothing rendered: the former exchange checkbox label, the not-yet-delivered note and the duplicate-submission wording. Their payload keys are no longer sent to the browser.
* New build check: a visitor-facing default that carries a shipped translation fails the build, so the "English source, merchant supplies the wording" rule cannot be undone by accident.

= 0.41.0 =
* Every field under Settings → Texts now carries a help tip naming where the string is rendered and the condition under which a customer sees it. The section previously showed 126 raw keys with no explanation — the only part of the settings without tips. Tips follow the admin language switcher.
* Tips for the operator-only groups state that those strings never reach a customer, so nobody translates them for one.

= 0.40.0 =
* Visitor-facing strings are no longer shipped pre-translated. English is the product default and a shop supplies its own wording under Settings → Texts, in whatever language it trades in. Bundled translations of these strings imposed one country's phrasing on every shop sharing that locale — a consumer-law sentence written for one jurisdiction is not correct in the next one. Operator-facing translations are unaffected, and the admin language toggle still works.
* Removed a hardcoded contact address from the shipped wording. The contact a customer should use belongs in the fallback contact setting, not in a translated string.

= 0.39.1 =
* Completed the German and Ukrainian defaults: 49 visitor-facing strings had no translation and fell back to English on a shop running either language. Every string a customer can see now ships translated, so an untouched Settings field no longer produces English. Operator-facing wording stays English by design.
* The exchange warning shown above the consent checkbox now states the fact — the replacement goes out after the returned parcel arrives and is checked, and the statutory period keeps running meanwhile — without advising the customer to choose one route over the other.
* The message about a route whose period has ended no longer suggests a different route; the routes still open are listed on the same screen.

= 0.39.0 =
* Every string a customer can see is now an editable field under Settings → Texts, including the twelve request-status names, the three email subjects, the status-email body and the built-in return reasons. A shop running any language can now write its own wording without a translation file; the bundled German and Ukrainian files remain as defaults. Operator-facing wording is untouched and stays in the admin language.
* The amount withheld and the return carriage are now counted in the month they were settled rather than the month the request was filed. A request filed in one month and settled in the next used to fall between two periods and be counted in neither. Amounts entered against an already-closed period are carried into the next open one, so no figure is lost.
* Database version 0.11.0: settlement columns and a one-time backfill that gives existing rows a settlement date and marks amounts already accounted for.

= 0.38.0 =
* Reverted: return reasons are aggregated per calendar year again. Counting them per month, introduced in 0.37.0, stored a reason-by-route cell that is frequently a single request, and a stored cell that thin makes the person who filed it identifiable from the order list. The empty state now explains why the table waits for a year rather than implying something is broken.
* Fixed: the amount withheld from refunds and the return carriage paid were recorded and aggregated but never printed. Both now appear in the Money table with help tips.
* Fixed: the admin-language override applied to every admin request, including the one that sends a customer a status email, so a shop running another language mailed English status names. The override is now limited to screens that render the plugin's own interface.
* Fixed: the Statistics screen asked for the reason and variation rows of the year currently running, which by design has none. It now uses the newest year that has ended, so a completed cross stays reachable instead of disappearing for twelve months.
* Fixed: anonymisation left the operator note, the confirmation the customer agreed to, and the status history in place. All three are cleared with the rest.
* The personal-data export now includes the two amounts, the agreed outcome, its note and the confirmed statement.

= 0.37.1 =
* Fixed: the bundled translations were never applied. The text domain was loaded on `plugins_loaded`, which runs before WordPress resolves the locale of the request, so every shipped string fell back to English while only the strings typed into Settings → Texts appeared translated. Loading moved to `init`.

= 0.37.0 =
* Fixed: five messages the form shows when a request is refused — wrong order details, too many attempts, expired session, already submitted, missing reason — existed only as hardcoded strings. They were the one group of visitor-facing texts not editable under Settings → Texts, and on a shop running another language they appeared in English. All five are now editable keys.
* Fixed: return reasons were aggregated only after a whole calendar year had ended, so the Reasons table stayed empty for up to twelve months. Reasons are now counted per month, like every other figure on the screen.
* The repeat-customer line now says what it counts, with a help tip: it counts people rather than requests, and explains that a zero can mean "fewer than the smallest figure to publish".
* New help tip on the Reference heading explaining the dash rule.
* Database version 0.10.0: the reason period column widens to hold a month label.

= 0.36.0 =
* Fixed: the amount withheld from a refund and the return carriage paid were counted by the period statistics but had no column in the request table and no field in the admin screen. All three figures that depend on them — total withheld, number of requests carrying a deduction, and total return cost — were therefore always zero, and a period keeps whatever it was closed with.
* New "Amounts" block on a request records both figures. They are stored per request and feed the statistics of the period the request belongs to.
* Fixed: the note taken when a request was converted to another route was stored and never displayed. The new "Agreed outcome" block shows the recorded outcome, its timestamp and that note.
* Added German and Ukrainian translations for the per-route submit labels, the fault-report label and the new admin strings. On a German-only shop the withdrawal button previously fell back to its English default.
* New schema-to-reader test: a build fails if the aggregation reads a request column that the schema does not define, or reads a key that no query returns.
* Database version 0.9.0. The two new columns are added by the standard upgrade path; existing rows start at zero.

= 0.35.0 =
* Statistics are now aggregated as soon as the plugin is installed or updated, instead of waiting for the first scheduled run two hours later. A shop that had just updated saw an empty screen although completed months were sitting in the database.
* The empty Statistics screen now names which of three situations it is in: no collection has run yet, no completed month carries a request, or every figure is below the "smallest figure to publish" threshold — with the threshold in force spelled out. It previously said only "Nothing aggregated yet", which did not distinguish a normal empty state from a broken one.
* New "Collect now" button on the Statistics screen runs the aggregation on demand and reports how many periods it closed. Periods already closed keep their published figures.
* The five new screen messages are editable under Settings → Texts, like every other visitor- and operator-facing string.
* The "Replacement line…" picker in the request list no longer runs past the right edge of the table. Both selects in the Status column are now capped at the column width, and an overlong line label is shortened with the full text kept on hover.

= 0.34.0 =
* A variation that sold fewer units than the "smallest figure to publish" setting no longer appears on the Statistics screen at all. Its units sold were printed as a plain number beside a suppressed return count, and one unit sold in a year points at a single order that can be found by filtering the order list on the same variation. The whole row is now withheld rather than one cell of it.
* The "top reasons" beside a variation now pass through the same threshold. A reason given once printed as "Size (1)", which named what a single identifiable customer said. Below the threshold the reason is now listed without its number.
* The missed-deadlines table no longer carries the value refunded that month. A month with one miss showed an exact amount next to it. The count of misses stays.
* Uninstall now clears the statistics scheduling hook as well as the retention one. It was left behind in the cron table with nothing to run it.

= 0.33.0 =
* Indexes are now added to tables that already exist. `dbDelta` creates an index only at the same time as the table, so any install upgraded rather than freshly created was missing every index declared after it was first set up. The upgrade replays them one by one.
* New index behind the per-line window lookup, and two on the event log behind the abuse counters and the deadline-notice check. Those queries scanned the table before.
* The reason and product crosses are no longer at the mercy of a single day's cron run. They were only ever closed while the newest ended month still fell in the previous calendar year, which is January and no other month; a scheduler that missed it left a year unaggregated permanently, with no backfill. The year is now closed on any run.
* The requests screen loads each order once per page instead of up to three times per row, and a repeated per-line window lookup is answered once instead of on every call.
* The monthly order count no longer builds a list of every order id of the month in memory to measure its length.
* Removed a stored column and two editable strings that nothing wrote or read, an obsolete email-subject setting superseded by the per-status ones, and three unused methods.
* The "cost of returns" and "deductions for loss of value" figures no longer appear on the Statistics screen. No request column records either amount, so both were a permanent zero presented as a measurement. The aggregate columns stay for a later release that records them.

= 0.32.0 =
* New Statistics screen, built on aggregate counters kept in their own tables. Nothing on it is computed by querying the request rows: a period is aggregated once after it has ended, the row is frozen, and the source rows carry on ageing out under the retention rules with nothing depending on them. The aggregate tables hold no order reference, no identifier, no hash and no date, so there is no path from a figure back to a request.
* Figures small enough to describe a single request are withheld. A count below the new "Smallest figure to publish" setting (default 5) prints as a dash; because hiding one cell of a row whose total is shown reveals it by subtraction, a second cell is withheld alongside it, and where no second cell exists the row total is withheld instead.
* A closed period is never recalculated. A declaration that arrives by post for a month already aggregated is counted in the month still open, so a published figure does not change afterwards.
* Sections: money kept versus refunded, with exchanges and refusals counted separately rather than merged; variations by the absolute number returned, always beside the number sold; reasons crossed with the route they were filed on, plus a separate block for reasons blaming the shop; typical time from declaration to money, as a median rather than an average; the missed-deadline log by month; and the return rate as a fraction.
* Reason history is stored by key, so renaming a reason in the settings no longer splits its history in two.
* The triage bar above the request list is now a single line and carries only what needs attention today — overdue refunds, deadlines within three days, requests stalled beyond the new configurable threshold, and requests held for review. It collapses to a thin line when all four are zero.
* A build-time guard fails the test suite if any column naming an order, a person, an address, a token, a hash or a date is added to the aggregate tables.

= 0.31.0 =
* The voluntary return route is no longer rendered while the statutory withdrawal window is still open. The two read as the same offer at that point and the statutory one carries no conditions, so showing both only asked the customer to choose between a right and a lesser version of it. Once the statutory window closes, the voluntary route appears and the statutory one is shown closed with its reason.
* The route state returned by the lookup gained a `visible` flag. It governs rendering only: the server still accepts a route it did not render, so a stale script or a direct post cannot cost a customer their statutory declaration. A request filed on the voluntary route inside the statutory window is stored as before, with the withdrawal recorded underneath it.
* Declaration timeliness is now shown in the admin. Whether a declaration left the customer inside the statutory window is measured against the handover of the goods, so one posted on the last day counts however long it travelled. The request list and detail panel flag a late or undecidable one. Advisory only — nothing is rejected on it.
* The date hint on the manual entry form now names the two channels separately. For email it asks for the timestamp in the message header rather than the day the message was opened, so one that sat in a spam folder still counts from when it arrived.
* With scripting off the portal now renders a visible panel carrying the contact details instead of a line of small print.

= 0.30.0 =
* A replacement can no longer be recorded as delivered before it was sent. The date previously accepted any past day, which silently shortened or closed the return window the replacement carries.
* A declaration whose dispatch date is typed later than its arrival date now ignores the dispatch date instead of treating an in-time declaration as late, which had removed the statutory refund clock.
* The route checked before a request is stored is now the route actually filed. A replacement payload files an exchange whatever the posted route says, so the per-line and order-level checks weigh the exchange rules rather than the posted ones.

= 0.29.0 =
* Security release. Customer-supplied text in acknowledgement and operator emails is now escaped; only merchant-authored lines keep their markup.
* Values substituted into email templates are escaped, and every mail subject is collapsed to a single line so a value cannot append mail headers.
* The submit endpoint now enforces its own per-address limit and honours a block placed at the lookup step.
* A ticket is claimed atomically before the request is written, so two simultaneous submits with one ticket can no longer both be filed.
* Throttle counters ignore surrounding whitespace and letter case, closing a way of getting a fresh allowance for the same order number.
* An unknown order number now costs the same time as a wrong email, so the two cannot be told apart by response timing.
* Anonymised requests no longer collide on the unique replay index; previously only the first row in a retention run was anonymised.
* Removed the duplicate detector, which had not marked anything since per-unit availability replaced it.

= 0.28.0 =
* An item received as a replacement from an earlier exchange now has its own return period, counted from the day that replacement reached the customer rather than from the original delivery. The length is the new "replacement return days" setting.
* Windows are now evaluated per item instead of per order, so one order can hold an ordinary item and a replacement item running on different clocks at the same time.
* A replacement item is offered the return route only. It carries no statutory withdrawal period, because an exchange concludes no new contract, and it is not exchanged a second time.
* Until the replacement's delivery date is recorded, its return period stays closed and the customer is pointed at direct contact, rather than a period being guessed from the original order date.
* A route stays available for the order while at least one item still offers it; each selected item is checked again on submit, so an item the route is closed for is refused rather than filed.
* Three new editable texts for these cases: replacement window closed, replacement window note, and replacement not yet delivered.

= 0.27.0 =
* The portal now offers exactly three routes: the statutory withdrawal, a voluntary return, and an exchange. Each has its own configurable window and closes on its own day.
* A closed route stays visible, disabled, with the reason. When no route is available the form is replaced by a notice and the contact fallback.
* A faulty or incorrect item is no longer a separate route. Mark such reasons with a leading `*` in the reason list; a request filed with one is flagged for the operator.
* Any refund request filed inside the statutory window records the statutory declaration underneath, so a customer cannot lose the right by picking the wrong item in the list.
* Submitting a route that has closed is refused with that route's own message instead of being reclassified.

= 0.10.4 =
* Emails are now HTML with each field on its own line; the acknowledgement receipt time uses a stable 24-hour format.
* Admin and acknowledgement subjects render {ref} {order} {name} tokens; the admin email includes the customer email.
* Per-status customer emails (opt-in, off by default); the whole email is localisable through editable Settings texts.
* Reason and comment become required outside the statutory withdrawal window.

= 0.9.0 =
* Bot protection: signed time-token, optional Cloudflare Turnstile, HMAC IP-hash auto-block, optional country logging, and a lookups security panel with a configurable window.
* Requests: multi-select status filter, inline detail with live contact and shipping address, reasons statistics box, order-screen banner and per-line-item return marker.
* Admin-UI language toggle (English / Deutsch / Українська) plus shipped de_DE/uk translations. Instant field tooltips.

= 0.8.1 =
* Setup screen simplified to hand over the shortcode only; the plugin no longer creates pages. The merchant places the shortcode on any page they already have.

= 0.8.0 =
* Careback now has its own top-level admin menu (Requests / Setup / Settings) instead of entries under WooCommerce.
* New Setup screen: the portal shortcode and a readiness checklist.
* Editable reason list, per-string text overrides for localisation, optional-field toggles (reason / comment / quantity) with legal guards, and editable email templates.
* The security-event log is now included in the retention job.

= 0.7.0 =
* Returns are now visible from the order: order notes on every transition and a panel on the order screen. The order status itself is still never changed by the plugin.

= 0.6.0 =
* Flow model of every branch; undelivered acknowledgements are now recorded and surfaced to admins.

= 0.5.0 =
* Failed-attempt logging, admin search and pagination, explicit uninstall behaviour, distribution files.

= 0.4.0 =
* Status history, lifecycle actions, customer status updates, per-item quantities, reason reporting, personal-data export and erasure.

= 0.3.0 =
* Local test harness: PHP syntax checks, unit, stress and end-to-end suites.

= 0.2.0 =
* Single-use step tokens, required name, field length limits, retention for never-closed requests.

= 0.1.0 =
* First working version.
