=== smartEngin buy - full changelog ===

The plugin page on wordpress.org shows only the current release. Everything below
is the complete history of this code base, including the versions that were only
ever distributed from smartengin.de before the plugin joined the directory.

== Changelog ==

= 1.6.6 =
* The read-only AI connection now closes itself completely. The connection code always expired on its own, but the "Allow the AI connection" tick stayed on, which reads as "open" and left the endpoints answering requests for no reason. The whole connection now switches itself off: the moment the code expires, and 24 hours after being switched on (or the code revoked) with no code in place. The screen says when and why. Nothing to remember, nothing to switch off by hand.
* Fixed: every call to that connection was written to its access log twice, so the list showed thirty rows for fifteen visits. WordPress asks a permission callback a second time after answering, only to build the Allow: header, and the plugin counted that as a second visit. The same doubling made a wrong code count twice, locking a caller out after five wrong attempts instead of the ten documented. The answer is now worked out once per request. Nothing about who is let in has changed.
* Fixed: after paying with a redirect payment method the buyer came back to the wrong page. The return address was built from the path only, so anything after the question mark was dropped. It now keeps the query string, minus this plugin's own return markers. The order itself was never at risk — it is finalized by the payment_intent.succeeded webhook either way.
* Fixed: the box shown after such a return did not carry an add-on's success address, so the wrong confirmation could be shown. It now inherits it from the checkout box on the page; the same applies to the return from a subscription checkout.
* Klarna, Amazon Pay, Bancontact, eps and every other Stripe payment method that redirects the buyer now work for one-off purchases. Stripe refuses to confirm such a payment without a return address and none was given, so those buyers only ever saw an English error message. The shop now hands Stripe a return address (built on the server, checked against your own domain) and finishes the purchase when the buyer returns; if they close the browser instead, the payment_intent.succeeded webhook completes the order as before. Nothing to set up. Card payments and subscriptions are unchanged.
* The payment markers Stripe appends on the way back, the payment secret among them, are removed from the address bar right away.
* Fixed, same root cause as the previous entry in a second place: the duplicate-request key for creating a recurring price did not include the gateway mode, so the first live subscription after a switch from test mode failed. The mode is now part of the key; the protection against double-clicks is unchanged.
* Fixed: no subscription could be bought after switching a payment gateway from test/sandbox to live. The cached recurring Stripe price and PayPal billing plan did not record which mode they were created in, so the test object stayed cached after the switch and was then sent with the live key, and every subscription attempt failed at the last step. The mode is now part of the cache signature. One-off purchases were never affected.
* The invoice CSV export now applies the same spreadsheet formula protection as the other exports to its text columns: buyer-typed values (name, address, e-mail) starting with = + - @ get a leading apostrophe, so Excel/LibreOffice can no longer run them as formulas. Number columns keep their minus and stay summable.
* The credit-note e-mail now reassures the customer about the money: unless agreed otherwise, the refund was initiated through the payment method used for the purchase, and depending on the bank or payment provider it can take a few days to arrive. (If you have written your own credit-note template, add the sentence there yourself — your text always wins over the built-in one.)
* Security hardening (wordpress.org review round of 26 Aug 2026). Every value the plugin prints is now escaped at the very point of output; plugin-built HTML fragments pass through wp_kses() with an explicit allow-list (sels_allowed_html, extendable by add-ons via the filter of the same name).
* The DE/EN language switcher only acts on links carrying its own security nonce. A plain ?sels_lang= link from a foreign page no longer changes anything — neither the cookie nor a logged-in member's saved language (CSRF fix). The switcher's own links carry the nonce automatically.
* Database queries taking a list of ids now bind every id through $wpdb->prepare() placeholders.
* File exports (CSV, JSON backup, Markdown guide) are written to the output stream instead of being echoed; the CSV keeps its spreadsheet formula protection and its bytes are unchanged.
* The price shortcode is called [sels_preis], with [sels_price] as its English alias. Both names carry the plugin's four-character prefix, so no other plugin can claim them. The backend screen "Prices in texts" shows the ready-made line for every purchase option to copy.
* File storage: a code path that copied a deliverable from disk instead of receiving it as an upload has been removed; it was unreachable, as every call site passes a real upload. Deliverables are stored through WordPress' own uploader (wp_handle_upload) into a protected folder inside the uploads directory, resolved at runtime with wp_upload_dir() and closed to direct access.
* Package storage moved to the plugin-specific uploads sub-directory (uploads/smartengin-buy/packages). Existing files are migrated automatically and safely on the first request after the update; stored references are plain file names, so nothing in the database changes.
* The protected directory's guard files (.htaccess and index.php) are ensured before every write into it, not only on activation.
* This edition sells digital files and courses. It contains no executable software-update infrastructure: no update endpoint, no update token, no update package upload, and no code that hands update information to WordPress. A few database columns inherited from the shared code base (such as the stored version or package file name) are kept as passive compatibility data so a site can move between smartEngin editions without losing anything — nothing in this edition acts on them.
* This plugin never changes another plugin's active state. If smartEngin Licence & buy is active, activating this one is refused with a message asking you to deactivate that plugin first — the decision stays yours, and nothing is deleted either way.
* Removed the last remnants of software-update plumbing inherited from the shared code base (an update download token and its special case in the download limit). This edition sells digital files and courses; it serves no software updates, and every download is counted normally.
* Removed the last dead references to the paid edition's licence and update endpoints: the REST hook that would have registered them, the "Server has moved to" setting that only they use, and the comments describing them. None of it could run here; it is simply gone now.
* No functional changes intended: rendered pages and export files were compared byte for byte before release.

