=== 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.0.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.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`.
