=== WooCommerce Redsys Gateway ===
Contributors: j.conti
Tags: Redsys, Gateway, WooCommerce
Requires at least: 5.4
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 32.1.0
License: GPLv3 or later License
URI: http://www.gnu.org/licenses/gpl-3.0.html


== Agentic Commerce ==

This plugin exposes WooCommerce stores to AI agents (ChatGPT, Claude, Gemini,
Perplexity) via two parallel protocols:

 * ACP (OpenAI/Stripe Agentic Commerce Protocol). Enabled by default.
 * UCP (Universal Commerce Protocol, NRF 2026). Disabled by default.

Configure them under WooCommerce → Settings → Agentic Commerce.

Discovery URLs:

 * ACP: https://your-site.com/.well-known/agent-commerce.json
 * UCP: https://your-site.com/.well-known/ucp.json

See `desarrollo/ACP-API-DOCS.md` and `desarrollo/UCP-API-DOCS.md` in the
plugin folder for full API references (endpoints, OAuth/Bearer auth,
webhook signatures and supported events).


== Installation ==

 * Unzip the files and upload the folder into your plugins folder (wp-content/plugins/) overwriting old versions if they exist
 * Activate the plugin in your WordPress admin area.
 * Open the settings page for WooCommerce and click the "Payment Gateways" tab
 * Click on the sub tab for "Redsys/Servired"
 * Configure your Redsys settings.
 * Configure Sequential Invoice Number
 * Activate Sequencial Invoice Number


== Changelog ==

= 32.1.0 =
* SECURITY: The plugin was writing the address of every shopper's order-received page into its own
  log file on every payment, and that address contains the order key - the private token that
  WooCommerce puts in the shopper's payment link and that this plugin requires before it will show
  an order. Anyone able to read the log file could therefore open those orders and see the name,
  email, telephone and addresses on them. It happened whether or not the shop had switched debug
  logging on, because this one place wrote to the log without checking that setting. It now respects
  the debug setting like every other log the plugin writes, and the key is replaced with
  "[redacted]" before anything is written, so it is not recorded even with logging switched on. That
  replacement was then applied to every log line the plugin writes rather than to the one that was
  reported: around ninety further lines across the gateways printed the same addresses whenever
  debug logging was on, including the ones that dump the whole message sent to the bank. Log files
  already written still contain those addresses: if you have them, delete them.
* FIX: With InSite, paying with a card the shopper had saved could take the money and still show the
  order as failed. When the bank approves such a payment without asking the shopper to confirm with
  their bank app - which is what happens with a saved card - the plugin was looking for the result
  in the wrong place, found nothing, and treated "nothing" as a refusal. The charge had already gone
  through, so shoppers who were told the payment had failed paid a second time and were charged
  twice. The result is now read from the bank's own signed answer, the same one the rest of the
  payment already uses. It affected the classic checkout and the block checkout equally, and it also
  affected subscription renewals. Reported by two shops within days of each other, one of whom
  traced it to the exact line - thank you.
* FIX: On the card gateway, a payment made with a saved card was completed on the strength of an
  authorisation number alone, without checking the bank's actual answer beside it. A refused payment
  that still carried something in that field would therefore mark the order as paid, and the shop
  would send the goods without having been paid. Both are now required to agree before an order is
  completed. If the bank's answer is missing altogether the payment is not completed, because there
  is nothing left that says it was approved; a shop whose bank genuinely omits that field can
  restore the previous behaviour on purpose with the redsys_allow_payment_without_ds_response
  filter, which is documented and which cannot be used to accept a payment the bank actually
  refused.
* FIX: On stores using certain checkout plugins - Fluid Checkout among them - every payment was
  refused by Redsys with the error SIS0574. The bank requires a short description of the shopper's
  browser with each payment (its language, its screen size and similar), which the plugin collects
  through hidden fields it adds to the checkout. Those fields were being added at the moment the
  card gateway was built, which on those stores happens after another plugin has already asked
  WooCommerce for the definitive list of checkout fields - and WooCommerce builds that list once and
  never again. The fields were therefore never created, never filled in and never sent, and the bank
  refused the payment. They are now added as soon as the plugin loads, before anything can ask for
  the list, which affects every gateway that needs them - the card gateway, Bizum and the wallets -
  not only InSite. In InSite the browser description now also travels with the rest of the payment
  data instead of depending on the order alone, so it survives a checkout that rebuilds itself.
  Reported by an integrator who had already proved the diagnosis - thank you.
