Skip to content

Allow credit usage without active plan#1575

Merged
riderx merged 28 commits into
mainfrom
riderx/allow-credit-no-plan
Feb 6, 2026
Merged

Allow credit usage without active plan#1575
riderx merged 28 commits into
mainfrom
riderx/allow-credit-no-plan

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Feb 4, 2026

Summary (AI generated)

  • Use Solo plan credit fallback when no Stripe plan is configured.
  • Treat available credits as allowing usage checks even without active plans.

Test plan (AI generated)

  • bun run lint:backend

Screenshots (AI generated)

Not applicable (backend-only change).

Checklist (AI generated)

  • My code follows the code style of this project and passes
    .
  • My change requires a change to the documentation.
  • I have updated the documentation
    accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce
    my tests

Generated with AI

Summary by CodeRabbit

  • New Features

    • Orgs can use credits without a traditional subscription; top-ups now derive a fallback credit product when needed.
    • New org listing returns a comprehensive, permission-aware org view.
  • Improvements

    • Centralized, stricter Stripe and Bento configuration checks.
    • More reliable billing-cycle defaults and status-aware plan logic.
  • Bug Fixes / Behavior

    • Replaced error-driven failures with data-driven fallbacks; removed audit trails and plan-cache purge side effects.
  • Tests

    • Strengthened plan, queue, and audit tests for robustness and batch handling.

Copilot AI review requested due to automatic review settings February 4, 2026 05:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 4, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Replaces throw-on-missing Stripe credit configuration with a data-driven fallback (Solo plan lookup); removes Stripe audit logging and plan-cache purge side effects; centralizes Stripe/Bento configuration checks; surfaces stripe_info.status; adds billing-cycle defaults and a permission-aware get_orgs_v7 migration.

Changes

Cohort / File(s) Summary
Credit fallback util & usage
supabase/functions/_backend/utils/credits.ts, supabase/functions/_backend/private/credits.ts
Adds getFallbackCreditProductId and uses it where credit-product resolution previously threw; missing/unsupported Stripe product_id now resolves via a Solo-plan lookup instead of raising credit_product_not_configured.
Stripe webhook & handlers
supabase/functions/_backend/triggers/stripe_event.ts
Removes audit logging and plan-cache purge/background tasks; simplifies createdOrUpdated and updateStripeInfo signatures (removed previousStripeInfo/org args); routes missing credit resolution to fallback util; updates imports and webhook call sites.
Stripe utilities & middleware
supabase/functions/_backend/utils/stripe.ts, supabase/functions/_backend/utils/utils.ts, supabase/functions/_backend/utils/hono_middleware_stripe.ts
Centralizes Stripe-availability checks to isStripeConfigured(c), broadens live-key detection to sk_ and rk_, and replaces ad-hoc env checks across Stripe helpers and middleware.
Plan / credits logic & PG helpers
supabase/functions/_backend/utils/plans.ts, supabase/functions/_backend/utils/pg.ts
Adds billing-cycle defaults, surfaces stripe_info.status, introduces hasCreditsExpression so plan validation accepts orgs with available credits (supports credits-only org flows).
Database migration
supabase/migrations/20260127121000_allow_credits_without_plan.sql
Adds permission-aware get_orgs_v7 (multi-CTE org view) and helper functions is_paying_and_good_plan_org / is_paying_and_good_plan_org_action; sets function ownership to postgres.
Bento config & email guards
supabase/functions/_backend/utils/bento.ts, supabase/functions/_backend/utils/org_email_notifications.ts
Adds isBentoConfigured predicate and early-exit guards in notification/email helpers when Bento is not configured; filters placeholder env values.
Tests
supabase/tests/11_test_plan.sql, tests/queue_big_job_archive.test.ts, tests/audit-logs.test.ts
Adjusts test state/setup: sets credits_consumed = credits_total in plan test; expands queue archive test to batch sync with retries/backoff and longer timeout; inserts test creator as org_user for audit tests.
Misc small changes
supabase/functions/_backend/utils/supabase.ts, supabase/functions/_backend/utils/bento.ts, supabase/functions/_backend/utils/pg.ts
Adds local Stripe secret-key guard, formatting and expression tweaks, and placeholder filtering; no public API signature changes except the two webhook handler signatures.

