Use this matrix when restarting end-to-end testing from scratch. Each scenario should be tested with two Stripe test accounts: a seller/source account and a buyer/destination account.
Baseline setup
Before each scenario:
- Create a fresh RevBridge project.
- Use same-mode Stripe keys from two different accounts.
- Use normal Stripe test-mode subscriptions for this matrix, not Stripe Test Clock subscriptions. Stripe's subscription list API omits Test Clock subscriptions unless filtered by a specific test clock or customer, and RevBridge v1 does not ask for a Test Clock ID.
- Copy customers from source to destination with Stripe Customer Data Copy, then upload the unmodified mapping CSV.
- Include every source customer that has a billing-relevant source subscription in the uploaded mapping. RevBridge ignores terminal
canceledandincomplete_expiredsubscriptions, but active, trialing, incomplete, past-due, unpaid, and paused subscriptions must be represented so dry-run can either migrate or block them. - Run dry-run, review blockers and warnings, then execute only when the expected outcome below says the scenario should be executable.
- After execution, verify destination subscription state, source
cancel_at_period_end, CSV audit, and PDF audit.
Supported migration scenarios
| Scenario | Stripe setup | Expected dry-run | Expected execution |
|---|---|---|---|
| Monthly paid subscription | Source customer with one active monthly licensed subscription and one card. | Clean CREATE_SUBSCRIPTION; maybe price/product clone actions. | Destination subscription is active, period end matches source item current_period_end, source is set to cancel at period end. |
| Annual paid subscription | Same as monthly, but annual recurring price. | Clean CREATE_SUBSCRIPTION. | Destination period end remains the annual renewal date; no immediate renewal shift. |
| Quarterly or weekly subscription | Same as monthly, but a price with interval=month, interval_count=3 or interval=week. | Clean CREATE_SUBSCRIPTION with the source interval. | Destination period end matches the source renewal; later renewals follow the same cadence. |
| Multi-item subscription | Source subscription with two licensed items on the same interval and period end (one shared destination price, one missing). | Clean CREATE_SUBSCRIPTION with one items entry per item; CLONE_PRICE only for the missing price. | Destination has one item per source item with matching quantities; source is set to cancel at period end. |
| Inclusive tax_behavior price | Source price has tax_behavior=inclusive. | Clean CREATE_SUBSCRIPTION. | Cloned destination price preserves tax_behavior; renewal amount matches source; destination automatic tax stays disabled. |
| Mid-window repeating coupon | Attach a 3-month repeating percent coupon, let one discounted invoice finalize, then migrate. | Clean coupon clone action carrying the remaining whole months. | Destination discounts exactly the remaining discounted renewals; compare source and destination upcoming-invoice previews. |
| Expiring repeating coupon | Repeating window ends before the subscription's next renewal. | Warning REPEATING_COUPON_EXPIRES_BEFORE_RENEWAL; no destination coupon planned. | Destination renews at the undiscounted amount, same as the source would. |
| Scheduled custom cancel_at | Source subscription has cancel_at set beyond the next renewal (not via cancel_at_period_end). | Warning SOURCE_CANCEL_AT_PERIOD_END_MIRRORED describing the mirrored timestamp. | Destination subscription carries the exact cancel_at; source is set to cancel at period end. |
| Non-collecting recurring subscription | Source customer with one active licensed subscription (any documented interval) whose recurring amount is provably zero: fixed price 0, quantity 0, or a supported subscription-level forever coupon that fully discounts the subtotal. The source and destination customer may have no cards. | Clean CREATE_SUBSCRIPTION with requiresPaymentMethod=false. | Destination subscription is active, period end matches source item current_period_end, and no payment method is bound. |
| Existing source cancellation | Set the source subscription to cancel at period end before dry-run. | Warning SOURCE_CANCEL_AT_PERIOD_END_MIRRORED. | Destination subscription also has cancel_at_period_end=true; source remains canceling. |
| Forever percent coupon | Attach a subscription-level forever percent coupon. | Clean clone/reuse coupon action. | Destination subscription discount matches source discount. |
| Forever amount coupon | Attach a subscription-level forever amount-off coupon with currency. | Clean clone/reuse coupon action. | Destination amount-off discount matches source. |
| Unconsumed once coupon | Attach a subscription-level once coupon before it has been applied to a finalized/open/paid invoice. | Clean coupon action while the latest source invoice has not consumed the coupon. | Destination receives the once coupon exactly once. |
| Visible full once coupon without cards | Attach a subscription-level once coupon that fully discounts the next invoice, then remove all source/destination card instruments. | SOURCE_CUSTOMER_NO_PAYMENT_METHOD or DESTINATION_CUSTOMER_NO_PAYMENT_METHOD. | Do not execute; a once coupon can still lead to non-zero future renewals. |
| Consumed once coupon | Let the once coupon apply to a finalized/open/paid invoice. Test both when Stripe removes it from the subscription and when it remains visible on the subscription. | Warning ONCE_COUPON_ALREADY_CONSUMED; no destination coupon should be planned. | Destination renews at normal future amount. |
| Voided once-coupon invoice | Let a once coupon appear on a latest invoice, then void that invoice. | Blocker VOIDED_ONCE_COUPON_UNSUPPORTED. | Resolve manually; RevBridge cannot infer whether the once coupon should be consumed or still pending. |
| Active trial with card | Source subscription is trialing with future trial_end and a card. | Clean action with trial_exact_end; warning if trial is near its end. | Destination preserves trial end when Stripe accepts it; near-term fallback preserves next charge timing. |
| Active trial without source card | Source trial has no source payment instrument; destination customer has at least one copied card. | Warning TRIAL_WITHOUT_PAYMENT_METHOD. | Destination trial is preserved; operator must verify the buyer-side customer has the intended card before renewal. |
| Customer default fallback | Subscription has no default payment method, but source customer has invoice_settings.default_payment_method. | Warning SOURCE_CUSTOMER_DEFAULT_PAYMENT_METHOD_USED; destination card match required. | Destination subscription is explicitly bound to matched buyer pm_.... |
| Oldest card fallback | No subscription or customer default; source customer has cards. | Warning SOURCE_CUSTOMER_OLDEST_PAYMENT_METHOD_USED or SOURCE_CUSTOMER_OLDEST_SOURCE_USED. | Destination subscription is explicitly bound to the matched oldest card. |
| Latest invoice evidence fallback | No current defaults; latest invoice has PaymentIntent, PaymentMethod, or Charge card evidence. | Warning SOURCE_LATEST_INVOICE_PAYMENT_METHOD_USED or SOURCE_LATEST_INVOICE_SOURCE_USED. | Destination subscription is bound using the matched card fingerprint. |
| Already migrated rerun | Execute once successfully, then create a new project with the same mapping and keys. | Warning SUBSCRIPTION_ALREADY_MIGRATED_SKIPPED; no duplicate create action for the migrated subscription. | Execute is disabled if every subscription is already safely recovered. |
Blocker scenarios
| Scenario | Stripe setup | Expected blocker |
|---|---|---|
| Missing mapping row | Leave one active/trialing source customer out of the mapping CSV. | MISSING_CUSTOMER_MAPPING |
| Destination customer has no card | Copy/map a destination customer with no attached card instruments for a subscription that can collect a future non-zero invoice. | DESTINATION_CUSTOMER_NO_PAYMENT_METHOD |
| Ambiguous destination card | Attach two buyer cards with the same brand, last4, expiry as the selected source card. | SUBSCRIPTION_PAYMENT_METHOD_MAPPING_AMBIGUOUS |
| Existing buyer subscription | Add a non-terminal subscription to the mapped destination customer before dry-run. Terminal canceled and incomplete_expired buyer-side subscriptions are ignored. | DESTINATION_CUSTOMER_HAS_EXISTING_SUBSCRIPTION unless it is a safe RevBridge recovery match. |
| Metered price | Any source subscription item uses a metered recurring price. | METERED_PRICE |
| Mixed item intervals | Source subscription has two items billing on different intervals (for example monthly plus annual). | MIXED_ITEM_INTERVALS |
| Divergent item period ends | Source subscription items report different current_period_end timestamps. | ITEM_PERIOD_END_MISMATCH |
| Subscription schedule | Source subscription is controlled by a subscription schedule or billing schedule. | SUBSCRIPTION_SCHEDULE |
| Paused subscription | Source subscription is paused. | PAUSED_SUBSCRIPTION |
| Unsupported status | Source subscription is incomplete, past_due, unpaid, paused, or malformed trialing. Terminal canceled and incomplete_expired subscriptions are ignored. | UNSUPPORTED_SUBSCRIPTION_STATUS, PAUSED_SUBSCRIPTION, or MISSING_TRIAL_END |
| Unsupported trial settings | Source trial has an unknown trial_settings.end_behavior.missing_payment_method value or another non-empty unsupported trial setting. | TRIAL_SETTINGS_UNSUPPORTED |
| Trial Offer | Source subscription item has a Stripe Trial Offer / current_trial.trial_offer. | TRIAL_OFFER_UNSUPPORTED |
| Presentment details | Source subscription has non-empty presentment_details / adaptive presentment currency state. | PRESENTMENT_DETAILS_UNSUPPORTED |
| Manual collection | Source subscription uses collection_method=send_invoice. | MANUAL_COLLECTION_UNSUPPORTED |
| Custom cancel timestamp before renewal | Source subscription has cancel_at earlier than its next renewal. | CUSTOM_CANCEL_AT_UNSUPPORTED (a cancel_at at or beyond the next renewal is supported and mirrored instead) |
| Billing thresholds | Subscription or item has billing thresholds. | BILLING_THRESHOLDS_UNSUPPORTED |
| Pending update | Source has a pending update or pending invoice item interval. A pending_setup_intent alone (for example on a no-card trial or zero-amount subscription) must NOT block. | PENDING_SUBSCRIPTION_CHANGE_UNSUPPORTED |
| Pending invoice item | Create a pending invoice item for the source customer or source subscription, without attaching it to an invoice. | PENDING_INVOICE_ITEM_UNSUPPORTED |
| Destination pending invoice item | Create a pending invoice item for the mapped destination customer, without attaching it to an invoice. | DESTINATION_PENDING_INVOICE_ITEM_UNSUPPORTED |
| Billing anchor config | Source subscription uses billing_cycle_anchor_config, such as end-of-month billing rules. | BILLING_CYCLE_ANCHOR_CONFIG_UNSUPPORTED |
| Outstanding latest invoice | Source subscription latest invoice has amount_remaining greater than zero. | OUTSTANDING_INVOICE_UNSUPPORTED |
| Source customer outstanding invoice | Create or leave a non-zero draft, open, or uncollectible invoice on the source customer. | CUSTOMER_OUTSTANDING_INVOICE_UNSUPPORTED |
| Destination customer outstanding invoice | Create or leave a non-zero draft, open, or uncollectible invoice on the mapped destination customer. | DESTINATION_CUSTOMER_OUTSTANDING_INVOICE_UNSUPPORTED |
| Unknown billing mode | Source billing mode is neither classic nor flexible, or flexible configuration carries unknown fields. Plain flexible subscriptions must NOT block; the destination must mirror the mode and proration_discounts. | FLEXIBLE_BILLING_MODE_UNSUPPORTED |
| Tax settings | Stripe Tax, customer tax settings/IDs, default tax rates, or item tax rates are enabled. Explicit price tax_behavior alone is supported and must not block. | TAX_UNSUPPORTED, CUSTOMER_TAX_UNSUPPORTED, or DESTINATION_CUSTOMER_TAX_UNSUPPORTED |
| Customer invoice settings | Source or destination customer has invoice custom fields, footer, or rendering options. | CUSTOMER_INVOICE_SETTINGS_UNSUPPORTED or DESTINATION_CUSTOMER_INVOICE_SETTINGS_UNSUPPORTED |
| Promotion code discount | Source subscription discount is promotion-code backed. | PROMOTION_CODE_UNSUPPORTED |
| Item-level discount | Discount is attached to the subscription item. | ITEM_DISCOUNT_UNSUPPORTED |
| Customer-level discount | Source or destination customer has a customer-level discount. | CUSTOMER_DISCOUNT_UNSUPPORTED or DESTINATION_CUSTOMER_DISCOUNT_UNSUPPORTED |
| Customer balance or credit | Source or destination customer has non-zero customer balance, invoice credit balance, or cash balance. | CUSTOMER_BALANCE_UNSUPPORTED or DESTINATION_CUSTOMER_BALANCE_UNSUPPORTED |
| Multiple subscription discounts | Source subscription has more than one discount. | MULTIPLE_DISCOUNTS_UNSUPPORTED |
| Product-scoped coupon | Coupon applies only to selected products. | PRODUCT_SCOPED_COUPON |
| Unmatchable repeating coupon window | Repeating coupon on a weekly or daily subscription where no whole-month clone duration discounts the same renewals. | REPEATING_COUPON_UNSUPPORTED (repeating coupons with matchable windows are carried instead) |
| Unsupported price shape | Tiered billing, transform quantity, custom unit amount, multi-currency currency_options, an undocumented interval value, or missing recurring data. | UNSUPPORTED_PRICE_STRUCTURE, UNSUPPORTED_INTERVAL, or NO_RECURRING_PRICE |
Failure and rerun scenarios
| Scenario | How to create it | Expected behavior |
|---|---|---|
| Bad CSV headers | Upload headers outside the supported pairs. | API returns a readable 400 validation error, not a 500. |
| Duplicate CSV mapping | Repeat a source or destination customer ID. | API returns a readable 400 validation error before Redis is updated. |
| Wrong key mode/account | Use mixed test/live keys or the same Stripe account for both sides. | Key setup returns a readable validation error. |
| Missing read scope | Remove a required source or destination read permission. | Key setup names the failed Stripe read operation. |
| Destination price retrieve permission failure | Use a destination key that cannot retrieve prices. | Dry-run fails with the real Stripe error; RevBridge must not treat it as a missing price clone. |
| Destination coupon retrieve permission failure | Use a destination key that cannot retrieve coupons. | Dry-run fails with the real Stripe error; RevBridge must not treat it as a missing coupon clone. |
| Source drift after dry-run | Change source price, quantity, coupon, discount shape, cancellation flag, scheduled cancel_at, trial behavior, or renewal timestamp after dry-run — including adding, removing, or changing a single item on a multi-item subscription. | Worker stops before buyer-side subscription creation and tells the operator to rerun dry-run. |
| Rerun more than 24 hours after a failure | Fail a run mid-batch, wait past Stripe's idempotency window, re-save keys, rerun dry-run, and execute. | Previously cloned products, prices, and coupons are found by revbridge_source_* lineage metadata and reused; no duplicate buyer-side catalog objects are created. |
| Retry after destination create | Simulate worker retry after destination subscription creation. | Same idempotency key is reused and no duplicate destination subscription is created. |
| Unexpected destination initial invoice | Simulate or force a post-create destination subscription_create invoice with a non-zero amount that is not void, including a paid invoice. | Worker writes a failed CREATE_SUBSCRIPTION audit row with the destination subscription ID and does not cancel the source subscription. A paid initial invoice means Stripe charged earlier than RevBridge expected and needs manual reconciliation. |
| Source cancellation failure | Remove source subscription update permission after destination creation. | Destination remains created; source is not falsely reported as canceled; audit contains failed CANCEL_SOURCE. |
| Preflight destination write failure | Remove destination product write permission. | Worker fails before subscription execution, marks planned subscriptions failed, and writes failed verification audit. |
| Audit export | Run any success and any failure scenario. | CSV and PDF include non-PII source/destination subscription, customer, price, coupon, idempotency, action, status, and message details. |
Post-run verification
For every successful execution:
- Source subscription is scheduled to cancel at period end, not immediately canceled.
- Destination subscription status is
activeortrialing. - Destination item period end equals the source renewal timestamp from dry-run.
- Destination subscription has the expected explicit
default_payment_methodordefault_sourcewhen a card was resolved. - Destination discount state matches the supported source discount state. For repeating coupons, the destination discount's end date must discount exactly the same set of future renewals as the source window.
- Destination items match the source items one-to-one: same quantities and matching prices via ID or
revbridge_source_price_idlineage. - Destination subscription description matches the source subscription description when the source had one.
- No unexpected destination subscriptions were created.
- Audit CSV and PDF tell the same story as the Stripe dashboards.
For every blocker or failure:
- The UI shows a readable error or blocker message.
- The project status matches the outcome.
- No source subscription is canceled unless a safe destination subscription was created or reused first.
- The audit export explains what happened without storing names, emails, card numbers, or raw CSV rows.