* SECURITY: The page that forwards a shopper to Redsys (and to Bizum) accepted any order number
  typed into its address, without checking that the person asking for it was the person who placed
  that order. Because order numbers run in sequence, someone could step through them and read the
  data the plugin sends to the bank for other customers' orders: name, email, telephone and the
  billing and delivery address, together with the amount and the description of what was bought. No
  card number, no security code and no signature key were ever exposed, and nothing could be paid or
  modified this way. That page now requires the order key that WooCommerce puts in the shopper's own
  payment link, so a request that does not belong to the order is refused with a "Forbidden" page
  instead of being answered. The same check was added to the payment pop-up on the checkout page.
  Reported by the security team at Tivify (TVUP Streaming Media), who described the problem clearly
  and gave us the time to fix it before saying anything publicly - thank you.
* FIX: If a payment was cancelled or refused, the shopper came back to the shop but the order was
  left pending and the basket was not restored. The address the plugin gave the bank to send the
  shopper back to was written in the form used for links inside a web page, not the form used for a
  real address, so the shop received the return without the order reference, its number or its
  security token and had nothing to cancel. It affected Bizum, Google Pay, Apple Pay, direct debit,
  bank transfer, InSite and the card gateway whenever the "return to" setting is set to cancel the
  order. On MasterPass the same address was being sent completely empty, because of a typing mistake
  of a single character repeated on three lines. All of it now goes through a single shared piece of
  code, so a gateway added in the future cannot get it wrong again.
* FIX: In the pop-up (modal) payment mode, cancelling a payment did nothing and the shopper was left
  on the cart with the order still pending. The address the plugin sent the browser to was being
  written into the page in a way the browser reads as a link inside a document rather than as a web
  address, so everything after the first parameter was discarded - the order reference, its number
  and its security token all disappeared, and WooCommerce had nothing to cancel. The same flaw
  affected the address the shopper is returned to after paying, where it silently dropped a tracking
  parameter. Both are fixed, in the card gateway and in the InSite gateway, and the corrected
  behaviour was verified in a real browser.
* FIX: The checkout could die with a critical error, instead of sending the shopper to the bank, for
  a logged-in customer whose customer record had never been changed since it was created. The plugin
  builds a set of security data for the bank on every payment, and one of the fields is the date the
  customer account was last modified - which WooCommerce leaves empty for an account that has never
  been touched, typically one created by an import, a migration, or automatically by another plugin.
  The plugin read that empty date as if it were a real date and the payment page stopped there. It
  now falls back to the date the account was created, which is what "never modified" means, and the
  same protection was added to the equivalent code used by subscriptions - where the same empty date
  was silently being reported to the bank as "modified today", which is wrong information for the
  bank's fraud checks.
* SECURITY: The three preauthorization buttons in the order screen (authorise, cancel, charge a
  deposit) and the PayGold customer search accepted their requests without a security token. The
  permission check was already in place, so only a shop manager or administrator could ever use
  them, and nobody else could have run them - but an administrator who was already logged in could
  have been tricked into firing one from another website without noticing. All four now carry and
  verify a WordPress security token, which closes that path.
* FIX: Redsys and the other gateways could disappear from the checkout for everyone, including
  ordinary shoppers, on stores where the "show only to these users" list in test mode had never been
  filled in. The plugin read that empty setting as "show it to one user whose id is nothing", which
  matches nobody, so the payment method was hidden from every visitor. It only affected stores whose
  settings were written by something other than the settings screen - an import, a migration, or the
  plugin's own management API - because saving that screen by hand stores a different empty value
  that never triggered it. Fixed in the nine gateways that shared the same code.
* FIX: Subscriptions paid with Apple Pay or Google Pay by a customer without an account never
  obtained the recurring payment permission from the bank, so the first renewal failed with a note
  saying there was no card. It happened on stores where the general "allow customers to create an
  account during checkout" option is switched off, which is common on stores that allow guest
  checkout: the wallet payment paths never saw WooCommerce Subscriptions' own override of that
  option, so no account was created and, without an account, the bank permission could not be
  stored. Fixed in the four places that handle it (Apple Pay and Google Pay, on both the classic and
  the block checkout).
* FIX: The block checkout's payment script was built against a version of React that WordPress
  itself does not accept. A development-only dependency pulled a newer React into the build, and
  WordPress rejects elements produced by it, so the payment method could fail to appear on the block
  checkout. The build now pins the version WordPress uses and the script has been rebuilt.
* CHANGE: When a subscription renewal fails, the note added to the order now says exactly which step
  failed and which merchant code and terminal were used for the attempt. Until now all six possible
  failures wrote the same uninformative note, which made a failed renewal almost impossible to
  diagnose without access to the store - especially on stores where the debug log is not being
  written.