= 1.4.11 =
* The product form now explains its fields: what belongs in each one and what happens if a value is wrong. (The update fields themselves belong to the paid package.)

= 1.4.6 =
* Fixed: a credit note could end up with NO currency at all — the amount then stood there as a bare number on the credit note itself, in the invoice list and in the customer account. It happened when the invoice being credited had an incomplete stored snapshot; the credit note inherits that snapshot, and the currency was taken from it without a fallback. A credit note now takes the currency of the invoice it corrects, and a document can no longer be written without one.

= 1.4.5 =
* Tested with WordPress 7.1 (released 19.08.2026) and marked as such. Everything was actually run against 7.1, not just read: purchase, invoice, credit note, customer account, subscription portal, all backend screens and all server endpoints. Nothing WordPress removed or retired in 7.0/7.1 is used anywhere in the plugin.
* Fixed for WordPress 7.1: the editor now always builds the page preview inside its own frame, and offer-card styling only reached that frame by accident, which WordPress rebuked in the browser console on every editor load. The styling is now registered the way WordPress asks for, so the preview keeps its look for good. Customer-facing pages were never affected.
* Smaller correction: an empty call to the PayPal notification endpoint (typically a scanner, never PayPal itself) wrote two "deprecated" lines into the error log. Payments were never affected; the log stays quiet now.
* Fixed: this package’s own help text named the paid package as „smartEngin buy“ — that is, as itself — in the very sentences explaining why this package issues no licence keys. The short overview also promised a licence key, which this package never hands out; it now correctly says a protected download link is e-mailed.

= 1.4.4 =
* Fixed, and this one silently cost money and paperwork: for FILE products and for add-on products (e.g. courses from smartEngin Courses, appointments from smartEngin MeetMix) a Stripe refund was never recognised. Completing the purchase erased the payment id Stripe later refers to, so the refund notification found no order: the order stayed on "paid" and NO credit note was written. PayPal refunds were not affected. The payment id now survives the purchase, and every product type is refunded correctly. Please note: orders completed BEFORE this update have already lost the id — refunding them still needs the credit note to be written by hand under Invoices.

