Skip to content

feat(billing): add signupGrant + oneShot config fields to Free plan#3661

Merged
PierreBrisorgueil merged 6 commits into
masterfrom
feat-billing-plan-signup-grant
May 11, 2026
Merged

feat(billing): add signupGrant + oneShot config fields to Free plan#3661
PierreBrisorgueil merged 6 commits into
masterfrom
feat-billing-plan-signup-grant

Conversation

@PierreBrisorgueil
Copy link
Copy Markdown
Contributor

Summary

Implements Task 1 of docs/superpowers/plans/2026-05-11-n2-free-tier-grant.md — the N2 "Free Tier One-Shot Grant" workstream's first PR.

  • Adds optional signupGrant: z.number().int().positive() + oneShot: z.boolean() to the planDefinitions Zod schema (billing.config.zod.js)
  • Sets Free plan : signupGrant: 500, oneShot: true, meterQuota: 0
  • Co-presence guard : either both or neither (runtime + schema validation); guard returns plan without partial fields on violation
  • Plan service exposes getSignupGrant(planId) helper
  • Tests : 203 new lines covering Free has fields, Growth/Pro do not, schema rejects malformed values, co-presence enforced

Why

N2 roadmap : every fresh signup credits 500 compute in BillingExtraBalance, no renewal, no card. This PR adds the config contract. Subsequent PRs in the workstream :

  • T2 : creditGrant() repository method (idempotent)
  • T3 : signup hook in organizations.service.js calls creditGrant
  • T4-7 : trawl config + UI + migration + propagation

Test plan

  • Unit tests pass : billing.config.planDefinitions.unit.tests.js (141 lines) + billing.plan.service.unit.tests.js (+103 lines)
  • Zod rejects negative / non-integer / lone-field config
  • Co-presence guard: lone fields are NOT propagated to returned plan object
  • getSignupGrant('free') returns 500; getSignupGrant('pro') returns undefined
  • Backward compat : plans without signupGrant (Growth, Pro) load cleanly

Roadmap

[[Trawl — Product — Roadmap]] N2 (active 2026-05-11)

Adds N2 one-shot signup grant fields to BillingPlan config (Task 1/7).
- billing.development.config.js: Free plan signupGrant:500, oneShot:true
- billing.config.zod.js (new): Zod schema for planDefinitions entries
- billing.plan.service.js: getPlanFromConfig passes through new fields
- Tests: planDefinitions unit tests + plan.service signupGrant passthrough
- test: use actual devkit planIds (starter/pro/enterprise) so paid-plan
  signupGrant absence test cannot pass vacuously on missing plan IDs
- zod: ratios default(() => ({})) to prevent shared-reference mutation
…efine

- signupGrant uses .positive() (zero grant is a no-op, disallow it)
- .refine() enforces signupGrant + oneShot must be defined together
- tests: add zero/orphan-oneShot/orphan-signupGrant rejection cases
…an service

Addresses Phase 0 gate [high]: Zod schema refine() was test-only.
Add logger.warn in getPlanFromConfig when signupGrant/oneShot are not
both defined, catching misconfigured planDefinitions at runtime.
Tests cover both asymmetric cases + the symmetric OK paths.
Phase 0 gate [critical]: export getSignupGrant(planId) helper from billing.plan.service.js
Phase 0 gate [medium]:   co-presence guard returns plan without partial fields on mismatch
Phase 0 gate [low]:      rename hasGrant/hasOneShot → hasSignupGrant/hasOneShotFlag for clarity
Add tests: getSignupGrant returns 500 for free, undefined for pro/unknown;
           co-presence guard early-return drops lone signupGrant or oneShot
Copilot AI review requested due to automatic review settings May 11, 2026 18:10
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 822172e5-296d-4fb2-a5aa-4d382003e418

📥 Commits

Reviewing files that changed from the base of the PR and between 25db1be and 2fbbfd2.

📒 Files selected for processing (6)
  • modules/billing/config/billing.config.zod.js
  • modules/billing/config/billing.development.config.js
  • modules/billing/services/billing.plan.service.js
  • modules/billing/tests/billing.config.planDefinitions.unit.tests.js
  • modules/billing/tests/billing.plan.service.unit.tests.js
  • modules/billing/tests/billing.quota.unit.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-billing-plan-signup-grant

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.21%. Comparing base (25db1be) to head (2fbbfd2).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3661      +/-   ##
==========================================
+ Coverage   89.20%   89.21%   +0.01%     
==========================================
  Files         136      137       +1     
  Lines        4669     4683      +14     
  Branches     1452     1455       +3     
==========================================
+ Hits         4165     4178      +13     
- Misses        392      393       +1     
  Partials      112      112              
Flag Coverage Δ
integration 59.57% <53.33%> (-0.03%) ⬇️
unit 64.44% <93.33%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 25db1be...2fbbfd2. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… load

billing.plan.service.js now imports logger at module load time.
Logger reads config.log.fileLogger on init — absent from the quota
test's minimal billing-only config fixture → TypeError on all 40 tests.

Fix: add jest.unstable_mockModule for billing.plan.service.js in the
first describe block (which never needed the real service anyway).
The second describe block already mocked it at line 295.
Copy link
Copy Markdown

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

Adds configuration support for an N2 “Free Tier One‑Shot Grant” by extending billing plan definitions with optional signupGrant and oneShot fields, exposing a helper in BillingPlanService, and adding unit coverage for the new config contract and runtime guardrails.

Changes:

  • Extend BillingPlanService plan resolution to pass through signupGrant/oneShot only when both are present, and add getSignupGrant(planId).
  • Update the devkit Free plan defaults to include signupGrant: 500 and oneShot: true.
  • Add Zod schema + unit tests to validate field types and the co-presence invariant.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
modules/billing/services/billing.plan.service.js Adds runtime co-presence guard for signupGrant/oneShot and exposes getSignupGrant() helper.
modules/billing/config/billing.development.config.js Updates dev defaults to include Free plan grant fields and documents them.
modules/billing/config/billing.config.zod.js Introduces a Zod schema for validating individual planDefinitions entries (including co-presence rule).
modules/billing/tests/billing.plan.service.unit.tests.js Expands unit coverage for passthrough behavior, warnings, and getSignupGrant().
modules/billing/tests/billing.config.planDefinitions.unit.tests.js Adds unit tests for default config expectations and Zod validation of the new fields.

Comment on lines +12 to +16
beforeEach(async () => {
jest.resetModules();
const mod = await import('../../../config/index.js');
config = mod.default;
});
Comment on lines +23 to +38
it('Free plan has signupGrant: 500 and oneShot: true', () => {
const definitions = config?.billing?.planDefinitions ?? [];
const free = definitions.find((p) => p.planId === 'free');
expect(free).toBeDefined();
expect(free.signupGrant).toBe(500);
expect(free.oneShot).toBe(true);
expect(free.meterQuota).toBe(0);
});

it('Paid plans (starter, pro, enterprise) do not have signupGrant', () => {
const definitions = config?.billing?.planDefinitions ?? [];
const paidPlanIds = ['starter', 'pro', 'enterprise'];
for (const planId of paidPlanIds) {
const plan = definitions.find((p) => p.planId === planId);
// Assert the plan exists so this test cannot pass vacuously if IDs change
expect(plan).toBeDefined();
@PierreBrisorgueil PierreBrisorgueil merged commit 5a113f9 into master May 11, 2026
7 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the feat-billing-plan-signup-grant branch May 11, 2026 18:29
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