* CHANGE: When Apple Pay cannot validate the shop with Apple, the log now records Apple's own
  explanation and whether the certificate files can be read. Apple answers with a generic
  "Expectation Failed" and puts the real reason in the body of its reply, which was never being
  recorded, so those failures used to be a dead end.
* NEW: The WooCommerce status report (WooCommerce - Status) now also lists what is switched on in
  the Redsys "Advanced settings" tab: push notifications, sequential invoices, QR codes, saved
  cards, order status overrides, conditional rules, subscriptions, e-mail reconciliation, the
  agentic commerce protocols and the management app API. Credentials are never printed - each one is
  reported only as set or not set. It is included when you copy the report to send it for support,
  which is the point: a support request now arrives with the configuration already in it.
* CHANGE: The distributed plugin no longer contains the project's own development and build scripts,
  its local test environment configuration, its editor and git-hook directories, its developer
  README or an internal security review document. None of it was ever loaded when the plugin runs,
  but none of it belongs on a shop's server either. What the package contains is now checked
  automatically, before every release, against a list of what the plugin really ships, so something
  added to the project in the future cannot travel inside the zip unnoticed.
* FIX: The cart and the checkout could become noticeably slow, for logged-out visitors too, on
  stores with saved cards or subscriptions. The plugin's internal helper WCRed() built a brand new
  copy of its global object every single time it was called, and that object registers three
  WordPress hooks the moment it is built. WordPress does not replace those hooks, it adds them, so
  on a store where the helper is called once per saved card or subscription the same three checks
  ended up registered hundreds of times and were re-run, all of them, every time WooCommerce
  assembled the list of available payment gateways. On a store that reported the problem this
  reached around 438 duplicated registrations per page load and about 1.7 seconds of PHP work on the
  cart page, of which only 56 milliseconds were database queries. The helper now builds that object
  once and reuses it, so the hooks are registered exactly once per request. Nothing about the
  plugin's behaviour changes: the object holds no per-request data. The same one-object-per-request
  treatment was applied to the WCPSD2() helper, which had the identical shape.
* FIX: On a small share of stores the Agentic Commerce (UCP) message-signing key was written in a
  form that could never be used, so every signature check against it failed and the published key
  document did not follow the standard. When the plugin created that key, one of its two coordinates
  occasionally came back from OpenSSL one byte shorter than the standard requires - about 1 store in
  135 - and the plugin stored it as it was instead of padding it. From that moment the store could
  not verify its own signatures, and no external agent could either, until the key was rotated. The
  key is now stored padded to the required length, and a key already stored in the short form is
  repaired automatically the first time it is read, so no merchant has to rotate anything.
* FIX: On shops running PHP 8.0 or older, switching on the e-mail reconciliation broke the page with
  a server error instead of simply not working. The plugin now checks first and leaves a clear note
  in its log. The feature itself still needs PHP 8.1.
* SECURITY: The Agentic Commerce Protocol could end up switched on without anyone choosing it, on
  shops that update automatically. It publishes addresses that machines can talk to, so it is meant
  to be off unless you turn it on - and now it is. If you did switch it on, nothing changes for you.
* FIX: Background jobs were piling up into dozens of copies of themselves in the WooCommerce
  scheduled actions list. Fixed everywhere the plugin schedules anything, and the copies already
  there are cleared automatically.
* CHANGE: The e-mail reconciliation job now exists only while that feature is switched on, and only
  once. Before, it was created on every shop and woke up every five minutes doing nothing.
* FIX: A notification to the mobile app could be lost without any error if it was sent at the wrong
  moment.
* FIX: PHP warnings appeared in the server log on every visit to the "pay for order" page, from the
  Apple Pay and Google Pay gateways. Nothing was broken for shoppers, but the debug log for those
  gateways was not recording anything from that step - so anyone trying to diagnose a problem there
  was looking at a fault, not at silence. Reported by a merchant, and found in three gateways rather
  than the one reported - thank you.
* FIX: Turning the debug log on to look into a card payment problem produced warnings instead of the
  information you turned it on for.
* FIX: The "Connect with Google" button in the e-mail reconciliation settings was never displayed.
  Granting access was already possible by filling in the Google details and pressing "Save changes",
  so this restores a shortcut rather than unlocking the feature.
* SECURITY: The Google library bundled with the plugin has been updated, fixing two flaws in the
  part that makes network requests.
* CHANGE: All the translations are complete again. Sixty-nine texts added by recent versions were
  still showing in English - settings labels, the status report, and a few messages a shopper can
  see. Spanish, Catalan, Basque, Galician, French and Portuguese are back at one hundred per cent.
