From f007fbafd534cff8250fed9f562b7d10fe1a3543 Mon Sep 17 00:00:00 2001 From: Anthony Alaribe Date: Sun, 25 Oct 2026 22:57:57 +0100 Subject: [PATCH] Cancel the provider subscription when a project picks Free Choosing Free in the plan picker cleared sub_id but never cancelled the Stripe or LemonSqueezy subscription, so the customer kept being billed for a plan they left. --- src/Pages/Projects.hs | 2 ++ test/integration/Pages/BusinessFlowsSpec.hs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/Pages/Projects.hs b/src/Pages/Projects.hs index 443e10708..1dd6bf5f6 100644 --- a/src/Pages/Projects.hs +++ b/src/Pages/Projects.hs @@ -1419,6 +1419,8 @@ pricingUpdateH pid PricingUpdateForm{orderIdM, plan, isOnboarding} = do Settings.notifyPlanChange envCfg project (ET.planUpgradedEmail productName) _ -> addErrorToast "Something went wrong while fetching subscription id" Nothing Nothing -> do + -- Free clears sub_id below; cancel first, or the provider keeps billing a sub nothing points at. + whenJustM (Projects.projectById pid) (Settings.cancelProjectSubscription envCfg) _ <- updatePricing "Free" "" "" "" auditPlan ("Free" :: Text) handleOnboarding diff --git a/test/integration/Pages/BusinessFlowsSpec.hs b/test/integration/Pages/BusinessFlowsSpec.hs index f37e86460..469f3fb29 100644 --- a/test/integration/Pages/BusinessFlowsSpec.hs +++ b/test/integration/Pages/BusinessFlowsSpec.hs @@ -477,6 +477,14 @@ stripeBillingTests = do (reqs, _) <- runAsBaseRecordingHTTP tr $ atAuthToBase tr.trSessAndHeader $ CreateProject.deleteProjectGetH testPid map fst reqs `shouldContain` ["https://api.stripe.com/v1/subscriptions/sub_test123"] + -- Choosing Free cleared sub_id but left the Stripe sub running, still billing the customer. + it "freePlanChoice_cancelsTheRunningStripeSubscription" \TestContext{tcResources = tr, tcProjectId = testPid} -> do + _ <- runQueryEffect tr $ Projects.updateStripeProjectBilling testPid (Projects.PlanName "GraduatedPricing") (Projects.SubId "sub_test123") (Projects.SubItemId "si_test") (Projects.CustomerId "cus_test") + session <- refreshSession tr.trPool tr.trATCtx.hasqlPool tr.trSessAndHeader + (reqs, _) <- runAsBaseRecordingHTTP tr $ atAuthToBase session $ CreateProject.pricingUpdateH testPid def + map fst reqs `shouldContain` ["https://api.stripe.com/v1/subscriptions/sub_test123"] + fmap (.paymentPlan) <$> runQueryEffect tr (Projects.projectById testPid) `shouldReturn` Just "Free" + it "deleteProject cancels the project's LemonSqueezy subscription" \TestContext{tcResources = tr, tcProjectId = testPid} -> do _ <- runQueryEffect tr $ Projects.updateProjectPricing testPid (Projects.PlanName "GraduatedPricing") (Projects.SubId "987654") (Projects.SubItemId "si_test") (Projects.OrderId "ord_test") (reqs, _) <- runAsBaseRecordingHTTP tr $ atAuthToBase tr.trSessAndHeader $ CreateProject.deleteProjectGetH testPid