Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions supabase/functions/_backend/triggers/on_organization_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MiddlewareKeyVariables>()
Expand All @@ -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({
Expand Down
29 changes: 28 additions & 1 deletion supabase/functions/_backend/utils/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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"();
4 changes: 2 additions & 2 deletions supabase/seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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,
Expand Down