From 438f6ec943c339745a67b4a98189db2683481ee1 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 15 Jan 2026 06:28:39 +0100 Subject: [PATCH] fix: create stripe_info synchronously on org creation to fix trial delay --- .../triggers/on_organization_create.ts | 10 ++- supabase/functions/_backend/utils/supabase.ts | 29 ++++++++- ...5051444_sync_stripe_info_on_org_create.sql | 62 +++++++++++++++++++ supabase/seed.sql | 4 +- 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql diff --git a/supabase/functions/_backend/triggers/on_organization_create.ts b/supabase/functions/_backend/triggers/on_organization_create.ts index a9a29310dc..11ab440e95 100644 --- a/supabase/functions/_backend/triggers/on_organization_create.ts +++ b/supabase/functions/_backend/triggers/on_organization_create.ts @@ -5,7 +5,7 @@ import { trackBentoEvent } from '../utils/bento.ts' import { BRES, middlewareAPISecret, simpleError, triggerValidator } from '../utils/hono.ts' import { cloudlog } from '../utils/logging.ts' import { logsnag } from '../utils/logsnag.ts' -import { createStripeCustomer } from '../utils/supabase.ts' +import { createStripeCustomer, finalizePendingStripeCustomer } from '../utils/supabase.ts' import { backgroundTask } from '../utils/utils.ts' export const app = new Hono() @@ -19,8 +19,12 @@ app.post('/', middlewareAPISecret, triggerValidator('orgs', 'INSERT'), async (c) throw simpleError('no_id', 'No id', { record }) } - if (!record.customer_id) - await createStripeCustomer(c, record as any) + if (!record.customer_id) { + await createStripeCustomer(c, record) + } + else if (record.customer_id.startsWith('pending_')) { + await finalizePendingStripeCustomer(c, record) + } const LogSnag = logsnag(c) await backgroundTask(c, LogSnag.track({ diff --git a/supabase/functions/_backend/utils/supabase.ts b/supabase/functions/_backend/utils/supabase.ts index 9b95cb5b01..6944c75ffe 100644 --- a/supabase/functions/_backend/utils/supabase.ts +++ b/supabase/functions/_backend/utils/supabase.ts @@ -721,7 +721,6 @@ export async function getDefaultPlan(c: Context) { export async function createStripeCustomer(c: Context, org: Database['public']['Tables']['orgs']['Row']) { const customer = await createCustomer(c, org.management_email, org.created_by, org.id, org.name) - // create date + 15 days const trial_at = new Date() trial_at.setDate(trial_at.getDate() + 15) const soloPlan = await getDefaultPlan(c) @@ -751,6 +750,34 @@ export async function createStripeCustomer(c: Context, org: Database['public'][' cloudlog({ requestId: c.get('requestId'), message: 'stripe_info done' }) } +export async function finalizePendingStripeCustomer(c: Context, org: Database['public']['Tables']['orgs']['Row']) { + const pendingCustomerId = org.customer_id + if (!pendingCustomerId?.startsWith('pending_')) { + cloudlog({ requestId: c.get('requestId'), message: 'finalizePendingStripeCustomer: not a pending customer_id', pendingCustomerId }) + return + } + + await createStripeCustomer(c, { ...org, customer_id: null }) + + const { data: updatedOrg } = await supabaseAdmin(c) + .from('orgs') + .select('customer_id') + .eq('id', org.id) + .single() + + if (!updatedOrg?.customer_id || updatedOrg.customer_id.startsWith('pending_')) { + cloudlogErr({ requestId: c.get('requestId'), message: 'finalizePendingStripeCustomer: org still has pending customer_id, skipping delete' }) + return + } + + const { error: deleteError } = await supabaseAdmin(c) + .from('stripe_info') + .delete() + .eq('customer_id', pendingCustomerId) + if (deleteError) + cloudlogErr({ requestId: c.get('requestId'), message: 'finalizePendingStripeCustomer: orphan pending stripe_info', deleteError }) +} + export function trackBandwidthUsageSB( c: Context, deviceId: string, diff --git a/supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql b/supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql new file mode 100644 index 0000000000..ab2b98d3c2 --- /dev/null +++ b/supabase/migrations/20260115051444_sync_stripe_info_on_org_create.sql @@ -0,0 +1,62 @@ +-- Fix race condition: create stripe_info synchronously on org creation +-- Pending customer_id (pending_{org_id}) is replaced with real Stripe customer_id by async handler + +CREATE OR REPLACE FUNCTION "public"."generate_org_user_stripe_info_on_org_create"() + RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO '' +AS $$ +DECLARE + solo_plan_stripe_id VARCHAR; + pending_customer_id VARCHAR; + trial_at_date TIMESTAMPTZ; +BEGIN + INSERT INTO public.org_users (user_id, org_id, user_right) + VALUES (NEW.created_by, NEW.id, 'super_admin'::"public"."user_min_right"); + + IF NEW.customer_id IS NOT NULL THEN + RETURN NEW; + END IF; + + SELECT stripe_id INTO solo_plan_stripe_id + FROM public.plans + WHERE name = 'Solo' + LIMIT 1; + + IF solo_plan_stripe_id IS NULL THEN + RAISE WARNING 'Solo plan not found, skipping sync stripe_info creation for org %', NEW.id; + RETURN NEW; + END IF; + + pending_customer_id := 'pending_' || NEW.id::text; + trial_at_date := NOW() + INTERVAL '15 days'; + + INSERT INTO public.stripe_info ( + customer_id, + product_id, + trial_at, + status, + is_good_plan + ) VALUES ( + pending_customer_id, + solo_plan_stripe_id, + trial_at_date, + NULL, + true + ); + + UPDATE public.orgs + SET customer_id = pending_customer_id + WHERE id = NEW.id; + + RETURN NEW; +END $$; + +DROP TRIGGER IF EXISTS "generate_org_user_on_org_create" ON "public"."orgs"; + +CREATE TRIGGER "generate_org_user_stripe_info_on_org_create" + AFTER INSERT ON "public"."orgs" + FOR EACH ROW + EXECUTE FUNCTION "public"."generate_org_user_stripe_info_on_org_create"(); + +DROP FUNCTION IF EXISTS "public"."generate_org_user_on_org_create"(); diff --git a/supabase/seed.sql b/supabase/seed.sql index f2d7acae64..2d63f9d294 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -249,7 +249,7 @@ BEGIN ('2022-06-03 05:54:15+00', '', 'encrypted', 'Capgo', NULL, 'encrypted@capgo.app', 'f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f708193', NOW(), 't', 't'); ALTER TABLE public.users ENABLE TRIGGER generate_org_on_user_create; - ALTER TABLE public.orgs DISABLE TRIGGER generate_org_user_on_org_create; + ALTER TABLE public.orgs DISABLE TRIGGER generate_org_user_stripe_info_on_org_create; INSERT INTO "public"."orgs" ("id", "created_by", "created_at", "updated_at", "logo", "name", "management_email", "customer_id") VALUES ('22dbad8a-b885-4309-9b3b-a09f8460fb6d', 'c591b04e-cf29-4945-b9a0-776d0672061a', NOW(), NOW(), '', 'Admin org', 'admin@capgo.app', 'cus_Pa0k8TO6HVln6A'), ('046a36ac-e03c-4590-9257-bd6c9dba9ee8', '6aa76066-55ef-4238-ade6-0b32334a4097', NOW(), NOW(), '', 'Demo org', 'test@capgo.app', 'cus_Q38uE91NP8Ufqc'), @@ -260,7 +260,7 @@ BEGIN ('f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b92', 'e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a81', NOW(), NOW(), '', 'CLI Hashed Test Org', 'cli_hashed@capgo.app', 'cus_cli_hashed_test_123'), ('a7b8c9d0-e1f2-4a3b-9c4d-5e6f7a8b9ca4', 'f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f708193', NOW(), NOW(), '', 'Encrypted Test Org', 'encrypted@capgo.app', 'cus_encrypted_test_123'), ('e5f6a7b8-c9d0-4e1f-9a2b-3c4d5e6f7a82', '6aa76066-55ef-4238-ade6-0b32334a4097', NOW(), NOW(), '', 'Private Error Test Org', 'test@capgo.app', NULL); - ALTER TABLE public.orgs ENABLE TRIGGER generate_org_user_on_org_create; + ALTER TABLE public.orgs ENABLE TRIGGER generate_org_user_stripe_info_on_org_create; INSERT INTO public.usage_credit_grants ( org_id,