* NEW: You can now decide whether customers are offered "Add a credit card for Subscriptions" in My
  account - Payment methods. Until now that option appeared by itself as soon as a subscriptions
  plugin was active, with no way to remove it, which is not what every shop wants: on a shop where
  the card for a subscription is always captured during the purchase, it only invited customers to
  store a card nothing would ever charge. The new switch is in Redsys - Advanced settings - Saved
  Cards, it starts switched on so nothing changes for shops that were already offering it, and
  switching it off leaves the 1-click card on its own. It is also reported in the WooCommerce status
  report.
* FIX: The plugin sent the shop an e-mail saying "Redsys is not sending tokenization fields" after
  payments in which Redsys had sent every one of them. The check behind that notice tested a
  variable that does not exist anywhere in the plugin, and a test on something that does not exist
  is always true, so the warning went out on every payment that saved a card. Merchants were taking
  it to their bank, which correctly answered that the terminal was fine. The notice now reports only
  real absences, and it names in the log which field was missing - the expiry date, the card brand
  or the card number - instead of only saying that something was. It also had to stop reading the
  brand after the plugin had turned it into a display name, because that conversion answers
  "Unknown" when there is no brand, which would have turned the warning into one that could never
  appear at all. The same block was writing "unknown" as the card number in the debug log on every
  payment, through a variable name that carried one letter too many; it now writes the real masked
  number.
* FIX: Searching the Redsys tokens screen (WooCommerce - Redsys tokens) by e-mail or username could
  not find any card saved after the first twenty-five. The screen asked the database for one page of
  tokens and then searched inside that page, and since it asked for them in no particular order,
  that page was always the twenty-five oldest tokens in the shop - so the recently saved cards,
  which are the only ones anybody looks for, were unreachable. The search now runs in the database,
  over every token, and the listing arrives newest first. The counter above the table and the list
  below it can no longer disagree, sorting by username or e-mail now sorts the whole table instead
  of the visible page, and the screen no longer loads every token in the shop into memory just to
  count them, which on shops with thousands of saved cards it did on every page load.
* CHANGE: The token that guards the "add a card" link is now compared in constant time, like every
  other secret this plugin compares. Timing that comparison over the internet is not a practical
  attack, so nothing was exploitable; the inconsistency was the defect, because this was the one
  comparison left behind when the rest were migrated during the signature work.

= 32.0.0 =
 * New: Manage your store from a native app. This release turns on the management app API that
   until now shipped hidden. Your store can be connected to PackDesk, the native macOS
   management app, to work with orders, refunds, products and customers. The API (namespace
   redsys-manager/v1) is strictly opt-in: OFF by default, it requires HTTPS and an active
   license, and no app can reach the store until a merchant enables it. It offers an order
   picking queue and order updates, product and customer routes, a "changes" delta feed for
   efficient sync, safe write retries via Idempotency-Key, and a per-employee rate limit
   (applied per user, never per IP).
 * New: A "Behaviors towards the APP" section in the Redsys advanced settings to enable or
   disable the API, allow or restrict refunds for the Customer service profile, require a
   minimum app version, and set the per-employee requests-per-minute limit (default 300).
 * New: A per-user "App profile" on the user edit screen, so each employee connecting from the
   app operates under a profile (for example Administrator or Customer service) that gates what
   they can see and do, including whether they may issue refunds.
 * New: An "Apps and Plugins" section in the Redsys advanced settings: a read-only overview of
   the native app and the rest of the plugins, websites and skills.
 * Change: Compatibility declared with WooCommerce 11 ("WC tested up to" is now 11.0), so
   WooCommerce no longer warns that the gateway is untested on the version you run. The minimum
   supported WooCommerce is unchanged (7.4).
 * New: PackDesk is published on the Mac App Store, and the official "Download on the Mac App
   Store" button now appears at the top of "Behaviors towards the APP" and in the featured-app
   area of "Apps and Plugins", so the app can be installed straight from the store settings. It
   uses Apple's own artwork in the administrator's language (English, Spanish, Catalan, French
   or Portuguese; English for Basque and Galician, which Apple does not publish) and opens each
   visitor's own country store. The app entry now shows the published version and states that
   it requires macOS 26 Tahoe or later.
 * Fix: The product QR code did not appear on stores whose media library is offloaded to an
   external service such as Cloudflare Images. The plugin saved and showed a URL it had built by
   hand from the local uploads folder instead of the media library's real attachment URL, so on
   an offloaded store that address pointed at a file no longer served locally and the image was
   broken. The QR URL is now resolved through wp_get_attachment_url, the attachment id is stored,
   and the address is resolved every time the QR is shown, so it follows the file wherever it is
   served - including offloading enabled after the QR was created. Existing QR codes are healed
   automatically the first time their product is opened; otherwise the "Regenerate QR Code" link
   rebuilds them correctly.
 * Fix: A Bizum payment could be charged at the bank yet leave the order pending, with the log
   showing "Signature verification failed in successful_request". The order-completion step in one
   of the two Bizum gateways verified the bank notification with the gateway's plain configured
   signature key, while the operation had been signed with the per-order key actually used for it -
   as happens on stores with a per-user or dual/test terminal, or the bizum_modify_data_to_send
   filter. The notification passed the first validation and was then rejected by this second check,
   so the money was taken but the order stayed pending. The completion step now resolves the signing
   key exactly like the notification validation - and like the other Bizum gateway already did -
   from the order meta, then the request transient, then the settings key for the order's customer,
   so both agree.

