Add deprecated gateway migration workflow - #3700
Conversation
Consolidate all deprecated gateway handling into includes/deprecated-gateways.php (replacing deprecated-gateway-sunset.php and the gateway functions in deprecated.php) and rename everything from the sunset prefix to pmpro_deprecated_gateway_*. Workflow changes: - Live workflow state per gateway/environment with progress polling, reset on each new run, and deleted on gateway cleanup - Pause mode guards in both scheduling and the batch worker - Email members before cancelling, with meta to prevent duplicate emails - Stripe placeholder recovery via stored transaction IDs to prevent duplicates - Skip members who already have another active subscription for the level - Treat missing and past next payment dates the same; Force option expires the membership on the missed date and cancels instead of skipping - Migrate billing limits to Stripe as a remaining-payment count, with a $0 migration order standing in for the initial order - Cleanup blocked by live subscriptions in either environment and redirects to payment settings with a confirmation message UI changes: - Replace the POST form with an AJAX panel built on PMPro components (pmpro_section, pmpro_message, pmpro_tag, the wizard stepper) - Three-step flow: activate a new gateway (with inline Stripe Connect), migrate subscriptions, remove gateway data - Live progress bar with Complete/Skipped/Failed counts and a migration log download button on completion - Instructions to search the migration log for failed and skipped entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Skip the in-progress placeholder in the duplicate-subscription check so a rerun after a mid-run crash adopts it instead of cancelling the old subscription without notifying the member. - Vary the Stripe idempotency key per creation attempt and only adopt active local records, so a dead placeholder is never silently reused. - Write only counters from record_result() so it cannot revert a concurrent stop. - Normalize the gateway environment everywhere to live/sandbox. - Enqueue the initial batch as a unique action to prevent parallel chains from concurrent start requests. - Set up Stripe webhooks when activating Stripe from the panel. - Keep the status poll alive through transient request failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecated-gateway-sunset-flow
There was a problem hiding this comment.
Pull request overview
Adds an admin-guided “sunset” workflow for deprecated payment gateways, including UI on the Payment Settings screen, Action Scheduler–driven subscription migration (to Stripe placeholders or expiration dates), migration logging, and two new member-facing email templates. It also updates Account/Billing UI to allow “Update Billing Info” links for subscriptions that may not have an associated order (e.g., Stripe placeholder subscriptions).
Changes:
- Introduces
includes/deprecated-gateways.phpto own deprecated gateway loading plus a 3-step admin migration/cleanup workflow with logging + AJAX status polling. - Adds Stripe-side placeholder subscription creation (
PMProGateway_stripe::create_deprecated_gateway_migration_subscription()) and registers two new email templates for member notifications. - Relaxes “Update Billing Info” link gating in account/billing templates to support order-less placeholder subscriptions.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
shortcodes/pmpro_account.php |
Shows update-billing action link without requiring an order. |
pages/billing.php |
Shows update-billing link for subscriptions without requiring a successful order. |
paid-memberships-pro.php |
Loads the new deprecated-gateways module and new email template classes. |
includes/deprecated.php |
Removes deprecated-gateway helpers moved into the new module. |
includes/deprecated-gateways.php |
New deprecated gateway loading + migration state machine, batch processing, panel UI, AJAX, and logging. |
classes/gateways/class.pmprogateway_stripe.php |
Adds Stripe API helper for creating migration placeholder subscriptions. |
classes/email-templates/class-pmpro-email-template-deprecated-gateway-stripe-migration.php |
New email template for Stripe placeholder migrations. |
classes/email-templates/class-pmpro-email-template-deprecated-gateway-checkout-required.php |
New email template for “checkout again” expiration strategy. |
adminpages/paymentsettings.php |
Replaces deprecated gateway warning with the guided workflow panel + adds “removed” success notice. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
flintfromthebasement
left a comment
There was a problem hiding this comment.
PR: #3700 — Add deprecated gateway sunset workflow
dparker1005 → v3.8 | 9 files, +2081 -103
#3700
Summary
Large, well-constructed feature. The state machine design, idempotency scheme, crash recovery, and staging-site guards are all solid. One bug in the stop implementation that leaves queued AS actions un-cancelled, plus three minor issues. Not blocking, but the stop bug should be fixed before this ships.
Issues
-
Major
includes/deprecated-gateways.php:1003—as_unschedule_all_actions( '', array(), $group )passes an empty string as the hook name. AS buildsWHERE hook = ''internally, which matches no real actions. The queuedpmpro_deprecated_gateway_process_batchactions are not removed from the queue. The state is correctly set to'stopped', so batches that run will detect it and exit early — but for large migrations, this means dozens to hundreds of no-op AS action runs before the queue drains. The PR description says "Pending batches are unscheduled immediately," which isn't accurate. Fix:as_unschedule_all_actions( 'pmpro_deprecated_gateway_process_batch', null, pmpro_deprecated_gateway_get_action_group( $gateway, $environment ) );
-
Minor
includes/deprecated-gateways.php:860-866—pmpro_deprecated_gateway_record_result()is a read-modify-write on counter fields. With concurrent AS workers (multi-threaded queue runners), two workers can readprocessed=5, both computeprocessed=6, and both writeprocessed=6— losing one count. Low risk with AS's default single-worker behavior, but worth noting if sites run parallel queues. -
Minor
classes/gateways/class.pmprogateway_stripe.php:617—catch ( \Throwable $e )(PHP 7+) subsumes all exceptions and errors. Thecatch ( \Exception $e )block immediately after it is dead code in PHP 7+. Remove it, or reorder to\Exceptionfirst if PHP 5 compatibility matters here. -
Minor
includes/deprecated-gateways.php:1334—$bridge_order->saveOrder()result is unchecked. If the insert fails silently, the billing limit bridge order is missing, and the remaining-payment count will be off by one (allowing one extra renewal cycle for billing-limited subscriptions).
Looks Good
- The "cancel last" ordering (create replacement → email → cancel old) is the right call. Partial failures leave the member's original subscription intact.
- The attempt-counter idempotency scheme for dead placeholders is correctly handled — bumping the counter before clearing stale meta ensures Stripe's 24-hour replay window doesn't return the cancelled subscription.
- Auth check before nonce check in
pmpro_deprecated_gateway_ajax()is correct ordering. pmpro_is_paused()guard in bothschedule()andprocess_batch()is good belt-and-suspenders against staging database copies.- The shared PayPal credential deletion logic (only delete once all three PayPal gateways are removed) is handled correctly.
Questions
PMPro_Action_Scheduler::dispatch_queue()is called after scheduling the initial batch — does this reliably kick the queue on sites using WP-Cron, or are there configurations where it silently no-ops and the first batch waits for the next cron tick?
State explicitly in the strategy description, start confirmation, member email, and template help text that members who do not add a payment method before their next payment date will have their membership cancelled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key the bridge order on the placeholder's stored billing limit and only count successful orders, mirroring billing_limit_reached(). Flag the subscription as needs_review if the bridge order cannot be saved, since reruns never revisit this branch and the limit would silently allow one extra payment. Addresses PR review feedback from Copilot and flintfromthebasement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@flintfromthebasement Thanks for the thorough review. Point-by-point:
Triple-catch in the Stripe class — you're right that Unchecked |
- Only count same-environment subscriptions in the already-subscribed check. A leftover sandbox subscription could previously cause a live subscription to be cancelled with no replacement and no member email. - Block gateway cleanup while sandbox subscriptions remain, matching the panel UI, so a direct request cannot orphan them. - Use esc_url() for the Stripe Connect link output. - Guard email template recipient accessors against deleted users. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stripe placeholder subscriptions created by the deprecated gateway migration start with no payment method, and previously nothing surfaced that state after the migration email: members got the generic upcoming payment reminder, and admins had no view of who was still at risk. - Flag placeholders with deprecated_gateway_needs_payment_method meta, cleared when a billing update or completed payment proves a payment method exists, or by reverifying against Stripe (throttled to one API call per subscription per six hours). Visiting the billing update page resets the throttle so payment methods added through the Stripe Customer Portal are picked up as soon as the member returns. - Show an action-required notice on the Membership Account page and send the migration email in place of the generic recurring payment reminder while the flag is set. - Add an "Awaiting Payment Method" filter and status tag to the Subscriptions list, and an admin notice on PMPro pages linking to the filtered list while any flagged subscriptions remain. - Refuse to start a Stripe migration when Stripe credentials or the webhook are missing for the current environment (every subscription would otherwise be flagged needs_review), stop a queued workflow if credentials are disconnected mid-run, and explain a disabled Stripe option in the panel. - Keep the migration email template registered after gateway cleanup while flagged subscriptions remain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Describe what each strategy actually trades off: the Stripe option keeps the member's price and billing schedule and is backed by the account notice, swapped payment reminder, and Awaiting Payment Method tracking; the expiration option sends the standard expiration emails but a new checkout uses current level pricing. - Match the skipped warning to the "[skipped]" log format, matching the needs_review warning. - Tighten the step 3 removal text. - Note in the Stripe migration email template description that it is also sent in place of the upcoming payment reminder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Webhook detection and warnings already live on the Stripe gateway settings page, so they no longer gate the migration panel; on local sites Stripe disables undeliverable test webhooks, which was greying out the Stripe migration option. Missing credentials still block the Stripe strategy in the panel, at schedule time, and mid-run. Replace the migration-specific readiness method with a reusable PMProGateway_stripe::has_credentials(), which unlike has_connect_credentials() also covers legacy API keys, and route all three call sites through the panel blockers helper. With no API call involved, the readiness transient cache is no longer needed. Also note in the Stripe strategy description that trial pricing cannot be migrated: members still in a trial period will be billed the regular price beginning with their next payment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Subscriptions can carry nonstandard gateway_environment values (imports, hand-edited data). The counts bucketed anything non-live as sandbox, but the batch query and per-subscription checks matched 'sandbox' exactly, so those rows inflated the total, were never processed, and blocked gateway cleanup permanently. Treat anything that is not 'live' as sandbox everywhere via a single helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dry run analyzes every subscription with the same decision logic as a real run and records the planned outcome in the migration log, but makes no changes: no gateway calls, no cancellations, no emails, and no database writes. Local data problems (lapsed payment dates, reached billing limits, members who already re-checked-out, deleted users) show up in the preview; gateway-side failures can only surface during a real run. Subscriptions already mid-migration from an earlier run are reported as resumable rather than simulating the recovery logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Default the migration to a dry run and add a clear real-run treatment: unchecking Dry Run turns the panel red, relabels the button to "Start Real Migration", and flashes a permanence warning (respects prefers-reduced-motion). Avoids the word "live" to prevent confusion with the gateway environment. - Add external-link icons to the offsite links in the panel. - Make the two migration email templates more end-user friendly. - Add a pmpro_deprecated_gateway_batch_size filter for the per-batch count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each Stripe migration can make several sequential gateway API calls, so a batch of 10 risked approaching the PHP execution time limit mid-run, which would break the chain to the next batch. Halving the default greatly reduces that timeout exposure. Still filterable via pmpro_deprecated_gateway_batch_size. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the jargony "Force Migration" checkbox with a labeled choice about how subscriptions with a missed payment (a next payment date in the past, or none at all) are handled: skip and log for manual review (default), or cancel at the old gateway and expire the membership. The copy now notes that a missed payment date is most often a missed IPN or webhook rather than a stopped subscription, so the gateway may still be billing the member -- making "skip and verify" the safe default and the cancel/expire path an explicit opt-in. Renames the internal flag from $force to $expire_past_due (and the per- subscription $force_expiration to $expire_this_subscription) across the schedule/batch/process functions, state, AJAX wire param, and log messages. No change to migration behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each migration run now writes a downloadable CSV of the members it touched, in
addition to the existing global text log. A run ID stored in the workflow state
names the file (deprecated-gateways-{gateway}_{env}_{stamp}.csv), and each batch
appends one row per processed subscription:
processed_date, user_id, user_email, display_name, membership_level,
old_subscription_id, new_subscription_id, new_subscription_transaction_id,
action, handoff_date, outcome, email_sent, notes
The two subscription-id columns point at the new placeholder in PMPro and in the
Stripe dashboard respectively. This lets sites bulk-send through an email service
(run with emails off, export, send) instead of firing individual emails, and
gives an audit/triage record per run.
- process_subscription() now returns structured fields via a result helper.
- The panel offers a "Download Migration CSV" button after a completed run and
lists all of a gateway's CSVs at the cleanup step.
- The Member Emails setting explains the ESP/CSV alternative.
- The restricted-file access filter allows per-run .csv/.txt files (capability
gated, traversal-safe).
- Cleanup keeps the CSVs and text log as a record; it does not delete them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What this does
Adds a guided sunset workflow for deprecated payment gateways on the Payment Settings page. When editing a deprecated gateway, admins now see a three-step panel:
Activate a new gateway — offers the Stripe Connect flow (or activates Stripe directly if already connected, including webhook setup), with a pointer to the PayPal Add On as an alternative.
Migrate subscriptions — a batched Action Scheduler workflow that processes every active subscription for the gateway in the current environment, with live progress, per-subscription logging, and a stop button. Two strategies:
missing_payment_method => cancel). The member is emailed to add billing information before that date. Billing limits carry over as a remaining-payment count enforced by a $0 bridge order.In both cases the old gateway subscription is cancelled last, so members are never left unnotified by a partial failure. Subscriptions without an upcoming payment date are skipped unless the Force option is enabled. Every outcome is recorded as complete / skipped / needs review in a downloadable migration log.
Remove gateway data — once no live subscriptions remain, deletes the stored credentials (PayPal-shared credentials only once no PayPal gateway needs them) and stops loading the gateway.
Implementation notes
includes/deprecated-gateways.phpowns gateway loading (moved fromincludes/deprecated.php) plus the workflow: state machine in a per-gateway/environment option, batches chained through Action Scheduler with a unique initial action, and recovery designed so reruns are safe (crash-safe meta ordering, per-attempt Stripe idempotency keys, active-only placeholder adoption, already-processed subscriptions are skipped).deprecated_gateway_stripe_migrationanddeprecated_gateway_checkout_required, registered only while a deprecated gateway is loaded.PMProGateway_stripe::create_deprecated_gateway_migration_subscription()creates the placeholder subscription with migration metadata.Testing
🤖 Generated with Claude Code