Skip to content

feat(credits): enterprise-domain accounts get pro credits; PRO_CREDITS → 9,999#747

Merged
sweetmantech merged 2 commits into
mainfrom
feat/enterprise-pro-credits
Jul 3, 2026
Merged

feat(credits): enterprise-domain accounts get pro credits; PRO_CREDITS → 9,999#747
sweetmantech merged 2 commits into
mainfrom
feat/enterprise-pro-credits

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Part of recoupable/chat#1841 (item "Open — credits").

What

  1. Enterprise-domain accounts resolve pro in the credits system. getAccountSubscriptionState was Stripe-only, so enterprise customers without a Stripe subscription (e.g. all of Seeker Music) fell to the 333 free tier. New SRP helper lib/enterprise/isEnterpriseAccount.ts checks whether any of the account's emails (lib/supabase/account_emails/selectAccountEmails) has a domain (extractDomain) in ENTERPRISE_DOMAINS; the check runs in parallel with the two existing Stripe lookups.
  2. PRO_CREDITS raised from 1,000 to 9,999 in lib/credits/const.ts.

Design notes

  • activeSubscription stays Stripe-only. It feeds checkAndResetCredits's early-refill via current_period_start; an enterprise match returns isPro: true, activeSubscription: null, so enterprise-pro accounts refill only on the ≥1-month path. The AccountSubscriptionState interface shape is unchanged.
  • Coordination: the sibling chat PR must land back-to-back — chat duplicates the credits stack (its own PRO_CREDITS and pro resolution), and the two must agree or the sidebar total and the API total diverge.
  • Heads-up on hand-set balances: the monthly refill OVERWRITES the balance (remaining_credits := plan total, not an increment). Any account whose balance was hand-set above/below plan will be clobbered to 9,999 (pro) or 333 (free) at its next refill.

Verification

  • TDD: new tests written first and confirmed failing (missing module / PRO_CREDITS 1000 ≠ 9999 / enterprise case isPro: false), then implementation turned them green.
  • Full suite: 685 test files, 3,772 tests, all passing (pnpm test).
  • tsc --noEmit: 201 errors on baseline → 200 after (zero new; remainder are pre-existing in untouched test files).
  • pnpm lint clean; prettier check clean on all touched files.

New tests:

  • lib/enterprise/__tests__/isEnterpriseAccount.test.ts — domain match, case-insensitive, multi-email, non-match, no emails, null/malformed emails.
  • lib/credits/__tests__/getAccountSubscriptionState.test.ts — enterprise-no-Stripe → isPro: true + activeSubscription: null; enterprise + Stripe keeps activeSubscription Stripe-only; existing Stripe-only cases unchanged.
  • lib/credits/__tests__/const.test.ts — pins PRO_CREDITS = 9999, DEFAULT_CREDITS = 333.

🤖 Generated with Claude Code


Summary by cubic

Enterprise-domain accounts now resolve as pro in the credits system without Stripe. PRO_CREDITS increases to 9,999 so enterprise and pro users don’t worry about credits.

  • New Features

    • Added lib/enterprise/isEnterpriseAccount.ts to match account email domains against ENTERPRISE_DOMAINS (via extractDomain) and wired it into getAccountSubscriptionState alongside Stripe account/org checks.
    • activeSubscription stays Stripe-only; enterprise-only accounts get isPro: true with activeSubscription: null to avoid early refill via current_period_start.
  • Migration

    • Coordinate with the chat app to apply the same pro resolution so the UI and API totals stay in sync.
    • Monthly refill overwrites balances to plan totals; any hand-set balances will reset to 9,999 (pro) or 333 (free) at the next refill.

Written for commit 128a5b7. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Expanded pro access recognition for eligible enterprise email domains.
    • Increased monthly pro credit allocation.
  • Bug Fixes

    • Pro status now resolves more consistently across account, organization, and enterprise-based eligibility.
    • Credit limits now reflect the updated pro-tier allotment.

…9999

- Add lib/enterprise/isEnterpriseAccount.ts: resolves whether any of an
  account's emails has a domain in ENTERPRISE_DOMAINS (via
  selectAccountEmails + extractDomain).