= 31.0.7 =
 * Fix: Subscription products from Advanced Subscriptions for WooCommerce were never detected
   as subscriptions. The check guarded on a function name with one letter too many, so it
   always returned false regardless of the product, and the card could be tokenised as a
   one-off payment instead of as a recurring one.
 * Fix: Any anonymous visitor could trigger a fatal error through admin-ajax.php. The AJAX
   action check_token_insite_from_action was registered, for logged-in and logged-out visitors
   alike, against a method that does not exist; the working implementation and the name the
   JavaScript posts are check_token_insite_from_action_checkout, registered separately. The two
   obsolete registrations have been removed.
 * Fix: The Redsys Tokens admin page registered a form handler that was never written, so
   admin-post.php?action=redsys_tokens_search was a fatal for any logged-in user. Searching on
   that page has always worked through the page itself; the obsolete registration is gone and
   nothing changes for the user.
 * Fix: Twelve user-facing texts said "Redys" instead of "Redsys" — nine email titles and three
   in the Bank Transfer gateway description. The Catalan, Basque, Galician, Spanish, French and
   Portuguese translations were updated in the same release, so no translated text is lost.
 * Fix: A malformed or tampered bank notification could fill the log with "Trying to access
   array offset on null" warnings. Reading the order number from an undecodable notification now
   returns empty, which callers already treat as unresolvable.
 * Fix: With the order number format set to "Number created by WooCommerce only with zeros"
   (simpleorder), adding a payment method could be rejected by Redsys as a repeated order
   number (SIS0051). Real orders and card-additions were numbered from the same 12-digit
   zero-padded space, and the add-card counter started at 1 and walked straight through the
   range of existing order ids. The collision had a second, more damaging consequence: the
   plugin decides whether a bank notification belongs to a card-addition by reading a
   transient that lives 48 hours and is keyed on the number alone, so a real order paid with
   a number a card-addition had reserved in the previous two days was processed as a
   card-addition and returned without marking the order as paid — the money was taken and
   the order stayed pending. Card-addition numbers now use the reserved prefix 200 plus a
   9-digit counter, and the order-number generator can no longer produce that prefix: under
   the default format the number is a random 1-999 prefix followed by the last 9 digits of
   the order id, so the two values that would land in a reserved space, 100 and 200, are
   re-rolled. The card-addition decision is now taken from the number itself plus a check
   that no real order carries it, keeping the transient as a fallback so operations already
   in flight when the plugin is updated keep working. Nothing is migrated and no pending
   card-addition is lost. The "only with zeros" format keeps working; its remaining and
   inherent limitation — paying the same order twice reuses the number and Redsys answers
   SIS0051 — is now stated in the setting itself.
 * Fix: The InSite tokenization number could collide with the default order number format.
   It begins with 1, while the default format uses a random 1-999 prefix, so a random prefix
   of exactly 100 and an InSite counter matching the order id produced the same value. Unlike
   the item above this needed no particular setting — it affected the default configuration.
   The InSite number keeps its 100 prefix and the order generator now re-rolls it.
 * Fix: Two card-additions started at the same instant could be issued the same number and
   save the card onto the wrong account, because the internal counters were read and written
   back as two separate operations. The counters now claim each value atomically, using the
   same mechanism already used for subscription renewal locks. Nothing wraps and no number is
   ever reused: if a counter were exhausted, or a value could not be reserved, the operation
   is refused rather than sent to Redsys with a repeated number.
 * Fix: Card-addition and InSite tokenization operations could not be traced from the logs.
   The number was never logged together with the customer it belonged to, and the
   card-addition flow logged it under a variable named $order_id — so the log claimed an
   operation was an order when no order existed. Both now write an identity line when the
   number is reserved: number, user id, login and email, operation type, and an explicit
   note that it is not an order number.
 * Fix: A Redsys email arriving after a card-addition had expired could modify an unrelated
   order. The IMAP fallback resolved the number with a legacy heuristic that strips the
   first three characters, which for a card-addition number returned the id of a real order.
   Reserved-prefix numbers are now recognised and never resolve to an order; the check runs
   after the normal lookups, so orders paid in earlier versions whose random prefix happened
   to be 100 or 200 still resolve to their own order.
 * Fix: Apple Pay chosen as a normal payment method in the block checkout (not the express
   button) produced a paid order with no customer data at all — no name, email, phone or
   country, and DS_MERCHANT_TITULAR blank — while the payment went through. Introduced in
   31.0.6: the express fix released there was applied to both wallet modes, and the two need
   the opposite behaviour. Express brings its own data from the Apple Pay sheet, so letting
   WooCommerce Blocks re-sync the order destroys it; standard mode has no data of its own, so
   letting Blocks fill the order in is the only thing that populates it. This is now decided
   per mode. Apple Pay Express keeps the 31.0.6 behaviour and is unaffected.
 * Fix: Apple Pay in the block checkout could fail to pay when the shopper typed the address on
   the page. The sheet showed the pre-tax amount while the server charged the real total, and
   the payment was rejected on the mismatch. The amount was calculated once when the page was
   rendered and never updated as taxes and shipping were recalculated; it now follows the live
   cart total, so it always matches what is charged.
 * Fix: Apple Pay in the block checkout could open the payment sheet with required checkout
   fields still empty, and left WooCommerce's "Place order" button visible next to the Apple
   Pay button. Required fields are now checked before the sheet opens, and the "Place order"
   button is hidden while Apple Pay is the selected method.
 * Fix: Apple Pay in the block checkout still produced a paid order with no customer data when the
   gateway's terminal type was "mixed" instead of "secure". The earlier fix in this version fills
   the order on the payment page, but a mixed terminal charges directly and never loads that page.
   On a mixed terminal the order is now filled from the checkout form before the charge is sent.
   Secure terminals and Apple Pay Express are unaffected.
 * Change: Apple Pay and Google Pay now always use the Secure terminal, and the "Terminal type"
   selector has been removed from both. For a wallet the terminal type only affected the first
   payment (renewals go through the card/redirection gateway), and Secure already charges and
   requests the recurring subscription token. The Mixed terminal used a path with no payment page,
   which is where Apple Pay as a normal method lost the customer's data on the block checkout.
   Stores set to "mixed" move to Secure automatically on update; no data is lost.