Sequence Diagram(s)

sequenceDiagram
    participant Stripe as Stripe Webhook
    participant Handler as stripe_event Handler
    participant Credits as Credits Util
    participant DB as Database (plans)
    Stripe->>Handler: deliver event (customerId, product_id)
    Handler->>Handler: extract identifiers, decide flow
    Handler->>Credits: getCreditTopUpProductId(customerId, product_id)
    Credits->>DB: look up product -> find credit_id
    alt product_id missing or credit_id not found
        Credits->>Credits: call getFallbackCreditProductId(customerId, fetchPlan)
        Credits->>DB: query `plans` for "Solo" plan
        DB-->>Credits: return Solo plan.credit_id
        Credits-->>Handler: return fallback credit_id
    else product_id maps to credit
        DB-->>Credits: return mapped credit_id
        Credits-->>Handler: return mapped credit_id
    end
    Handler->>Handler: continue top-up processing using returned credit_id
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through plans by moonlit code,
When Stripe fell quiet I found another road.
Solo left breadcrumbs, a cozy little clue —
Fallback fetched, credits topped, the flow ran through.
Carrots and logs — a rabbit's thankful cue.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description lacks critical sections from the template. The 'Test plan' is generic ('bun run lint:backend'), missing detailed reproduction steps. Checklist items are present but entirely unchecked. The description is mostly AI-generated and incomplete. Add concrete test steps to verify credit-only usage works correctly. Complete and check relevant checklist items. If applicable, provide manual testing steps or E2E test verification.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Allow credit usage without active plan' is clear and directly summarizes the main objective: enabling credit-based usage even when there is no active Stripe subscription plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/allow-credit-no-plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27587ce270

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +197 to 216
const hasActivePlan = org?.stripe_info?.status === 'succeeded'
const totalStats = await getTotalStats(c, orgId)
const currentPlanName = await getCurrentPlanNameOrg(c, orgId)
if (!totalStats) {
return false
}

