Skip to content

Commit 3f25337

Browse files
fix(billing): add getSignupGrant export + harden co-presence guard
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
1 parent 1718c8a commit 3f25337

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

modules/billing/services/billing.plan.service.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ const getPlanFromConfig = (planId) => {
3333
// N2 signup-grant fields — only present when configured (optional on the def).
3434
// Guard: signupGrant and oneShot must be defined together (co-presence invariant).
3535
// This mirrors the Zod schema refine() and catches misconfigured planDefinitions at runtime.
36-
const hasGrant = def.signupGrant !== undefined;
37-
const hasOneShot = def.oneShot !== undefined;
38-
if (hasGrant !== hasOneShot) {
36+
const hasSignupGrant = def.signupGrant !== undefined;
37+
const hasOneShotFlag = def.oneShot !== undefined;
38+
if (hasSignupGrant !== hasOneShotFlag) {
3939
logger.warn(
4040
'[billing.plan] signupGrant and oneShot must be defined together — check planDefinitions config',
4141
{ planId: def.planId, signupGrant: def.signupGrant, oneShot: def.oneShot },
4242
);
43+
// Do not attach partial/broken grant fields — return plan without them.
44+
return plan;
4345
}
44-
if (hasGrant) plan.signupGrant = def.signupGrant;
45-
if (hasOneShot) plan.oneShot = def.oneShot;
46+
if (hasSignupGrant) plan.signupGrant = def.signupGrant;
47+
if (hasOneShotFlag) plan.oneShot = def.oneShot;
4648

4749
return plan;
4850
};
@@ -75,7 +77,22 @@ const getPlanByVersion = (planId, version) => {
7577
return plan;
7678
};
7779

80+
/**
81+
* @desc Get the signup grant amount for a given planId (N2 Free Tier Grant feature).
82+
* Returns the signupGrant value if the plan definition has it, or undefined.
83+
* Callers must handle undefined as "no grant configured for this plan".
84+
*
85+
* @param {string} planId - The logical plan identifier (e.g. "free").
86+
* @returns {number|undefined} The signupGrant value, or undefined.
87+
*/
88+
// biome-ignore lint/correctness/useQwikValidLexicalScope: false positive — Node.js service, not Qwik
89+
const getSignupGrant = (planId) => {
90+
const plan = getPlanFromConfig(planId);
91+
return plan?.signupGrant;
92+
};
93+
7894
export default {
7995
getActivePlan,
8096
getPlanByVersion,
97+
getSignupGrant,
8198
};

modules/billing/tests/billing.plan.service.unit.tests.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,47 @@ describe('BillingPlanService unit tests:', () => {
145145
BillingPlanService.getActivePlan('pro');
146146
expect(mockLogger.warn).not.toHaveBeenCalled();
147147
});
148+
149+
test('does not attach lone signupGrant when oneShot is missing (co-presence guard early return)', () => {
150+
mockConfig.billing.planDefinitions = [
151+
{ planId: 'free', meterQuota: 0, signupGrant: 500, ratios: {} },
152+
];
153+
const result = BillingPlanService.getActivePlan('free');
154+
expect(result.signupGrant).toBeUndefined();
155+
expect(result.oneShot).toBeUndefined();
156+
});
157+
158+
test('does not attach lone oneShot when signupGrant is missing (co-presence guard early return)', () => {
159+
mockConfig.billing.planDefinitions = [
160+
{ planId: 'free', meterQuota: 0, oneShot: true, ratios: {} },
161+
];
162+
const result = BillingPlanService.getActivePlan('free');
163+
expect(result.signupGrant).toBeUndefined();
164+
expect(result.oneShot).toBeUndefined();
165+
});
166+
});
167+
168+
describe('getSignupGrant', () => {
169+
test('returns signupGrant for a plan with both signupGrant and oneShot set', () => {
170+
mockConfig.billing.planDefinitions = [
171+
{ planId: 'free', meterQuota: 0, signupGrant: 500, oneShot: true, ratios: {} },
172+
];
173+
const grant = BillingPlanService.getSignupGrant('free');
174+
expect(grant).toBe(500);
175+
});
176+
177+
test('returns undefined for plans without signupGrant', () => {
178+
mockConfig.billing.planDefinitions = [
179+
{ planId: 'pro', meterQuota: 8000, ratios: {} },
180+
];
181+
const grant = BillingPlanService.getSignupGrant('pro');
182+
expect(grant).toBeUndefined();
183+
});
184+
185+
test('returns undefined for unknown planId', () => {
186+
const grant = BillingPlanService.getSignupGrant('nonexistent');
187+
expect(grant).toBeUndefined();
188+
});
148189
});
149190

150191
describe('getPlanByVersion', () => {

0 commit comments

Comments
 (0)