- getAccountSubscriptionState now runs the enterprise check in parallel
  with the two Stripe lookups; isPro is true if any match.
  activeSubscription stays Stripe-only so checkAndResetCredits's
  early-refill (current_period_start) never fires for enterprise-only
  accounts — they refill on the ≥1-month path.
- PRO_CREDITS raised 1000 → 9999 (matches the chat-side change).

Part of recoupable/chat#1841.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 3, 2026 7:19pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a241fdb1-9b26-4b85-8a76-c4ec6683ae28

📥 Commits

Reviewing files that changed from the base of the PR and between 84a3a01 and 9b14ebe.

⛔ Files ignored due to path filters (3)
  • lib/credits/__tests__/const.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/credits/__tests__/getAccountSubscriptionState.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/enterprise/__tests__/isEnterpriseAccount.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • lib/credits/const.ts
  • lib/credits/getAccountSubscriptionState.ts
  • lib/enterprise/isEnterpriseAccount.ts

📝 Walkthrough

Walkthrough

This PR adds a new isEnterpriseAccount helper that checks whether an account's email domain matches configured enterprise domains, integrates it into getAccountSubscriptionState to expand isPro eligibility, and increases PRO_CREDITS from 1000 to 9999.

Changes

Enterprise pro eligibility

Layer / File(s) Summary
Enterprise domain check helper
lib/enterprise/isEnterpriseAccount.ts
New async function fetches account emails, extracts domains via extractDomain, and returns true if any domain matches ENTERPRISE_DOMAINS.
Subscription state integration and credit allotment
lib/credits/getAccountSubscriptionState.ts, lib/credits/const.ts
getAccountSubscriptionState now resolves isEnterpriseAccount alongside Stripe subscription checks and includes it in isPro, while activeSubscription still follows account-then-org Stripe precedence; PRO_CREDITS raised from 1000 to 9999 with expanded documentation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • recoupable/api#477: Both PRs implement enterprise email-domain–based pro eligibility using ENTERPRISE_DOMAINS.
  • recoupable/api#547: Overlaps on PRO_CREDITS usage for computing credit totals.
  • recoupable/api#549: Depends on getAccountSubscriptionState's isPro and PRO_CREDITS values for account credit seeding.

Poem

A domain check, so clean and neat, 🐰
Turns enterprise mail into a pro-tier treat,
Nine-nine-nine-nine credits now in view,
SOLID and single-sourced, tried and true,
Hop along, reviewer — the logic's tight and new!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed Changes stay small and focused: enterprise lookup is isolated, getAccountSubscriptionState is concise, names match files, and no obvious DRY/KISS/SRP issues.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enterprise-pro-credits

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files

Confidence score: 4/5

  • In lib/credits/getAccountSubscriptionState.ts, adding an unconditional enterprise-domain lookup on every credits read can increase latency and DB load on a hot path even when Stripe already confirms a pro account, which could degrade request performance under traffic. Gate isEnterpriseAccount behind the non-pro path (or otherwise avoid the extra query when Stripe is definitive) before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/credits/getAccountSubscriptionState.ts">

