From effe20a1cbdd6c9bcf006d4aa77e4a00f064169a Mon Sep 17 00:00:00 2001 From: Daniel Ehrhardt Date: Fri, 9 Oct 2026 09:37:09 +0200 Subject: [PATCH] Cloud: leave other products' Stripe checkouts alone The Stripe account is shared with Godmode. Its checkouts carry no instanceId, so processCheckoutSession treated every completed Godmode trial as an orphan and canceled the subscription a second later. Only handle checkouts created here. --- cloud/src/server/billing-rules.test.ts | 14 ++++++++++++++ cloud/src/server/billing-rules.ts | 8 ++++++++ cloud/src/server/stripe.ts | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/cloud/src/server/billing-rules.test.ts b/cloud/src/server/billing-rules.test.ts index 1e72ebf..7667137 100644 --- a/cloud/src/server/billing-rules.test.ts +++ b/cloud/src/server/billing-rules.test.ts @@ -3,6 +3,7 @@ import { classifyCheckout, complimentaryAfterSubscription, instanceTransition, + isOwnCheckout, isReleasableReservation, isProcessedCheckout, isSubscriptionLive, @@ -147,3 +148,16 @@ describe("complimentary instances", () => { expect(isReleasableReservation({ status: "running", complimentary: false, createdAt: old }, now, ttl)).toBe(false); }); }); + +describe("isOwnCheckout (shared Stripe account)", () => { + it("accepts checkouts created by AutoSEO Cloud", () => { + expect(isOwnCheckout({ metadata: { instanceId: "i1", userId: "u1", slug: "acme" } })).toBe(true); + }); + it.each([ + ["another product's checkout", { metadata: { plan: "monthly", license_key: "GM-XXXX" } }], + ["no metadata", { metadata: null }], + ["instance without user", { metadata: { instanceId: "i1" } }], + ])("ignores %s", (_, session) => { + expect(isOwnCheckout(session)).toBe(false); + }); +}); diff --git a/cloud/src/server/billing-rules.ts b/cloud/src/server/billing-rules.ts index 1e2ed0c..d884ed5 100644 --- a/cloud/src/server/billing-rules.ts +++ b/cloud/src/server/billing-rules.ts @@ -113,3 +113,11 @@ export function isReleasableReservation( ): boolean { return instance.status === "pending_payment" && !instance.complimentary && now - instance.createdAt.getTime() > ttlMs; } + +/** + * The Stripe account is shared with other Codext products. Only checkouts created here carry an instanceId and + * userId; any other checkout belongs to another product and must be left alone (never canceled as an orphan). + */ +export function isOwnCheckout(session: { metadata?: Record | null }): boolean { + return Boolean(session.metadata?.instanceId && session.metadata?.userId); +} diff --git a/cloud/src/server/stripe.ts b/cloud/src/server/stripe.ts index 799168e..6461472 100644 --- a/cloud/src/server/stripe.ts +++ b/cloud/src/server/stripe.ts @@ -22,6 +22,7 @@ import { classifyCheckout, complimentaryAfterSubscription, instanceTransition, + isOwnCheckout, isProcessedCheckout, isSubscriptionLive, shouldApplySubscription, @@ -801,6 +802,10 @@ async function recreateReservation(session: Stripe.Checkout.Session): Promise { if (session.mode !== "subscription" || session.status !== "complete") return null; + if (!isOwnCheckout(session)) { + await logEvent("stripe.checkout_foreign", { data: { sessionId: session.id, source } }); + return null; + } const instanceId = session.metadata?.instanceId; const existing = instanceId ? await getInstance(instanceId) : null; const instance = existing && existing.status !== "deleted" ? existing : await recreateReservation(session);