const { data: currentPlan, error: currentPlanError } = await supabaseAdmin(c)
.from('plans')
.select('*')
.eq('name', currentPlanName)
.single()
if (currentPlanError) {
cloudlogErr({ requestId: c.get('requestId'), message: 'currentPlanError', error: currentPlanError })
let currentPlanName: string | null = null
let currentPlan: Database['public']['Tables']['plans']['Row'] | null = null
if (hasActivePlan) {
currentPlanName = await getCurrentPlanNameOrg(c, orgId)
const { data, error: currentPlanError } = await supabaseAdmin(c)
.from('plans')
.select('*')
.eq('name', currentPlanName)
.single()
if (currentPlanError) {
cloudlogErr({ requestId: c.get('requestId'), message: 'currentPlanError', error: currentPlanError })
}
currentPlan = data ?? null
}
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 Badge Load plan limits for free orgs without active Stripe status

The new hasActivePlan gate means getCurrentPlanNameOrg() is only called when stripe_info.status === 'succeeded'. For orgs on the free/default plan (no Stripe subscription or status), currentPlan stays null and later limits are coerced to 0, so any usage becomes overage and triggers exceeded flags/upgrade notifications. This changes behavior for free-plan orgs that should still get the Solo limits. Consider always fetching the plan name (it already falls back to Solo) and only skip paid-plan checks/notifications based on status, rather than skipping the plan lookup entirely.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request enables organizations to use credits for service usage even when they don't have an active Stripe subscription plan. The changes implement a "credits-only" usage model with a Solo plan fallback for credit product configuration.

Changes:

  • Modified database functions to allow service usage when credits are available (can_use_more returns true if credits exist)
  • Added Solo plan fallback logic in backend code when retrieving credit product IDs for organizations without configured Stripe plans
  • Updated plan validation to treat available credits as sufficient authorization for actions

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
supabase/migrations/20260127121000_allow_credits_without_plan.sql Updates get_orgs_v7, is_paying_and_good_plan_org, and is_paying_and_good_plan_org_action functions to check for available credits as an alternative to active plans
supabase/functions/_backend/utils/plans.ts Modifies applyCreditsForMetric to log when plan context is missing but billing cycle exists; updates userAbovePlan to handle credit-only organizations and adds early return logic for organizations without active plans
supabase/functions/_backend/utils/pg.ts Enhances buildPlanValidationExpression to check for available credits before requiring active subscription status
supabase/functions/_backend/triggers/stripe_event.ts Adds Solo plan fallback when getCreditTopUpProductIdFromCustomer cannot find organization's credit product configuration
supabase/functions/_backend/private/credits.ts Implements Solo plan fallback in getCreditTopUpProductId when credit product lookup fails for organizations

Comment on lines +61 to +79
let fallbackPlan: { credit_id: string | null } | undefined
try {
;[fallbackPlan] = await drizzleClient
.select({ credit_id: schema.plans.credit_id })
.from(schema.plans)
.where(eq(schema.plans.name, 'Solo'))
.limit(1)
}
catch (error) {
cloudlog({
requestId: c.get('requestId'),
message: 'credit_fallback_plan_missing',
customerId,
error,
})
}
if (!fallbackPlan?.credit_id)
throw simpleError('credit_product_not_configured', 'Credit product is not configured')
return fallbackPlan.credit_id
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

This fallback logic for fetching the Solo plan's credit_id is duplicated in four places across two files (twice in this file, twice in credits.ts). Consider extracting this into a shared helper function like getFallbackCreditProductId(c: Context, customerId: string) in a common utility module to improve maintainability and reduce the risk of inconsistencies.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +99
cloudlogErr({
requestId: c.get('requestId'),
message: 'credit_fallback_plan_missing',
customerId,
error: fallbackError,
})
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The logging approach differs between this file and stripe_event.ts. Here, cloudlogErr is used for logging the fallback plan query failure, while stripe_event.ts uses cloudlog (non-error). For consistency, consider using the same logging level across both files since this represents the same fallback scenario.

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 86
@@ -84,6 +84,15 @@ async function applyCreditsForMetric(
credit_step_id: null,
}
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

When there's no billing cycle (billingCycle is null or missing subscription_anchor_start/end), credits cannot be applied and all overage is returned as unpaid. However, the PR's goal is to allow credit usage without an active plan. Without an active plan, organizations might not have a billing cycle configured (see get_cycle_info_org which depends on stripe_info.subscription_anchor_start). This creates a situation where organizations with only credits cannot actually use them for overage, defeating the purpose of this PR. Consider either creating a default billing cycle for credit-only organizations or modifying the credit application logic to work without billing cycle information.

Copilot uses AI. Check for mistakes.
@riderx riderx force-pushed the riderx/allow-credit-no-plan branch from 27587ce to 843ad87 Compare February 5, 2026 01:11
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/utils/plans.ts`:
- Around line 110-111: The TypeScript error comes from passing
resolvedBillingCycle.subscription_anchor_start and subscription_anchor_end
(typed as string | null) into RPC params that require string; update the
assignment of p_billing_cycle_start and p_billing_cycle_end to assert non-null
(e.g., use the non-null assertion on
resolvedBillingCycle.subscription_anchor_start! and
resolvedBillingCycle.subscription_anchor_end!) or alternatively change
getDefaultBillingCycleRange to return a type with non-nullable fields and use
that type for resolvedBillingCycle so the properties are typed as string,
ensuring the RPC parameters p_billing_cycle_start and p_billing_cycle_end
receive non-nullable strings.
🧹 Nitpick comments (1)
supabase/functions/_backend/utils/plans.ts (1)

43-50: Consider a non-nullable return type for getDefaultBillingCycleRange.

The helper always returns non-null ISO strings, but returns BillingCycleInfo which has nullable fields. A dedicated non-nullable interface would provide type safety without assertions.

♻️ Optional: Introduce a non-nullable type
+interface BillingCycleInfoRequired {
+  subscription_anchor_start: string
+  subscription_anchor_end: string
+}
+
-function getDefaultBillingCycleRange(referenceDate = new Date()): BillingCycleInfo {
+function getDefaultBillingCycleRange(referenceDate = new Date()): BillingCycleInfoRequired {
   const start = new Date(Date.UTC(referenceDate.getUTCFullYear(), referenceDate.getUTCMonth(), 1, 0, 0, 0, 0))
   const end = new Date(Date.UTC(referenceDate.getUTCFullYear(), referenceDate.getUTCMonth() + 1, 1, 0, 0, 0, 0))
   return {

Then type resolvedBillingCycle as BillingCycleInfoRequired.

Also applies to: 91-93

Comment thread supabase/functions/_backend/utils/plans.ts Outdated
@riderx riderx force-pushed the riderx/allow-credit-no-plan branch from 92be18c to 85f7ab0 Compare February 5, 2026 04:50
@riderx riderx force-pushed the riderx/allow-credit-no-plan branch from 0facd5a to eac9d88 Compare February 6, 2026 03:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@supabase/migrations/20260127121000_allow_credits_without_plan.sql`:
- Around line 271-295: The two functions disagree on how trial expiration is
compared; unify them by using the same timestamp-granular comparison (trial_at >
now()) in both functions. Update the trial check in is_paying_and_good_plan_org
(replace the trial_at::date - (now())::date > 0 expression) so it matches the
check used in is_paying_and_good_plan_org_action (si.trial_at > now()), ensuring
both functions use si.trial_at > now() for consistency.
🧹 Nitpick comments (5)
supabase/tests/11_test_plan.sql (1)

27-31: Consider adding tests for the new credit-bypass path.

The UPDATE correctly exhausts credits so existing plan-action tests remain valid. However, this test file doesn't exercise the new behavior where available_credits > 0 bypasses plan checks — which is the core feature of this PR. Consider adding at least one test case that verifies is_paying_and_good_plan_org_action returns true when the org has available credits but no active/good plan.

supabase/functions/_backend/private/credits.ts (1)

90-100: Extract duplicate fallback callback.

The fetchPlan callbacks on lines 90-98 and 117-125 are identical. Consider extracting to a local helper to reduce duplication.

♻️ Proposed refactor
 async function getCreditTopUpProductId(c: AppContext, customerId: string, token: string): Promise<{ productId: string }> {
   const supabase = supabaseClient(c, token)
+  const fetchSoloCreditId = async () => {
+    const { data, error } = await supabase
+      .from('plans')
+      .select('credit_id')
+      .eq('name', 'Solo')
+      .single()
+    if (error)
+      throw error
+    return data ?? null
+  }
+
   const { data: stripeInfo, error: stripeInfoError } = await supabase
     ...
 
   if (stripeInfoError || !stripeInfo?.product_id) {
     ...
-    const productId = await getFallbackCreditProductId(c, customerId, async () => {
-      const { data, error } = await supabase
-        .from('plans')
-        .select('credit_id')
-        .eq('name', 'Solo')
-        .single()
-      if (error)
-        throw error
-      return data ?? null
-    })
+    const productId = await getFallbackCreditProductId(c, customerId, fetchSoloCreditId)
     return { productId }
   }
   ...
   if (planError || !plan?.credit_id) {
     ...
-    const productId = await getFallbackCreditProductId(c, customerId, async () => {
-      const { data, error } = await supabase
-        .from('plans')
-        .select('credit_id')
-        .eq('name', 'Solo')
-        .single()
-      if (error)
-        throw error
-      return data ?? null
-    })
+    const productId = await getFallbackCreditProductId(c, customerId, fetchSoloCreditId)
     return { productId }
   }

Also applies to: 117-127

supabase/migrations/20260127121000_allow_credits_without_plan.sql (1)

141-149: user_meets_password_policy is called twice per org row.

Lines 145-146 evaluate public.user_meets_password_policy(userid, o.id) and then NOT public.user_meets_password_policy(userid, o.id). Consider computing it once and deriving both values:

♻️ Proposed refactor
   password_policy_access AS (
     SELECT
       o.id AS org_id,
       o.password_policy_config,
-      public.user_meets_password_policy(userid, o.id) AS password_has_access,
-      NOT public.user_meets_password_policy(userid, o.id) AS should_redact_password
+      ppa_inner.meets_policy AS password_has_access,
+      NOT ppa_inner.meets_policy AS should_redact_password
     FROM public.orgs o
     JOIN user_orgs uo ON uo.org_id = o.id
+    CROSS JOIN LATERAL (
+      SELECT public.user_meets_password_policy(userid, o.id) AS meets_policy
+    ) ppa_inner
   )
supabase/functions/_backend/triggers/stripe_event.ts (1)

62-69: Same duplicate fallback pattern as in credits.ts.

The Solo plan fallback callback (lines 62-68 and 93-99) is duplicated within this function. Consider extracting it, similar to the suggestion for credits.ts.

♻️ Proposed refactor
 async function getCreditTopUpProductIdFromCustomer(c: Context, customerId: string): Promise<string> {
   const pgClient = getPgClient(c, true)
   const drizzleClient = getDrizzleClient(pgClient)
+  const fetchSoloCreditId = async () => {
+    const [fallbackPlan] = await drizzleClient
+      .select({ credit_id: schema.plans.credit_id })
+      .from(schema.plans)
+      .where(eq(schema.plans.name, 'Solo'))
+      .limit(1)
+    return fallbackPlan ?? null
+  }

   try {
     ...
     if (stripeInfoError || !stripeInfo?.product_id) {
       ...
-      return await getFallbackCreditProductId(c, customerId, async () => {
-        const [fallbackPlan] = await drizzleClient
-          .select({ credit_id: schema.plans.credit_id })
-          .from(schema.plans)
-          .where(eq(schema.plans.name, 'Solo'))
-          .limit(1)
-        return fallbackPlan ?? null
-      })
+      return await getFallbackCreditProductId(c, customerId, fetchSoloCreditId)
     }
     ...
     if (planError || !plan?.credit_id) {
       ...
-      return await getFallbackCreditProductId(c, customerId, async () => {
-        const [fallbackPlan] = await drizzleClient
-          .select({ credit_id: schema.plans.credit_id })
-          .from(schema.plans)
-          .where(eq(schema.plans.name, 'Solo'))
-          .limit(1)
-        return fallbackPlan ?? null
-      })
+      return await getFallbackCreditProductId(c, customerId, fetchSoloCreditId)
     }

Also applies to: 93-100

supabase/functions/_backend/utils/plans.ts (1)

117-118: Redundant non-null assertions.

resolvedBillingCycle is typed as BillingCycleRange where both fields are string (non-nullable). The ! operators on lines 117-118 are unnecessary.

♻️ Remove redundant assertions
-        p_billing_cycle_start: resolvedBillingCycle.subscription_anchor_start!,
-        p_billing_cycle_end: resolvedBillingCycle.subscription_anchor_end!,
+        p_billing_cycle_start: resolvedBillingCycle.subscription_anchor_start,
+        p_billing_cycle_end: resolvedBillingCycle.subscription_anchor_end,

Comment on lines +271 to +295
CREATE OR REPLACE FUNCTION "public"."is_paying_and_good_plan_org_action"("orgid" "uuid", "actions" "public"."action_type"[]) RETURNS boolean
LANGUAGE "plpgsql" SECURITY DEFINER
SET "search_path" TO ''
AS $$
DECLARE org_customer_id text; result boolean; has_credits boolean;
BEGIN
SELECT EXISTS (
SELECT 1
FROM public.usage_credit_balances ucb
WHERE ucb.org_id = orgid
AND COALESCE(ucb.available_credits, 0) > 0
) INTO has_credits;

IF has_credits THEN
RETURN true;
END IF;

SELECT o.customer_id INTO org_customer_id FROM public.orgs o WHERE o.id = orgid;
SELECT (si.trial_at > now()) OR (si.status = 'succeeded' AND NOT (
(si.mau_exceeded AND 'mau' = ANY(actions)) OR (si.storage_exceeded AND 'storage' = ANY(actions)) OR
(si.bandwidth_exceeded AND 'bandwidth' = ANY(actions)) OR (si.build_time_exceeded AND 'build_time' = ANY(actions))))
INTO result FROM public.stripe_info si WHERE si.customer_id = org_customer_id LIMIT 1;
RETURN COALESCE(result, false);
END;
$$;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent trial date comparison between the two functions.

is_paying_and_good_plan_org (line 262) uses date arithmetic: trial_at::date - (now())::date > 0, which is day-granular (trial day itself is excluded). is_paying_and_good_plan_org_action (line 289) uses si.trial_at > now(), which is timestamp-granular (trial end is the exact moment). This can produce different results near midnight or on the trial expiry day itself.

Consider aligning both to the same comparison style for consistency.

Proposed fix for consistency
 -- In is_paying_and_good_plan_org_action, align trial check:
-  SELECT (si.trial_at > now()) OR (si.status = 'succeeded' AND NOT (
+  SELECT (si.trial_at::date > now()::date) OR (si.status = 'succeeded' AND NOT (

Or alternatively update is_paying_and_good_plan_org to use trial_at > now() — just pick one style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE OR REPLACE FUNCTION "public"."is_paying_and_good_plan_org_action"("orgid" "uuid", "actions" "public"."action_type"[]) RETURNS boolean
LANGUAGE "plpgsql" SECURITY DEFINER
SET "search_path" TO ''
AS $$
DECLARE org_customer_id text; result boolean; has_credits boolean;
BEGIN
SELECT EXISTS (
SELECT 1
FROM public.usage_credit_balances ucb
WHERE ucb.org_id = orgid
AND COALESCE(ucb.available_credits, 0) > 0
) INTO has_credits;
IF has_credits THEN
RETURN true;
END IF;
SELECT o.customer_id INTO org_customer_id FROM public.orgs o WHERE o.id = orgid;
SELECT (si.trial_at > now()) OR (si.status = 'succeeded' AND NOT (
(si.mau_exceeded AND 'mau' = ANY(actions)) OR (si.storage_exceeded AND 'storage' = ANY(actions)) OR
(si.bandwidth_exceeded AND 'bandwidth' = ANY(actions)) OR (si.build_time_exceeded AND 'build_time' = ANY(actions))))
INTO result FROM public.stripe_info si WHERE si.customer_id = org_customer_id LIMIT 1;
RETURN COALESCE(result, false);
END;
$$;
CREATE OR REPLACE FUNCTION "public"."is_paying_and_good_plan_org_action"("orgid" "uuid", "actions" "public"."action_type"[]) RETURNS boolean
LANGUAGE "plpgsql" SECURITY DEFINER
SET "search_path" TO ''
AS $$
DECLARE org_customer_id text; result boolean; has_credits boolean;
BEGIN
SELECT EXISTS (
SELECT 1
FROM public.usage_credit_balances ucb
WHERE ucb.org_id = orgid
AND COALESCE(ucb.available_credits, 0) > 0
) INTO has_credits;
IF has_credits THEN
RETURN true;
END IF;
SELECT o.customer_id INTO org_customer_id FROM public.orgs o WHERE o.id = orgid;
SELECT (si.trial_at::date > now()::date) OR (si.status = 'succeeded' AND NOT (
(si.mau_exceeded AND 'mau' = ANY(actions)) OR (si.storage_exceeded AND 'storage' = ANY(actions)) OR
(si.bandwidth_exceeded AND 'bandwidth' = ANY(actions)) OR (si.build_time_exceeded AND 'build_time' = ANY(actions))))
INTO result FROM public.stripe_info si WHERE si.customer_id = org_customer_id LIMIT 1;
RETURN COALESCE(result, false);
END;
$$;
🤖 Prompt for AI Agents
In `@supabase/migrations/20260127121000_allow_credits_without_plan.sql` around
lines 271 - 295, The two functions disagree on how trial expiration is compared;
unify them by using the same timestamp-granular comparison (trial_at > now()) in
both functions. Update the trial check in is_paying_and_good_plan_org (replace
the trial_at::date - (now())::date > 0 expression) so it matches the check used
in is_paying_and_good_plan_org_action (si.trial_at > now()), ensuring both
functions use si.trial_at > now() for consistency.

riderx added 11 commits February 6, 2026 04:42
Reduce edge runtime CPU usage in CI by short-circuiting org member notification code when Bento env vars are placeholders or missing, and ensure audit logs tests add org membership.
CI uses a placeholder STRIPE_SECRET_KEY (e.g. 'test'); cron_stat_org must still compute org plan fit/exceeded flags and update stripe_info.is_good_plan without performing Stripe API calls. Remove the unnecessary Stripe-key gate from checkPlanStatusOnly.
Plan usage/fit and is_good_plan checks are Postgres RPCs and should still run when STRIPE_SECRET_KEY is set to placeholders like 'test' in CI. Only skip the RPCs when STRIPE_SECRET_KEY is truly unset.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Feb 6, 2026

@riderx riderx merged commit 57a4117 into main Feb 6, 2026
11 checks passed
@riderx riderx deleted the riderx/allow-credit-no-plan branch February 6, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants