Install
Works with Drupal: ^11Using Composer to manage Drupal site dependencies
Alternative installation files
Release notes
# WT Commerce 1.2.0 — release notes
Covers `wt_simple_pay`, `wt_simple_booking` and `wt_slot_booking`. Drupal 11, PHP 8.1+.
This release adds Stripe, closes a number of payment-verification and double-booking gaps, and makes the suite installable on a fresh site without creating content types by hand. **Please read the upgrade notes before updating a live site.**
## Security
- **Orders are tied to the buyer's session.** The bank-transfer, PayPal and Stripe bridge pages open only for the order the current session created (403 otherwise). Order IDs can no longer be enumerated or reused by someone else.
- **Payments are verified before an order is completed.**
- PayPal compares amount and currency (in cents) before capture and again after it. A mismatch does not capture, or leaves the order pending and e-mails the admin.
- The Viva webhook compares the amount in cents and skips orders that are already `completed`.
- Stripe and Viva keep a `cancelled` order cancelled, save the transaction id and e-mail the admin (`admin_orphan_payment`) when a payment arrives for a cancelled or missing order.
- **Stripe webhooks** verify the signature, compare the amount and are idempotent. Stripe keys live in `settings.php` only; the Stripe API is called through a per-request `StripeClient`, not the global API key.
- **Abandoned payments are cancelled.** A failed or cancelled payment, and the start of the next checkout, cancel the buyer's own unpaid order and free its bookings. A Stripe session is expired first, and a session that is already `complete` is never cancelled.
- **Flood control** on unpaid gateway checkouts: 5 per IP per hour (reception and bank transfer are exempt). Cleared on a verified payment.
- **iCal import is SSRF-safe.** Feed URLs go through `ICalUrlGuard` (public IPs only, resolved address pinned for the request). The importer only deletes rows with status `ical`, and only after the feed was fetched and parsed as a real calendar.
- **iCal URL guard hardened.** More non-public address ranges are rejected, including IPv6 addresses that wrap a non-public IPv4 address, and the pinned connection now works for hosts with IPv6 addresses. Covered by a unit test.
- **Permission fix.** The calendar routes used `administer content`, which is not a core permission, so only the admin role could open them. All admin routes now use the two new permissions (see upgrade notes).
- **PayPal keys move out of config.** The client ID and secret are read from `$settings['wt_simple_pay_paypal_client_id']` / `['wt_simple_pay_paypal_secret']` (legacy `$config` keys still work as a fallback). Saving the settings form purges any PayPal keys stored in the database.
- **Payment method allowlist.** Checkout accepts only `viva`, `paypal`, `stripe`, `bank_transfer` and `reception`; anything else is logged and falls back to `viva`.
- Bank-transfer "copy" buttons no longer use inline `onclick` handlers (CSP-friendly).
## Bug fixes
- **Double-booking race closed.** Availability is computed in one place per module (`wt_simple_booking.availability`, `wt_slot_booking.availability`). Fresh `pending` bookings block the calendar; checkout takes a lock per room/service, rejects overlapping cart items and re-checks every booking before creating anything.
- **Status machine gaps.** `pending_stripe` added to the order statuses; `bank_transfer` added to reservation and appointment statuses; the cron sweeper and the presave cascade handle both. When both booking modules are enabled, cron uses the shortest `expiration_time`.
- **Gateway sessions no longer outlive the cron sweeper.** Stripe `expires_at` and Viva `paymentTimeout` use the same window as the sweeper (`max(expiration_time, 30)` minutes). Viva `paymentTimeout` is capped at 65535.
- `PaypalController::captureOrder` checks that the order exists, is a `wt_order` and is `pending_paypal` before capturing.
- The thank-you page names the right gateway (Viva needs `?gateway=viva` on its success URL, see upgrade notes).
- **Fresh install fixed.** Enabling the modules on a new site now works: config that depends on `wt_order` moved to `config/optional`, a stale `media_library` dependency was removed from two form displays, and `wt_simple_pay_update_8101()` no longer overwrites an existing `wt_orders` view.
- `stripe/stripe-php` and `drupal/paragraphs` are now declared in `composer.json` (Stripe classes were used without being declared).
- Coding-standards clean-up across all three modules (phpcs Drupal + DrupalPractice, more constructor injection).
- **No "Menu settings" on orders.** Since `node.type.wt_order` moved to `config/optional` without `menu_ui` settings, the order edit form showed a menu box when `menu_ui` was enabled. The box is now removed from the `wt_order` form (no `menu_ui` dependency added); orders never get menu links.
- **Slot booking: no pre-set closed dates.** New installs start with an empty "closed dates" list instead of two dates in May 2026. Existing sites keep their saved value.
- **Stripe: clear message for too-small totals.** When Stripe rejects a total below its minimum (`amount_too_small`), the buyer now sees "The order total is below the minimum amount accepted for card payments…" instead of a generic gateway error, and the event is logged as a warning, not an error. Other Stripe errors are unchanged.
- **Stripe success page.** The thank-you page shows "Order reference: WT-N" instead of "Transaction ID: N/A" when the gateway returns no transaction ID. The real ID is stored on the order by the webhook.
- **Cron no longer deletes paid Stripe orders.** Before deleting an old `pending_stripe` order, the sweeper asks Stripe whether the session was paid. If it was, the order is kept for the webhook (Stripe retries it) and a warning is logged.
## Features
- **Stripe Checkout** with a signed `checkout.session.completed` webhook and automatic session expiry.
- **Config ships with the modules.** `wt_order`, `wt_room`, `wt_reservation`, `wt_service`, `wt_appointment`, their fields, displays, the three dashboards (`/admin/wt-commerce/orders`, `/reservations`, `/appointments`) and a config schema with secret-free defaults for `wt_simple_pay`. No manual content types. `wt_product` is still yours to create.
- **Two new permissions:** `administer wt commerce` and `view wt commerce orders` (both *restrict access*).
- Developer tooling: `ddev wt-lint`, drupal.org GitLab CI, curl-based QA regression scripts.
- Documentation: READMEs updated for Stripe, shipped config, Composer install into `modules/contrib`, permissions, `settings.php` key names and webhook URLs.
## Upgrade notes
1. **Back up the database**, then update the code with Composer (`composer update drupal/wt_commerce`, which also pulls in `drupal/paragraphs` and `stripe/stripe-php`).
2. **Run `drush updb -y`.** New update hooks:
- `wt_simple_pay_update_8102`: adds `pending_stripe` to the order status values.
- `wt_simple_pay_update_8103`: grants the new permissions (below).
- `wt_simple_booking_update_8101` / `wt_slot_booking_update_8101`: add `bank_transfer` to the booking status values.
All of them only add values or permissions; none removes data. Then `drush cr`.
3. **New permissions.** `administer wt commerce` is granted to roles that had `administer site configuration`; `view wt commerce orders` to roles with `administer site configuration` or `administer content types` (admin roles are skipped). Check **People > Permissions** and grant them to the right staff roles. Your existing `wt_orders`, `wt_reservations` and `wt_appointments` views keep their old access setting until you change it in Views.
4. **Paragraphs is now required** (hard dependency of `wt_simple_pay`). Composer installs it; Drupal enables it with the module.
5. **Existing content types are kept.** Shipped config is not applied over a type that already exists on your site.
6. **Viva Wallet:** set the Smart Checkout success URL to `https://yourdomain.com/checkout/wtpay/success?gateway=viva` for the correct wording on the thank-you page.
7. **PayPal keys → `$settings`.** Add `$settings['wt_simple_pay_paypal_client_id']` and `$settings['wt_simple_pay_paypal_secret']` to `settings.php`. The old `$config['wt_simple_pay.settings']['paypal_client_id']` / `['paypal_secret']` overrides keep working automatically as a fallback, so the update does not break PayPal. If keys were ever saved in the database, open the settings form and click *Save configuration* to purge them.
8. **Stripe (new):** add `$settings['wt_simple_pay_stripe_secret']` and `$settings['wt_simple_pay_webhook_secret']` to `settings.php` and create a webhook endpoint `https://yourdomain.com/checkout/wtpay/stripe/webhook` for `checkout.session.completed`.
9. Behaviour change: a buyer who goes back and starts a new checkout now has their previous unpaid gateway order cancelled. A buyer who still pays the old one is caught by the webhook guard and the admin receives `admin_orphan_payment` to refund by hand.
10. If you uninstall a booking module, its content types and field storage stay in your configuration, so re-enabling it later fails until you delete them (see `docs/KNOWN_ISSUES.md`).
11. **Translations.** Some settings-form strings (the PayPal credentials block and its status messages) were reworded, so existing translations of them may need updating. Re-import or re-translate at `/admin/config/regional/translate`.
12. **`wt_product` is no longer shipped.** Earlier versions installed the Product content type (`node.type.wt_product`), its fields (`field_wt_sku`, `field_wt_colors`, `field_wt_sizes`, `field_wt_out_of_stock`, and the price fields) and their form/view displays from `config/install`. 1.2.0 ships only the order, booking and shared price config, so you build products the way you like (the `wt_simple_pay` README describes the fields).
- **Existing sites:** nothing is removed. Updating the code does not delete your product type, fields or content; they stay in your active configuration and keep working. Do not delete them on the assumption that the module owns them.
- **New installs:** no Product type is created. Create `wt_product` (or any content type you use as a product) by hand before adding items to the cart. The shared price fields `field_wt_price` and `field_wt_sale_price` are still shipped.
- **Configuration sync:** if you export your configuration, the product config now belongs to your site, not to the module. Keep it in your sync directory.
13. **`wt_simple_pay_update_8101()` changed.** This update converts the order status from free text to a list field. Drupal runs an update only once per site, so if it already ran on your site nothing happens. It matters only for a site that still has the old text-based status field. The behaviour is the same except for the dashboard view:
- It no longer overwrites an existing `wt_orders` view. Earlier it replaced the view with the shipped one, which discarded your own changes to that dashboard. Now the shipped view (read from `config/optional`) is created only when the site has no `wt_orders` view.
- The status mapping and the data migration are unchanged. Take a database backup first: the old field is deleted and recreated, and the values are written back by the update. `8102` and `8103` run after it, in order.
Known issues that remain open are listed in `docs/KNOWN_ISSUES.md`.