v0.2.10
30 September 2026 Latest- Settings → Payment methods: the description line under a HULO method decodes HTML entities (
&,', …) that Vendure's rich-text editor stores, so "Card & Apple Pay" no longer shows asCard & Apple Pay.
Every release of @huloglobal/vendure-plugin-payments. Latest release: v0.2.10 — 30 September 2026.
&, ', …) that Vendure's rich-text editor stores, so "Card & Apple Pay" no longer shows as Card & Apple Pay.script-src, frame-src and connect-src, with img-src * data:, style-src 'self' 'unsafe-inline', frame-ancestors 'none', base-uri 'none', form-action 'self'. New plugin option hostedCsp: 'enforce' | 'report-only' | 'off' (default 'report-only': the policy is sent as Content-Security-Policy-Report-Only and violations posted to the new POST /hulo-payments/csp-report route are logged, rate-limited, tokens masked). The Stripe hosted flow was verified under 'enforce' in a real browser (Payment Element mounted, session created, zero violations).src/pg-corpus.test.ts, runs when HULO_PG_URL is set): every SQL statement in src/** is translated by the licence-sdk adapter and prepared against a scratch PostgreSQL 17 with quoted-camelCase stand-ins for the Vendure tables the plugin reads. 66 statements pass.dist (they were published with the package before).confirmPayment insisted on a Drop-in sessionId + sessionResult, which a payment link does not have. The AUTHORISATION / CAPTURE notification now confirms the payment (Authorized or Settled per the event, transactionId = pspReference, amount and currency checked against the order). The facts are registered by the webhook path and consumed once by the handler, so a storefront cannot forge a pspReference through addPaymentToOrder.sessionResult is now kept in the payment metadata.markSettledByWebhook) is a map with a 60 s TTL and is cleared in a finally around settlePayment, so a failed settle can no longer leave a stale "trust me" entry for that payment.huloRenewal and their payment_intent.* events ignored; Adyen notifications whose merchant reference is a scheduler reference (<order>-R<yyyymmdd>-<variant>-a<n>) are ignored. The scheduler's renewal row is the only one.refund.settled now targets the Vendure refund by the provider's refund id (Stripe re_… from charge.refunded's refund list or the new refund.updated / charge.refund.updated handling, Adyen's REFUND pspReference), then by exact amount, then the only pending one — never "the first pending refund" when several are open. A refund already settled from the handler is not written to the ledger a second time.amount − Σ lines (Klarna / Afterpay reject sessions whose lines do not sum to the amount).ArrangingPayment, so link previews and crawlers locked baskets. The GET is read-only (an AddingItems order still lists its providers); the transition happens in POST …/session, before the provider session is created.cancelSubscription uses the stored customer reference instead of listing up to 250 subscriptions to find it; the provider contract gained an optional providerCustomerRef argument.hulo_hosted_session.orderId was INT; it is now VARCHAR(36) (widened on boot with a guarded ALTER, MariaDB/MySQL and Postgres) and compared as text.payment.settled arriving before the customer returns) called addPaymentToOrder outside a transaction, which Vendure 3.7 rejects; the event was then marked processed and lost. Every webhook is now applied inside one transaction with the order row locked, in the order's own channel with that channel's method code.checkout.session.completed + payment_intent.succeeded pair) can no longer record two payments: both paths lock the order and skip a reference that is already on a live payment. The hosted /complete route also holds a one-minute lease on the session row so a double submit is answered from the order's state.UPDATE before the provider is called (the hourly cron and the admin "run now" run in different processes); the charge reference includes the attempt number so a retry never replays a cached refusal. Subscription rows are claimed with a pending row + unique key before the provider subscription is created (the bus fires for PaymentAuthorized and PaymentSettled seconds apart). Monthly and yearly periods are calendar arithmetic instead of 30/365 days. The plugin registers ScheduleModule itself, so renewals run on hosts that never wired it; disableScheduler is honoured.returnUrl and cancelUrl must be absolute http(s) URLs (optionally restricted with the new hostedReturnHosts option); methodCode and locale are validated; client metadata is allow-listed and server-side session ids win; provider and Vendure error text no longer reaches the customer; clickjacking and referrer headers are set; each link allows at most 30 provider sessions; expiry is stored as a date (no UTC/local drift); expired rows are purged.channelId, paymentMethodId, transactionId, orderId); the subscription updater no longer quotes the plugin's own columns; the channel lookup goes through the dialect adapter. Payment methods were silently absent on Postgres before.order.totalWithTax and the webhook accepts it; GoCardless refunds send the required total_amount_confirmation; refund idempotency keys are per minute, not per second.ALTER TABLE … IF NOT EXISTS is replaced by an information-schema check (MySQL 8 boots again).trackBy on every table, daily bars computed once per load, tab switches keep loaded data (Refresh reloads), every provider named in Transactions/Subscriptions/Routing and available in the Surcharges table, the providers cache expires after 60 s and is cleared after Connect, the pay-by-link capability chip reads the right key, the method panel shows the method being edited, and the action-bar button needs only ReadOrder.hulo_payment_event rows older than 180 days and expired hosted sessions older than 7 days are purged.vendureOrderId stamp) are acknowledged and ignored instead of being applied to the order; the plugin never adds a payment to an order that already carries another method's payment.POST /hulo-payments/methods { provider, channelId } behind it (admin, CreatePaymentMethod), idempotent per provider and channel.huloHostedCheckout(methodCode): the storefront can preselect one of the enabled methods, so a "Pay with PayPal" button opens the hosted page straight on PayPal. Without it the page opens on the first method in the channel's order, as before.?tab= and ?connect=<provider> deep links.huloHostedCheckout(returnUrl) returns a URL the storefront redirects to; the page shows every enabled method in the channel's preferred order, drives each provider's own client (Stripe Payment Element, Adyen Drop-in, PayPal buttons, Square Web Payments, Braintree Drop-in, hosted redirects, bank-transfer instructions), records the payment and sends the customer back with ?order=&result=. No provider code in the storefront. Branding (name, colour, logo) per channel in Settings./hulo-payments/webhook/<provider>, updating payments, refunds and subscriptions; disputes recorded and alerted.hulo-payment-rules eligibility checker, optional surcharges.