<violation number="1" location="lib/credits/getAccountSubscriptionState.ts:29">
P2: Credits reads now add an unconditional enterprise-domain DB lookup, even when Stripe already proves the account is pro. This increases per-request load/latency on the hot path; consider running `isEnterpriseAccount` only when both Stripe checks are inactive.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Credits as Credits System
    participant State as getAccountSubscriptionState
    participant StripeAcc as getActiveSubscriptionDetails
    participant StripeOrg as getOrgSubscription
    participant Enterprise as isEnterpriseAccount (NEW)
    participant DB as selectAccountEmails
    participant EmailUtil as extractDomain
    participant DomainSet as ENTERPRISE_DOMAINS

    Note over Credits,DomainSet: Runtime flow for resolving pro status<br/>PRO_CREDITS is now 9999 (constant)

    Credits->>State: getAccountSubscriptionState(accountId)

    Note over State: Parallel lookups (Promise.all)
    State->>StripeAcc: getActiveSubscriptionDetails(accountId)
    State->>StripeOrg: getOrgSubscription(accountId)
    State->>Enterprise: NEW: isEnterpriseAccount(accountId)

    Enterprise->>DB: selectAccountEmails({ accountIds })
    DB-->>Enterprise: rows[] (email + account_id)

    Note over Enterprise: Loop over rows
    Enterprise->>EmailUtil: extractDomain(row.email)
    EmailUtil-->>Enterprise: domain or null
    Enterprise->>DomainSet: check domain in ENTERPRISE_DOMAINS
    DomainSet-->>Enterprise: match boolean

    Enterprise-->>State: isEnterprise: true/false

    StripeAcc-->>State: accountSub (Stripe.Subscription | null)
    StripeOrg-->>State: orgSub (Stripe.Subscription | null)

    Note over State: Resolve isPro and activeSubscription

    alt Enterprise match, no Stripe subscription
        State-->>Credits: { isPro: true, activeSubscription: null }
    else Enterprise match + Stripe subscription (account or org)
        State-->>Credits: { isPro: true, activeSubscription: Stripe sub }
    else No enterprise, no Stripe subscription
        State-->>Credits: { isPro: false, activeSubscription: null }
    else No enterprise, Stripe subscription active
        State-->>Credits: { isPro: true, activeSubscription: Stripe sub }
    end

    Note over Credits: activeSubscription stays Stripe-only<br/>so enterprise-only accounts refill on ≥1‑month path
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const [accountSub, orgSub, isEnterprise] = await Promise.all([
getActiveSubscriptionDetails(accountId),
getOrgSubscription(accountId),
isEnterpriseAccount(accountId),

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: Credits reads now add an unconditional enterprise-domain DB lookup, even when Stripe already proves the account is pro. This increases per-request load/latency on the hot path; consider running isEnterpriseAccount only when both Stripe checks are inactive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/credits/getAccountSubscriptionState.ts, line 29:

<comment>Credits reads now add an unconditional enterprise-domain DB lookup, even when Stripe already proves the account is pro. This increases per-request load/latency on the hot path; consider running `isEnterpriseAccount` only when both Stripe checks are inactive.</comment>

<file context>
@@ -2,32 +2,36 @@ import type Stripe from "stripe";
+  const [accountSub, orgSub, isEnterprise] = await Promise.all([
     getActiveSubscriptionDetails(accountId),
     getOrgSubscription(accountId),
+    isEnterpriseAccount(accountId),
   ]);
   const hasAccountSub = isActiveSubscription(accountSub);
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — 2026-07-03

Preview https://api-l78g42hkx-recoup.vercel.app, sha-verified against PR head 128a5b75 (merge of latest main — clean, no conflicts; full suite re-run green on the merged tree: 3,873 tests, tsc 200 = baseline). Auth: Privy bearer. Test account: throwaway credits-test-20260703@seekermusic.com.

# Check (documented) Actual Result
1 Seeding path: new enterprise-domain account initializes at pro tier signup → {remaining: 9999, total: 9999, used: 0, is_pro: true}
2 Tier resolution: existing enterprise account with no Stripe sub resolves pro (evan@seekermusic.com, the account that motivated this PR) is_pro: true, total_credits: 9999 (was false/333 before this PR — live-confirmed earlier today post-#746)
3 Monthly lazy refill: stale enterprise row (backdated to 2026-05-01, remaining 100) refills on next access GET → remaining: 9999, timestamp bumped to now
4 Control: non-enterprise free account unaffected is_pro: false, total_credits: 333

Behavior notes (as designed, worth knowing):

  • No early refill on tier flip: evan's remaining_credits stays 333 until his ≥1-month reset (early refill is keyed to Stripe current_period_start only, which enterprise-pro accounts don't have). His used_credits reads 9666 in the meantime (computed total − remaining) — cosmetic, self-corrects at refill.
  • Refill overwrites: any hand-set balance resets to 9,999 at the account's next refill (flagged in the PR body — check internal accounts with boosted balances before relying on them).

Cleanup: throwaway's Seeker-org membership removed via DELETE /api/organizations/members (200); account + credits rows remain (inert, clearly named).

Must land back-to-back with recoupable/chat#1842 (chat's duplicated credits stack) per recoupable/chat#1841.

@sweetmantech sweetmantech merged commit 465aae4 into main Jul 3, 2026
6 checks passed
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.

1 participant