= 31.0.6 =
 * Fix: The ACP webhook dispatcher no longer floods the logs with "as_next_scheduled_action() /
   as_schedule_recurring_action() was called before the Action Scheduler data store was
   initialized" notices. The worker was scheduled on plugins_loaded, where the as_* functions
   already exist but Action Scheduler has not yet initialized its data store. Scheduling is now
   deferred to the action_scheduler_init hook when the store is not ready, keeping the WP-Cron
   fallback unchanged when Action Scheduler is absent.
 * Fix: Apple Pay Express orders were saved without any customer data (name, surname, email,
   phone, billing and shipping address) and were recorded as guest purchases, even though the
   payment completed correctly. Google Pay Express was unaffected. The express order was
   registered in the session as the Store API draft order, so the Checkout block hydration on
   the pay page re-synced it from the cart (OrderController::update_order_from_cart) and wiped
   the contact data seconds before the bank notification arrived. The express flow no longer
   hands its order to WooCommerce Blocks, and releases it from the draft session when the
   payment starts. Applies to the express button on both the Cart and the Checkout blocks.

= 31.0.5 =
 * Fix: Subscription renewals could still get stuck in an endless SIS0051 ("número de
   pedido repetido") loop after 31.0.4: Redsys reports a duplicate order number as a
   plain errorCode (not as a signed decline), so the stored number was never released
   and every retry resent the same DS_MERCHANT_ORDER. Renewal charges now ALWAYS
   generate a brand-new order number at the start of every attempt and never read a
   previously stored one. The attempt's iniciaPeticion/trataPeticion pair shares the
   fresh number, duplicate concurrent workers are stopped by the renewal lock, and the
   last number sent is still persisted so refunds and the IPN resolve the right order.
   Stores stuck in the loop self-heal on the next scheduled retry after updating.

= 31.0.4 =
 * Fix: Subscription renewals (with any subscription plugin) now respect the configured
   Redsys order-number type and no longer fail on retry with "número de pedido repetido"
   (SIS0051). The renewal charge used to generate the order number ignoring the "Type of
   order number" setting (it fell back to the default random scheme instead of the type
   configured for the gateway, unlike the initial checkout), and it persisted that number
   for the whole order lifetime, so a retry on a declined renewal resent the exact same
   DS_MERCHANT_ORDER and Redsys blocked the legitimate retry as a duplicate. Now the
   distinction between a payment "flow" and a "retry" is explicit: a single flow (its
   iniciaPeticion/trataPeticion pair, concurrent workers, or an Action-Scheduler re-run of
   a process that died mid-charge) keeps the same number to avoid SIS0502 and double
   charges; once Redsys confirms a decline (no money moved), the number is released so the
   next retry generates a fresh, unique one honouring the configured type. The reset fires
   only on a confirmed decline, never on transport/timeout errors, so a lost-response
   re-run can never double charge. Applies to Redirection and InSite (and Google Pay /
   Apple Pay, which delegate to Redirection). The "simple order number" type has no random
   component and is incompatible with subscription retries by design.
 * Dev: The "Behaviors towards the APP" and "Apps and Plugins" tabs under Redsys Advanced
   are hidden again until the native management app is publicly launched (the section
   handlers stay wired; only the menu entries are hidden).

