Skip to content
Open
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
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cloudflare_workers/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ import { app as on_version_update } from '../../supabase/functions/_backend/trig
import { app as pluginNotifications } from '../../supabase/functions/_backend/triggers/plugin_notifications.ts'
import { app as queue_consumer } from '../../supabase/functions/_backend/triggers/queue_consumer.ts'
import { app as send_email } from '../../supabase/functions/_backend/triggers/send_email.ts'
import { app as stripe_event } from '../../supabase/functions/_backend/triggers/stripe_event.ts'
import { app as stripe_event, appUs as stripe_event_us } from '../../supabase/functions/_backend/triggers/stripe_event.ts'
import { app as webhook_delivery } from '../../supabase/functions/_backend/triggers/webhook_delivery.ts'
import { app as webhook_dispatcher } from '../../supabase/functions/_backend/triggers/webhook_dispatcher.ts'
import { BRES, createAllCatch, createHono } from '../../supabase/functions/_backend/utils/hono.ts'
Expand Down Expand Up @@ -211,6 +211,7 @@ appTriggers.route('/on_version_delete', on_version_delete)
appTriggers.route('/on_manifest_create', on_manifest_create)
appTriggers.route('/on_deploy_history_create', on_deploy_history_create)
appTriggers.route('/stripe_event', stripe_event)
appTriggers.route('/stripe_event_us', stripe_event_us)
appTriggers.route('/on_organization_create', on_organization_create)
appTriggers.route('/cron_stat_app', cron_stat_app)
appTriggers.route('/cron_stat_org', cron_stat_org)
Expand Down
17 changes: 17 additions & 0 deletions read_replicate/schema_replicate.catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,16 @@
"position": 26,
"table": "stripe_info",
"type": "boolean"
},
{
"default": "'ee'::text",
"generated": "",
"identity": "",
"name": "billing_account",
"notNull": true,
"position": 27,
"table": "stripe_info",
"type": "text"
}
],
"constraints": [
Expand Down Expand Up @@ -2023,6 +2033,13 @@
"type": "u",
"valid": true
},
{
"definition": "CHECK (billing_account = ANY (ARRAY['ee'::text, 'us'::text]))",
"name": "stripe_info_billing_account_check",
"table": "stripe_info",
"type": "c",
"valid": true
},
{
"definition": "PRIMARY KEY (customer_id)",
"name": "stripe_info_pkey",
Expand Down
4 changes: 3 additions & 1 deletion read_replicate/schema_replicate.sql
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,9 @@ CREATE TABLE public.stripe_info (
last_stripe_event_at timestamp with time zone,
past_due_at timestamp with time zone,
churn_reason text,
is_above_plan boolean
is_above_plan boolean,
billing_account text DEFAULT 'ee'::text NOT NULL,
CONSTRAINT stripe_info_billing_account_check CHECK ((billing_account = ANY (ARRAY['ee'::text, 'us'::text])))
);

ALTER TABLE ONLY public.stripe_info REPLICA IDENTITY FULL;
Expand Down
11 changes: 6 additions & 5 deletions scripts/backfill_retention_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,19 +1224,20 @@ async function claimProcessedEventsPg(client: PgClient, movements: BackfillReven

const values: string[] = []
const placeholders = chunk.map((movement, index) => {
const offset = index * 3
values.push(movement.event_id, movement.customer_id, movement.date_id)
return `($${offset + 1}, $${offset + 2}, $${offset + 3})`
const offset = index * 4
values.push(movement.event_id, movement.customer_id, movement.date_id, 'ee')
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4})`
})

const { rows } = await client.query<{ event_id: string }>(`
INSERT INTO public.processed_stripe_events (
event_id,
customer_id,
date_id
date_id,
billing_account
)
VALUES ${placeholders.join(', ')}
ON CONFLICT (event_id) DO NOTHING
ON CONFLICT (billing_account, event_id) DO NOTHING
RETURNING event_id
`, values)

Expand Down
6 changes: 3 additions & 3 deletions src/pages/settings/organization/Plans.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const isTrial = computed(() => currentOrganization?.value ? (!currentOrganizatio
const isCreditsOnly = computed(() => isCreditsOnlyOrg(currentOrganization?.value))

async function prefetchStripeCheckoutUrl(plan: Database['public']['Tables']['plans']['Row'], isYear: boolean) {
if (!plan.stripe_id)
if (!plan.name)
return
const supabase = useSupabase()
const session = await supabase.auth.getSession()
Expand All @@ -155,7 +155,7 @@ async function prefetchStripeCheckoutUrl(plan: Database['public']['Tables']['pla
try {
const resp = await invokeCapgoApi('private/stripe_checkout', {
body: JSON.stringify({
priceId: plan.stripe_id,
planName: plan.name,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a US billing account has a distinct product, this account-aware checkout path still records plan.stripe_id (the EE product) in the Checkout Started event. Return or otherwise use the account-resolved product ID for tracking so US checkout analytics are not attributed to EE.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pages/settings/organization/Plans.vue, line 158:

<comment>When a US billing account has a distinct product, this account-aware checkout path still records `plan.stripe_id` (the EE product) in the `Checkout Started` event. Return or otherwise use the account-resolved product ID for tracking so US checkout analytics are not attributed to EE.</comment>

<file context>
@@ -155,7 +155,7 @@ async function prefetchStripeCheckoutUrl(plan: Database['public']['Tables']['pla
     const resp = await invokeCapgoApi('private/stripe_checkout', {
       body: JSON.stringify({
-        priceId: plan.stripe_id,
+        planName: plan.name,
         successUrl,
         cancelUrl,
</file context>

successUrl,
cancelUrl,
recurrence: isYear ? 'year' : 'month',
Expand Down Expand Up @@ -252,7 +252,7 @@ async function openChangePlan(plan: Database['public']['Tables']['plans']['Row']
}
}
else {
const didOpenCheckout = await openCheckout(plan.stripe_id, `${globalThis.location.href}?success=1`, `${globalThis.location.href}?cancel=1`, checkoutIsYearly, currentOrganization?.value?.gid ?? '')
const didOpenCheckout = await openCheckout(plan.name, `${globalThis.location.href}?success=1`, `${globalThis.location.href}?cancel=1`, checkoutIsYearly, currentOrganization?.value?.gid ?? '')
if (didOpenCheckout)
trackPlanCheckoutStarted(plan, checkoutIsYearly, 'direct')
}
Expand Down
4 changes: 2 additions & 2 deletions src/services/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export async function getAffonsoReferral() {
}
}

export async function openCheckout(priceId: string, successUrl: string, cancelUrl: string, isYear: boolean, orgId: string) {
export async function openCheckout(planName: string, successUrl: string, cancelUrl: string, isYear: boolean, orgId: string) {
// console.log('openCheckout')
const supabase = useSupabase()
const session = await supabase.auth.getSession()
Expand All @@ -160,7 +160,7 @@ export async function openCheckout(priceId: string, successUrl: string, cancelUr
try {
const resp = await invokeCapgoApi('private/stripe_checkout', {
body: JSON.stringify({
priceId,
planName,
successUrl,
cancelUrl,
recurrence: isYear ? 'year' : 'month',
Expand Down
18 changes: 18 additions & 0 deletions src/types/supabase.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2686,6 +2686,7 @@ export type Database = {
build_time_unit: number
created_at: string
credit_id: string
credit_id_us: string | null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
description: string
id: string
market_desc: string | null
Expand All @@ -2696,15 +2697,19 @@ export type Database = {
price_m_id: string
price_y: number
price_y_id: string
price_m_id_us: string | null
price_y_id_us: string | null
storage: number
stripe_id: string
stripe_id_us: string | null
updated_at: string
}
Insert: {
bandwidth: number
build_time_unit?: number
created_at?: string
credit_id: string
credit_id_us?: string | null
description?: string
id?: string
market_desc?: string | null
Expand All @@ -2713,17 +2718,21 @@ export type Database = {
native_build_concurrency?: number
price_m?: number
price_m_id: string
price_m_id_us?: string | null
price_y?: number
price_y_id: string
price_y_id_us?: string | null
storage: number
stripe_id?: string
stripe_id_us?: string | null
updated_at?: string
}
Update: {
bandwidth?: number
build_time_unit?: number
created_at?: string
credit_id?: string
credit_id_us?: string | null
description?: string
id?: string
market_desc?: string | null
Expand All @@ -2732,10 +2741,13 @@ export type Database = {
native_build_concurrency?: number
price_m?: number
price_m_id?: string
price_m_id_us?: string | null
price_y?: number
price_y_id?: string
price_y_id_us?: string | null
storage?: number
stripe_id?: string
stripe_id_us?: string | null
updated_at?: string
}
Relationships: []
Expand Down Expand Up @@ -2766,18 +2778,21 @@ export type Database = {
}
processed_stripe_events: {
Row: {
billing_account: string
created_at: string
customer_id: string
date_id: string
event_id: string
}
Insert: {
billing_account?: string
created_at?: string
customer_id: string
date_id: string
event_id: string
}
Update: {
billing_account?: string
created_at?: string
customer_id?: string
date_id?: string
Expand Down Expand Up @@ -3091,6 +3106,7 @@ export type Database = {
created_at: string
customer_country: string | null
customer_id: string
billing_account: string
id: number
is_above_plan: boolean | null
is_good_plan: boolean | null
Expand Down Expand Up @@ -3119,6 +3135,7 @@ export type Database = {
created_at?: string
customer_country?: string | null
customer_id: string
billing_account?: string
id?: number
is_above_plan?: boolean | null
is_good_plan?: boolean | null
Expand Down Expand Up @@ -3147,6 +3164,7 @@ export type Database = {
created_at?: string
customer_country?: string | null
customer_id?: string
billing_account?: string
id?: number
is_above_plan?: boolean | null
is_good_plan?: boolean | null
Expand Down
7 changes: 6 additions & 1 deletion supabase/functions/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ CF_ACCOUNT_ANALYTICS_ID=***
CF_ANALYTICS_TOKEN=***
# To ignore apps that can be used with the API
LIMITED_APPS=[{"id": "***.**.**", "ignore": 1}]
# Stripe
# Stripe (EE account — existing production billing)
STRIPE_WEBHOOK_SECRET=test
STRIPE_SECRET_KEY=test
# Optional US Stripe account scaffolding (leave unset until US billing is enabled)
# STRIPE_SECRET_KEY_US=
# STRIPE_WEBHOOK_SECRET_US=
# New org billing account default: ee (unset = ee). Requires US secrets when set to us.
# STRIPE_NEW_CUSTOMERS_ACCOUNT=ee
# Sentry if omitted, errors will not be sent
SENTRY_DSN=
# Bento to connect email service and send marketing and transactional emails
Expand Down
18 changes: 11 additions & 7 deletions supabase/functions/_backend/private/credits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getClaimsFromJWT, middlewareAuth } from '../utils/hono_jwt.ts'
import { cloudlog, cloudlogErr } from '../utils/logging.ts'
import { checkPermission } from '../utils/rbac.ts'
import { createOneTimeCheckout, getCreditCheckoutDetails, getStripe, isStripeEmulatorEnabled } from '../utils/stripe.ts'
import { normalizeBillingAccount, planProductIdOrFilter, resolveBillingAccount, resolvePlanCreditId } from '../utils/stripe_billing_account.ts'
import { supabaseAdmin, supabaseClient } from '../utils/supabase.ts'
import { getEnv } from '../utils/utils.ts'

Expand Down Expand Up @@ -234,7 +235,7 @@ async function getCreditTopUpProductId(c: AppContext, customerId: string, token:
const supabase = supabaseClient(c, token)
const { data: stripeInfo, error: stripeInfoError } = await supabase
.from('stripe_info')
.select('product_id')
.select('product_id, billing_account')
.eq('customer_id', customerId)
.single()

Expand All @@ -259,13 +260,15 @@ async function getCreditTopUpProductId(c: AppContext, customerId: string, token:
return { productId }
}

const account = normalizeBillingAccount(stripeInfo.billing_account)
const { data: plan, error: planError } = await supabase
.from('plans')
.select('credit_id, name')
.eq('stripe_id', stripeInfo.product_id)
.select('credit_id, credit_id_us, name')
.or(planProductIdOrFilter(stripeInfo.product_id))
.single()

if (planError || !plan?.credit_id) {
const creditId = plan ? resolvePlanCreditId(plan, account) : null
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (planError || !creditId) {
cloudlogErr({
requestId: c.get('requestId'),
message: 'credit_top_up_product_missing',
Expand All @@ -286,7 +289,7 @@ async function getCreditTopUpProductId(c: AppContext, customerId: string, token:
return { productId }
}

return { productId: plan.credit_id }
return { productId: creditId }
}

async function resolveOrgStripeContext(c: AppContext, orgId: string) {
Expand Down Expand Up @@ -593,7 +596,8 @@ app.post('/complete-top-up', middlewareAuth, async (c) => {
const { customerId, token } = await resolveOrgStripeContext(c, body.orgId)
const supabase = supabaseClient(c, token)

const stripe = getStripe(c)
const account = await resolveBillingAccount(c, customerId)
const stripe = getStripe(c, account)
const session = await resolveCheckoutSession(c, stripe, supabase, body.orgId, customerId, body.sessionId)
const resolvedSessionId = session.id

Expand All @@ -609,7 +613,7 @@ app.post('/complete-top-up', middlewareAuth, async (c) => {
const { productId } = await getCreditTopUpProductId(c, customerId, token)
const paymentIntentId = getCheckoutSessionPaymentIntentId(session)

const { creditQuantity, itemsSummary } = await getCreditCheckoutDetails(c, session, productId)
const { creditQuantity, itemsSummary } = await getCreditCheckoutDetails(c, session, productId, customerId)

if (creditQuantity <= 0)
throw simpleError('credit_product_not_found', 'Checkout session does not include the credit product')
Expand Down
31 changes: 28 additions & 3 deletions supabase/functions/_backend/private/stripe_checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { middlewareAuth } from '../utils/hono_jwt.ts'
import { cloudlog } from '../utils/logging.ts'
import { checkPermission } from '../utils/rbac.ts'
import { createCheckout } from '../utils/stripe.ts'
import { supabaseClient } from '../utils/supabase.ts'
import { resolveBillingAccount, resolvePlanProductId } from '../utils/stripe_billing_account.ts'
import { supabaseAdmin, supabaseClient } from '../utils/supabase.ts'
import { getEnv } from '../utils/utils.ts'

interface CheckoutData {
priceId: string
priceId?: string
planName?: string
clientReferenceId?: string
recurrence: 'month' | 'year'
attributionId?: string
Expand All @@ -25,6 +27,27 @@ export const app = new Hono<MiddlewareKeyVariables>()

app.use('/', useCors)

async function resolveCheckoutPlanProductId(c: Parameters<typeof createCheckout>[0], customerId: string, body: CheckoutData) {
if (body.planName) {
const account = await resolveBillingAccount(c, customerId)
const { data: plan, error } = await supabaseAdmin(c)
.from('plans')
.select('stripe_id, stripe_id_us, name')
.eq('name', body.planName)
.single()

if (error || !plan)
throw simpleError('invalid_plan', 'Invalid plan', { planName: body.planName })

return resolvePlanProductId(plan, account)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a customer is assigned to the US account before stripe_id_us is populated, this fallback returns the EE product ID and createCheckout submits it to the US Stripe client. Reject plans missing the selected account’s product ID instead of falling back across Stripe accounts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_backend/private/stripe_checkout.ts, line 42:

<comment>When a customer is assigned to the US account before `stripe_id_us` is populated, this fallback returns the EE product ID and `createCheckout` submits it to the US Stripe client. Reject plans missing the selected account’s product ID instead of falling back across Stripe accounts.</comment>

<file context>
@@ -25,6 +27,27 @@ export const app = new Hono<MiddlewareKeyVariables>()
+    if (error || !plan)
+      throw simpleError('invalid_plan', 'Invalid plan', { planName: body.planName })
+
+    return resolvePlanProductId(plan, account)
+  }
+
</file context>
Suggested change
return resolvePlanProductId(plan, account)
if (account === 'us' && !plan.stripe_id_us)
throw simpleError('invalid_plan', 'Plan is not configured for billing account', { planName: body.planName, billingAccount: account })
return resolvePlanProductId(plan, account)

}

if (body.priceId)
return body.priceId

throw simpleError('no_plan_provided', 'No plan provided')
}

app.post('/', middlewareAuth, async (c) => {
const body = await parseBody<CheckoutData>(c)
cloudlog({ requestId: c.get('requestId'), message: 'post stripe checkout body', body })
Expand Down Expand Up @@ -58,8 +81,10 @@ app.post('/', middlewareAuth, async (c) => {
if (!await checkPermission(c, 'org.update_billing', { orgId: body.orgId }))
throw simpleError('not_authorize', 'Not authorize')

const planProductId = await resolveCheckoutPlanProductId(c, org.customer_id, body)

cloudlog({ requestId: c.get('requestId'), message: 'user', org })
const checkout = await createCheckout(c, org.customer_id, body.recurrence ?? 'month', body.priceId ?? 'price_1KkINoGH46eYKnWwwEi97h1B', body.successUrl ?? `${getEnv(c, 'WEBAPP_URL')}/app/usage`, body.cancelUrl ?? `${getEnv(c, 'WEBAPP_URL')}/app/usage`, body.clientReferenceId, body.attributionId, {
const checkout = await createCheckout(c, org.customer_id, body.recurrence ?? 'month', planProductId, body.successUrl ?? `${getEnv(c, 'WEBAPP_URL')}/app/usage`, body.cancelUrl ?? `${getEnv(c, 'WEBAPP_URL')}/app/usage`, body.clientReferenceId, body.attributionId, {
visitorId: body.datafastVisitorId,
sessionId: body.datafastSessionId,
}, body.affonsoReferral)
Expand Down
Loading
Loading