feat(billing): add signupGrant + oneShot config fields to Free plan#3661
Conversation
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
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
… 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.
There was a problem hiding this comment.
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
BillingPlanServiceplan resolution to pass throughsignupGrant/oneShotonly when both are present, and addgetSignupGrant(planId). - Update the devkit Free plan defaults to include
signupGrant: 500andoneShot: 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. |
| beforeEach(async () => { | ||
| jest.resetModules(); | ||
| const mod = await import('../../../config/index.js'); | ||
| config = mod.default; | ||
| }); |
| 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(); |
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.signupGrant: z.number().int().positive()+oneShot: z.boolean()to theplanDefinitionsZod schema (billing.config.zod.js)signupGrant: 500, oneShot: true, meterQuota: 0getSignupGrant(planId)helperWhy
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 :creditGrant()repository method (idempotent)organizations.service.jscallscreditGrantTest plan
billing.config.planDefinitions.unit.tests.js(141 lines) +billing.plan.service.unit.tests.js(+103 lines)getSignupGrant('free')returns 500;getSignupGrant('pro')returns undefinedsignupGrant(Growth, Pro) load cleanlyRoadmap
[[Trawl — Product — Roadmap]] N2 (active 2026-05-11)