= 31.0.3 =
 * Fix: InSite card payments on the block-based (Blocks) checkout were rejected by Redsys
   with SIS0574 ("browserUserAgent not indicated"): the 3DS browser fingerprint never
   reached the order because the hook that copies it (woocommerce_checkout_create_order)
   does not run on the Store API checkout. The InSite Blocks form now collects the
   fingerprint and sends it with the token, and it is written onto the real order before
   process_payment runs.
 * Fix: InSite card payments on the block-based (Blocks) checkout were rejected with
   an "error code without token" after a few attempts (shown as "check that the
   checkout/card fields are filled in"). Because the order does not exist yet on the
   Blocks checkout (order id 0), the prepared Redsys order number was almost constant
   (~999 possible values, all ending in zeros) and Redsys rejects a reused order number.
   The InSite Blocks form now uses a unique incremental sequence as a surrogate id.
 * Fix: InSite card payments on the block-based (Blocks) checkout failed with a
   misleading "check that the checkout/card fields are filled in" error and the order
   was never paid (the browser console showed a 404 on save_order_data). Current
   WooCommerce no longer creates the order until "Place order" is pressed, so during
   card entry the Blocks checkout returns order id 0 and the InSite token (idOper) and
   prepared order number could not be saved to order meta. They are now stashed in the
   WooCommerce session via admin-ajax when the card is tokenised and moved onto the real
   order while it is created (woocommerce_store_api_checkout_update_order_from_request),
   before process_payment runs, and the order-number transient is re-mapped to the real
   order id so the notification (IPN) resolves the order. The classic/shortcode checkout
   is unaffected.
 * Security Fix: Added Inespay notification signature (signatureDataReturn / HMAC-SHA256)
   and amount verification in the Inespay callback to prevent unauthenticated payment
   forgery. The signature gate covers both the single-payin and the periodic-mandate
   notifications. Thanks to Shivamani Vastrala for the responsible disclosure.

= 31.0.2 =
 * UPDATE: El filtro redsys_modify_data_to_send ahora recibe
   'context' => 'add_payment_method' y 'user_id' en el flujo de "Añadir
   método de pago", para poder enrutar la tokenización al terminal correcto.
 * FIX: Añadir una tarjeta desde Mi Cuenta en tiendas multidivisa podía ser
   rechazado por Redsys con error de divisa. Ahora se envía la divisa base
   de la tienda, que coincide con el terminal por defecto.
 * FIX: Al añadir una tarjeta con la opción de suscripciones, el acuerdo COF
   se creaba como 'C' (one-click) en Redsys. Ahora se envía correctamente
   DS_MERCHANT_COF_TYPE 'R'.
 * FIX: Los pagos con tarjeta guardada (token R) y las renovaciones de
   suscripción no respetaban la preautorización. Ahora se envía una
   preautorización real de tipo 1 que puede confirmarse después.
 * UPDATE: Actualizadas todas las traducciones.
 * SECURITY: La clave secreta SHA-256 ya no se escribe en texto plano en los
   logs de depuración; se enmascara mostrando solo los 4 últimos caracteres.
 * SECURITY: Las firmas de las notificaciones (IPN) de Redsys se verifican
   con hash_equals() (comparación en tiempo constante) en todos los
   gestores de notificación.
 * SECURITY: El gestor AJAX de borrado de un token ahora exige la capacidad
   manage_woocommerce además del nonce.

= 31.0.1 =
 * FIX: Bizum en página de pago (Bizum InSite) no enviaba la descripción
   a Redsys. El cobro real del modal usa la API REST (trataPeticionREST)
   y esa petición no incluía DS_MERCHANT_PRODUCTDESCRIPTION, por lo que la
   "Descripción Redsys" configurada (p. ej. "ID Pedido") llegaba vacía al
   panel de Redsys. Ahora se calcula con el ajuste seleccionado y se envía
   en la petición REST.
 * FIX: La cuenta atrás del modal de Bizum en página de pago mostraba
   7 minutos. Ahora muestra 5 minutos y el control de tiempo en servidor
   que redirige al checkout se ajusta en consecuencia (60 iteraciones *
   5 segundos = 5 minutos).

= 31.0.0 =
 * NEW: Agent2Agent (A2A) protocol surface. Opt-in (disabled by default).
   Publishes an Agent Card at `/.well-known/agent-card.json` and a
   JSON-RPC 2.0 endpoint at `/wp-json/wc-redsys-a2a/v1/rpc` with seven
   skills: `query_payment_status`, `create_payment`, `capture_payment`,
   `refund_payment`, `list_payments`, `tokenize_card` and
   `recurrent_charge`. Authentication reuses the UCP OAuth 2.1 + PKCE
   authorization server with `a2a:payments:*` scopes (and an optional
   sandbox bearer for testing under WP_DEBUG).
 * NEW: Resumable Server-Sent Events stream at
   `/wp-json/wc-redsys-a2a/v1/tasks/{taskId}/stream` for live task
   updates, with `Last-Event-ID` resume and 15s heartbeats.
 * NEW: Signed outbound push notifications to a client-registered
   `push_url`. `X-A2A-Signature: sha256=<hmac>` over
   `<timestamp>.<body>`, retries 1m/5m/30m/2h (4 max), delivered via
   Action Scheduler.
 * NEW: Admin tab "A2A (Agent2Agent)" under Redsys Advanced → Agentic
   Commerce. Status / Clients (create / revoke / rotate sandbox
   bearers) / Limits & modes (`max_amount`, `sensitive_threshold` and
   exposed payment modes) / Confirmaciones (approve refunds or
   recurrent charges parked at `input-required`).
 * NEW: Per-skill ceilings — `max_amount` is a hard cap that fails the
   task with `limit_exceeded`; `sensitive_threshold` parks the task at
   `input-required` until an admin confirms it. Both default to no cap
   (opt-in).
 * NEW: Append-only audit log at `wp_redsys_a2a_audit_log`. Raw payloads
   are never stored — only a SHA-256 hash plus a sanitized one-line
   summary that masks PAN, tokens, secrets and email local-parts.
 * NEW: Self-healing storage. The four A2A tables are recreated on
   demand by every store's `ensure_table()` so a dropped table never
   fatally errors.
 * INTERNAL: New hook `redsys_post_authorization` fires after a
   successful preauthorization capture (TransactionType 1 → 2).
   `redsys_post_refund` continues to fire after refunds.
 * INTERNAL: UCP OAuth server gained an `a2a_*` client type and
   accepts dynamic registration with `client_type=a2a_*`.
 * DOCS: New `docs/A2A.md`, `docs/A2A-skills.md`, `docs/A2A-auth.md`,
   `docs/A2A-webhooks.md`, `docs/A2A-troubleshooting.md`. Working
   reference client under `examples/python-client/`.

= 30.4.1 =
 * NEW: Agentic Commerce support. ACP (OpenAI/Stripe Agentic Commerce
   Protocol) is enabled by default and exposes `/.well-known/agent-commerce.json`,
   the `redsys-acp/v1` REST namespace (checkout sessions, payment delegation,
   product feed, webhook subscriptions) and a Woo Agentic-compatible mirror
   under `wc/agentic/v1`. UCP (Universal Commerce Protocol, NRF 2026) ships
   alongside, disabled by default, and exposes `/.well-known/ucp.json`,
   OAuth 2.0 with PKCE under `redsys-ucp/oauth/v1` and the Checkout, Catalog
   and Orders capabilities under `redsys-ucp/{capability}/v1`. Both
   protocols share Redsys/Bizum HPP for the actual payment authorization.
 * For the full per-release notes including security and bug fixes, see
   `changelog.txt`.