= 1.4.3 =
* Fixed, and this is the one to update for: on the offer cards, NO message ever reached the buyer. Every hint and every error the card wanted to show ("please enter your e-mail", "please fill in your billing address", "this coupon works with card payment", "payment cancelled", any error from the payment provider) was swallowed — and some of them broke the click outright, so the button simply did nothing and the buyer was left staring at an unchanged page with no idea what was wrong. Two different helper functions in the card's JavaScript had ended up with the same name, so the one that displays the card's messages was replaced by the one that belongs to the coupon field. Anything that only warns or explains was affected; taking payment itself was not, which is why it stayed unnoticed for so long. All messages appear again.

= 1.4.2 =
* Fixed: a refunded SUBSCRIPTION charge was not recognised — Stripe refunds of renewal charges found no order (only the first payment's id is stored), and PayPal's PAYMENT.SALE.REFUNDED event was not handled at all. Both now find the charge through its own renewal invoice: the customer gets a credit note for exactly that charge, and the order and the subscription keep running. If you take PayPal subscriptions, add PAYMENT.SALE.REFUNDED to your webhook subscription in the PayPal dashboard — the instructions under Settings and Help & docs now list it.
* Refunds that arrive for a payment this site cannot match are now written to the security/activity log instead of being dropped silently, so a missing credit note has a visible cause.

= 1.4.1 =
* Fixed, and worth updating for on its own: deleting a product stopped halfway with a critical error — a leftover call into the licence module, which this package does not carry. The product's purchase options were already deleted at that point while the product itself remained. Deleting a product works again, cleanly and completely.
* Fixed, and this one touched money and paper: a subscription sold with the smartEngin Courses add-on (or a file subscription) ran the WHOLE first purchase again on every renewal charge. The customer received the purchase-confirmation e-mail again with every charge, a coupon with a limited quota was counted as redeemed again every time — and the renewal itself got NO invoice of its own, so money arrived without a document. A renewal now writes its own invoice per charge and sends no repeat mail. Reloading the thank-you page after a subscription purchase could trigger the same repeat; that is closed too.
* Fixed: the thank-you page and the payment provider's server notification often arrive within the same second, and both could complete the same order — two confirmation mails and a coupon counted twice. The database now picks exactly one winner; the second arrival answers from the stored order.
* Fixed: a PARTIAL refund (say 5 of 49 euro, out of goodwill) was treated like a full one — the order was marked refunded, a course customer lost access, and the credit note could be written over the full invoice amount. Only a full refund revokes now; a partial refund gets a credit note over exactly the refunded amount and everything else stays.
* Fixed: a coupon on a subscription CART with card payment reached the order and the invoice but never Stripe — the customer's card was charged the full first rate while the paperwork said the discounted one. The discount now reaches Stripe exactly as it does in the single checkout (first charge only). The same cart with PayPal would have applied the discount to EVERY rate silently; that combination now gets the same clear message as the single checkout: pay by card, or subscribe without the code.
* Fixed: a fixed-amount coupon (say 10 EUR) applied its number to purchase options in ANY currency — 10 EUR became 10 USD. It now refuses options priced in a different currency; percentage coupons are unaffected.
* Fixed: two customers buying in exactly the same moment could collide on the running order number, and the second one saw a server error instead of a purchase. The second order now simply takes the next free number.
* The readme now says what happens when you switch to smartEngin Licence & buy (the two packages deactivate each other on activation, all data stays), and no longer claims a bundled German translation — translations come from translate.wordpress.org.

= 1.4.0 =
* Uploads now go through WordPress' own uploader (wp_handle_upload). Your files still land in a
  protected folder outside the media library and are still stored under a neutral name, so no web
  server ever serves or runs them — only a signed download link does. What changes is the list of
  formats: this plugin now offers the 45 file types WordPress itself recognises. Formats it does not
  know — e-books (epub, mobi), 3D models (stl, blend), fonts, CAD drawings, installers and most
  design source files — can no longer be uploaded here.
* Fixed: opening Help & docs produced a PHP warning on any site running in debug mode, and the
  briefing it builds for an AI assistant carried the literal word "Array" where the per-product
  file overview should have been. Both had the same cause. That overview now reads as an indented
  list per product, so the assistant gets a real answer where it previously got nothing.
* Translations now come from translate.wordpress.org instead of being bundled with the plugin.
  That is how translations reach a plugin hosted here: generated for every locale, open to
  community contributions and delivered by WordPress itself as language packs. Until the strings have
  been entered there, the plugin shows English.
* Fixed, and this one bit anyone moving up to the paid package: deleting this plugin after
  installing "smartEngin Licence & buy" damaged the plugin they had just switched to. Both are
  built from one code base and share everything — the same tables, the same settings, the same
  signing key, the same permission — and this plugin's uninstall routine took those with it. The
  menu vanished for every role, and the key that signs download links was regenerated, so every
  download link already sitting in a customer's purchase e-mail stopped working. Sales data itself
  was never touched. An uninstall now checks whether the sibling package is still installed and, if
  so, leaves everything alone. Removing the last of the two still cleans up as before.
* Fixed, and this one could have cost you sales without a trace: the dashboard reported everything
  green while one payment provider was live and the other was still in test mode. The "Ready to go
  live" check only asked whether SOME provider was live, so a shop taking real money through PayPal
  while Stripe sat in test mode looked perfectly configured — and a customer paying by card was sent
  into Stripe's test environment. The check now looks at every enabled provider and turns amber
  while one of them is still testing, naming which one.
* Fixed: the dashboard item "Every product has a file to deliver" counted appointment and event
  products as products without a file. Those deliver no file by design — their add-on provides
  the service — so the item could never go green, no matter what you uploaded, and its link sent
  you to a product screen with nothing to find.
* New: the product list has a "File" column showing how many files each product actually hands
  over, including anything hanging on a purchase option. Green when a buyer gets something, red
  and clickable when they would not, and a plain dash where there is nothing to check. Hovering
  the number says what it is made of. Until now you were told that SOME product had nothing to
  deliver, never which one.
* The same column catches a trap nothing used to show: a purchase option ticked "Free download"
  that has no file of its own does not hand out something free instead of the paid product — it
  hands out the paid product itself, for nothing. The column turns red and names the option.
* An add-on now says whether it draws offer cards, and the product screen follows that. Where cards
  are drawn (smartEngin Courses: one per course, plus one per bundle), the sort order, the
  "featured" and "sellable" ticks and the full card design stay exactly where they were. Where only
  the payment box is embedded, those fields are left out because they could not reach a buyer. Same
  for the right-of-withdrawal tick: it disappears only where the add-on demonstrably asks for the
  waiver itself.
* A product that another smartEngin plugin maintains now shows only the settings that actually
  do something for it. Everything that could never take effect is left out of the form instead of
  sitting there greyed out: the download fields, sort order, the flags, the extra-file list, the
  shortcode box and the card settings that only apply to an offer card. Whatever is hidden keeps
  its stored value, so nothing is lost and nothing is quietly reset when you save.
* Fixed, and this one was invisible: the payment box such a plugin embeds always used the default
  button colours. Whatever you set under Card design reached your own offer cards but never that
  box, so the same product looked different in two places. It now takes the product's colours and
  icons for the Card/SEPA and PayPal buttons.
* The right-of-withdrawal tick is no longer offered on such a product. A plugin selling a service
  (an appointment) asks for the waiver itself, before checkout, under the rule that fits a
  service — this tick is written for digital content. With both active the buyer would have been
  asked twice, the second time with the wrong wording. The form now says who handles it.
* Fixed: a product's reminder wording was silently emptied whenever the product was saved from a
  screen without the Reminders tab. Anything written there was gone with no warning; it survives now.
* New: other smartEngin plugins can now sell through this checkout. A purchase can carry a
  reference of its own, a new `sels_order_created` action hands it over the moment the pending
  order exists, and a buy box can name where the buyer goes after paying. Built for smartEngin
  MeetMix, which uses it to sell appointments: your visitor books a time, pays here, and the
  appointment becomes firm — with your invoice, your coupons and your tax settings, from one
  place. Nothing changes for the products you sell yourself.
* New product type "appointment" for such add-ons: it delivers neither a licence key nor a
  file, because the service itself is what was bought. It is created by the add-on, never in
  the product form, and a save there can no longer turn it into something else by accident.
  An add-on registers its own type, and the product form then names that type and explains
  who owns it, instead of offering a choice that does not apply. Such a product also gets its
  own badge in the product list rather than being labelled "Licence".
* Fixed: when another plugin saved one of your products or purchase options, every field it did
  not mention was reset — a VAT rate to 0, a description to empty, and on a purchase option even
  "active", so the option could disappear from your offer cards with nothing explaining why. What
  is not mentioned now stays as it is. Saving from the product form is unaffected.
* Safer: on a product an add-on maintains, the price, VAT rate and name are shown but no
  longer editable here. They used to be plain fields — a change looked like it had worked and
  was quietly overwritten on the next purchase, which is worse than a field you cannot touch.
  You now see the value together with a note saying who maintains it.
* New: the Settings screen now states, right in its header, which language your customers are served in — and it reads the answer from your site instead of just explaining it. WordPress has two language switches and they are easy to confuse: "Settings → General → Site Language" decides everything a customer sees (offer cards, checkout, invoices, the account page and every e-mail), while the language in your own user profile only changes the admin screens YOU look at. When the two differ, the header says so plainly, so a language question can be settled in seconds instead of being mistaken for a bug.

= 1.3.1 =
* The built-in AI briefing (Help & docs → "Copy setup briefing for an AI assistant", and the read-only REST guide) now describes exactly THIS package. It no longer explains licence keys, activation limits, trials or the partner program — concepts that exist only in the paid package — and no longer lists a "Licences" menu entry that does not exist here. An AI assistant guided by it can no longer send you looking for screens this plugin does not have.

= 1.2.1 =
* FIX: the new language switcher won the click but lost the page. A signed-in course customer could see it set to English while the text around it stayed German — *smartEngin Courses* keeps a language preference of its own, and depending on which plugin loaded first, the wrong one decided. One rule settles it now: a click on the switcher wins immediately and updates the member-area preference with it; without a click, a signed-in course customer keeps the language they chose in the member area. One visitor, one language, whichever way you switch.

= 1.2.0 =
* New: a small "Deutsch · English" switcher in the customer area — top right on the magic-link form, on the account page after signing in, and on the subscription self-service form. It starts in the language of the website and remembers the visitor's choice for a year, which helps when a site sells in German but the occasional customer reads English, or the other way round. The choice also applies to the e-mails this plugin sends that visitor, such as the magic-link e-mail. Mail templates you wrote yourself are never touched.
* FIX: e-mails to customers triggered from the backend (re-sending an invoice link, issuing a credit note, announcing a new file to subscribers) were worded in the ADMIN's profile language, not in the language of the website. An admin who runs wp-admin in English would unknowingly send English mails to the German customers of a German site. All customer mails now follow the language of the website; the "send test to me" buttons show exactly what the customer would get.
* FIX: on German sites, the column above the invoice numbers (customer account, backend invoice lists) said "Anzahl" — the translation had confused number-of-things with number-of-the-invoice. It now says "Nummer".

= 1.1.6 =
* The pointers to the two sibling plugins now show their names in the house style: bold, "smart" and the trailing part dark blue, "Engin" green.
* FIX: every backend screen carried the badge "smartEngin Licence & buy" — the name of the separate paid plugin. It now says "smartEngin buy", the plugin you actually installed.
* FIX: the product form showed a red note about the licence products of the separate paid plugin, which read as if you already owned that plugin and had missed a step. This package has no licence type at all, so the note was wrong twice over. It is gone.
* In its place, and only where something is genuinely missing, there is now a short pointer to the two plugins that go with this one: smartEngin Licence & buy (sell your own software with licence keys) and smartEngin Courses (sell online courses). They sit where the gap is actually felt — the product form — and once more in Help & docs. Both disappear as soon as you have the plugin in question. No dashboard banners, no pop-ups, nothing dismissible to click away.

= 1.1.4 =
* Many more file types can be sold: documents and e-books, Office and data files, archives (ZIP, RAR, 7Z, TAR…), images, audio, video, design source files (PSD, AI, EPS, Figma, Sketch, XD), software installers (EXE, MSI, DMG, PKG, DEB, APK), 3D models (STL, OBJ, FBX, GLB…), CAD drawings (DWG, DXF, STEP…) and fonts (TTF, OTF, WOFF, WOFF2) — around 70 types in total. Web and code files (PHP, HTML, JS, CSS) are deliberately not accepted on their own; they are sold as a ZIP, which is how templates and code bundles ship anyway.
* The file dialog now offers exactly what the server accepts. Until now it was a hand-written list that had drifted apart from the check, so it hid types (CSV, SVG, WebP…) that would have been taken.
* Hardening: uploaded product files are now stored with a neutral ".bin" extension, the real one kept inside the name. The package folder is locked by an .htaccess, which Apache honours but nginx ignores — there a stored SVG could be opened straight from the browser and would run any script inside it. No web server executes or renders a ".bin", so the protection no longer depends on the server software. Customers still receive the correct file name; existing files are renamed automatically on update, and are moved back untouched if anything about the rename fails.

= 1.1.3 =
* Every hint that names a backend click-path now says "smartEngin buy → …" — the menu this plugin actually has. Before, many hints (help page, notices, the AI briefing) said "Licence & buy → …", the menu name of the paid package, sending you to look for an entry that does not exist here.
* First round of wording fixes for the same reason: the Dashboard subtitle, the Products page description, the order-deletion texts and the 100 %-coupon hint no longer speak of licences — this package issues no licence keys, it sells files and courses. The paid package keeps its wording.
* Removed a stale, unused copy of the AI briefing (docs/AI-SETUP-GUIDE.md, frozen at 0.50.0). The live briefing under Help & docs and the REST /guide endpoint are always current and remain the real sources.
* Only wording and that one dead file changed — no data, no setting and no purchase is affected.

= 1.1.2 =
* The plugin no longer shows two things it cannot do: the "Licences" figure on the Dashboard and the "Licence server address" setting. Both belong to the paid "smartEngin Licence & buy" package and had stayed visible here. Nothing else changed — no data, no setting and no purchase is affected.

= 1.0.3 =
* FIX: on the offer cards, the coupon field stood open from the start — right under its own "Redeem a coupon code" link, which made no sense to look at. Clicking the link only made the LINK disappear, and reloading brought it back. The field is meant to stay closed until the customer asks for it, exactly like the e-mail field.
* Cause, worth naming because it is invisible from the outside: the markup did say "hidden", but the browser puts that instruction in its own weakest rule book, and a single line of the plugin's own styling overruled it. One guard line per front-end area settles it for good, including anything added later.
* No behaviour changed — nothing was ever mis-sent or mis-counted, only shown.

= 1.0.1 =
* FIX: buying from a page served by a page cache could fail for good. The security stamps baked into the sales page outlive any cache: a stale page answered every purchase with "your session expired", and reloading fetched the same stale page again. The checkout script now fetches fresh stamps on load — through a channel page caches never store — and again when the visitor starts interacting, so a tab left open overnight buys just as reliably.
* FIX: "remove all data" on uninstall now really removes all data. The settings (including payment-provider credentials) survived it before; the transient sweep also covers every counter now instead of a stale list.
* Dashboard: the revenue curve is summed per sale day in the database instead of loading every paid order into memory — same numbers, and the page stays fast when the shop has grown to thousands of orders.
* readme: documented what to check when the site runs behind Cloudflare or another proxy (real visitor addresses must be restored by the host, or the purchase brake and the security log see one shared address).

= 1.0.0 =
* First public release of the free sales core: products, purchase options, cart, Stripe and PayPal, invoices with German small-business support, coupons, customer account and reminders.
