Skip to content

Commit 158f2ef

Browse files
committed
refactor(billing): enhance promo billing test suite with improved fixture setup and utility functions for clarity
1 parent 5693467 commit 158f2ef

1 file changed

Lines changed: 184 additions & 157 deletions

File tree

test/integration/cases/billing/promo.test.ts

Lines changed: 184 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import checksumService from '../../../../src/utils/checksumService';
1717
import { mainRequest, transactionId } from '../../billingMocks';
1818
import type { Global } from '@jest/types';
1919

20-
declare var global: Global.Global;
20+
declare var global: Global.Global;
21+
22+
/** Plan price before promo (RUB) */
23+
const PLAN_MONTHLY_CHARGE = 1000;
24+
25+
/** Expected charge after 25% promo discount (RUB) */
26+
const PROMO_DISCOUNTED_CHARGE = 750;
2127

2228
describe('Promo billing webhooks', () => {
2329
let accountsDb: Db;
@@ -44,85 +50,101 @@ describe('Promo billing webhooks', () => {
4450
promoCodeUsagesCollection = accountsDb.collection<Omit<PromoCodeUsageDBScheme, '_id'>>('promoCodeUsages');
4551
});
4652

47-
beforeEach(async () => {
48-
const currentPlanId = (await plansCollection.insertOne({
49-
name: 'CurrentTestPlan',
50-
monthlyCharge: 10,
51-
monthlyChargeCurrency: 'RUB',
52-
eventsLimit: 1000,
53-
isDefault: false,
54-
})).insertedId;
55-
56-
const workspaceId = (await workspacesCollection.insertOne({
57-
name: 'PromoBillingTest',
58-
accountId: '123',
59-
tariffPlanId: currentPlanId,
60-
} as WorkspaceDBScheme)).insertedId;
61-
const workspaceResult = await workspacesCollection.findOne({ _id: workspaceId });
62-
63-
if (!workspaceResult) {
64-
throw new Error('Failed to create workspace');
65-
}
66-
67-
workspace = workspaceResult as WorkspaceDBScheme;
68-
69-
const adminId = (await usersCollection.insertOne({
70-
email: 'admin@promo-billing.test',
71-
})).insertedId;
72-
const adminResult = await usersCollection.findOne({ _id: adminId });
53+
afterEach(async () => {
54+
await accountsDb.dropDatabase();
55+
});
7356

74-
if (!adminResult) {
75-
throw new Error('Failed to create admin');
57+
/**
58+
* Insert a document and load the persisted record.
59+
* Throws if insert or read fails — keeps fixture setup explicit in tests.
60+
*/
61+
async function insertAndLoad<T extends { _id?: ObjectId }, R extends T>(
62+
collection: Collection<T>,
63+
document: Omit<T, '_id'>,
64+
errorMessage: string
65+
): Promise<R> {
66+
const insertedId = (await collection.insertOne(document as T)).insertedId;
67+
const result = await collection.findOne({ _id: insertedId } as Partial<T>);
68+
69+
if (!result) {
70+
throw new Error(errorMessage);
7671
}
7772

78-
admin = adminResult as UserDBScheme;
73+
return result as R;
74+
}
7975

80-
const planToChangeId = (await plansCollection.insertOne({
81-
name: 'PromoBasic',
82-
monthlyCharge: 1000,
76+
/**
77+
* Seed workspace, admin, target plan and promo code for promo billing tests.
78+
*
79+
* Workspace starts on a cheap current plan; payment targets `planToChange` (1000 RUB).
80+
* Promo `SAVE25` gives 25% off → expected first charge is 750 RUB.
81+
* Admin is added to workspace team so check/pay webhooks pass membership checks.
82+
*/
83+
async function seedPromoBillingFixtures(): Promise<void> {
84+
const currentPlanId = (await plansCollection.insertOne({
85+
name: 'CurrentTestPlan',
86+
monthlyCharge: 10,
8387
monthlyChargeCurrency: 'RUB',
84-
eventsLimit: 10000,
88+
eventsLimit: 1000,
8589
isDefault: false,
8690
})).insertedId;
87-
const planToChangeResult = await plansCollection.findOne({ _id: planToChangeId });
88-
89-
if (!planToChangeResult) {
90-
throw new Error('Failed to create planToChange');
91-
}
9291

93-
planToChange = planToChangeResult as PlanDBScheme;
94-
95-
const promoCodeId = (await promoCodesCollection.insertOne({
96-
value: 'SAVE25',
97-
benefit: {
98-
type: 'percent_discount',
99-
percent: 25,
92+
workspace = await insertAndLoad(
93+
workspacesCollection,
94+
{
95+
name: 'PromoBillingTest',
96+
accountId: '123',
97+
tariffPlanId: currentPlanId,
98+
} as WorkspaceDBScheme,
99+
'Failed to create workspace'
100+
);
101+
102+
admin = await insertAndLoad(
103+
usersCollection,
104+
{ email: 'admin@promo-billing.test' },
105+
'Failed to create admin'
106+
);
107+
108+
planToChange = await insertAndLoad(
109+
plansCollection,
110+
{
111+
name: 'PromoBasic',
112+
monthlyCharge: PLAN_MONTHLY_CHARGE,
113+
monthlyChargeCurrency: 'RUB',
114+
eventsLimit: 10000,
115+
isDefault: false,
100116
},
101-
createdAt: new Date(),
102-
updatedAt: new Date(),
103-
createdBy: admin._id.toString(),
104-
})).insertedId;
105-
const promoCodeResult = await promoCodesCollection.findOne({ _id: promoCodeId });
106-
107-
if (!promoCodeResult) {
108-
throw new Error('Failed to create promo code');
109-
}
110-
111-
promoCode = promoCodeResult as PromoCodeDBScheme;
117+
'Failed to create planToChange'
118+
);
119+
120+
promoCode = await insertAndLoad(
121+
promoCodesCollection,
122+
{
123+
value: 'SAVE25',
124+
benefit: {
125+
type: 'percent_discount',
126+
percent: 25,
127+
},
128+
createdAt: new Date(),
129+
updatedAt: new Date(),
130+
createdBy: admin._id.toString(),
131+
},
132+
'Failed to create promo code'
133+
);
112134

113135
const team = accountsDb.collection<Omit<ConfirmedMemberDBScheme, '_id'>>(`team:${workspace._id.toString()}`);
114136

115137
await team.insertOne({
116138
userId: admin._id,
117139
isAdmin: true,
118140
});
119-
});
120-
121-
afterEach(async () => {
122-
await accountsDb.dropDatabase();
123-
});
141+
}
124142

125-
async function buildPromoChecksum() {
143+
/**
144+
* Checksum from composePayment with promo id embedded.
145+
* CloudPayments check/pay handlers revalidate amount against this promo.
146+
*/
147+
async function buildPromoChecksum(): Promise<string> {
126148
return checksumService.generateChecksum({
127149
workspaceId: workspace._id.toString(),
128150
userId: admin._id.toString(),
@@ -135,112 +157,117 @@ describe('Promo billing webhooks', () => {
135157
});
136158
}
137159

138-
describe('/billing/check', () => {
139-
it('should accept discounted amount when promo is valid', async () => {
140-
const data: CheckRequest = {
141-
...mainRequest,
142-
Amount: '750',
143-
Currency: Currency.RUB,
144-
Data: JSON.stringify({
145-
checksum: await buildPromoChecksum(),
146-
cloudPayments: {
147-
recurrent: {
148-
interval: 'Month',
149-
period: 1,
150-
amount: 1000,
151-
},
160+
/**
161+
* Build /billing/check request with promo checksum and recurrent subscription metadata.
162+
*/
163+
async function buildPromoCheckRequest(chargeAmount: number): Promise<CheckRequest> {
164+
return {
165+
...mainRequest,
166+
Amount: chargeAmount.toString(),
167+
Currency: Currency.RUB,
168+
Data: JSON.stringify({
169+
checksum: await buildPromoChecksum(),
170+
cloudPayments: {
171+
recurrent: {
172+
interval: 'Month',
173+
period: 1,
174+
amount: PLAN_MONTHLY_CHARGE,
152175
},
153-
}),
154-
};
155-
156-
const apiResponse = await apiInstance.post('/billing/check', data);
157-
const createdBusinessOperation = await businessOperationsCollection.findOne({
158-
transactionId: transactionId.toString(),
159-
});
176+
},
177+
}),
178+
};
179+
}
160180

161-
expect(apiResponse.data.code).toBe(CheckCodes.SUCCESS);
162-
expect(createdBusinessOperation?.status).toBe(BusinessOperationStatus.Pending);
163-
});
181+
beforeEach(async () => {
182+
await seedPromoBillingFixtures();
183+
});
164184

165-
it('should reject full plan amount when promo expects discounted charge', async () => {
166-
const data: CheckRequest = {
167-
...mainRequest,
168-
Amount: '1000',
169-
Currency: Currency.RUB,
170-
Data: JSON.stringify({
171-
checksum: await buildPromoChecksum(),
172-
cloudPayments: {
173-
recurrent: {
174-
interval: 'Month',
175-
period: 1,
176-
amount: 1000,
177-
},
178-
},
179-
}),
180-
};
185+
describe('/billing/check', () => {
186+
describe('with promo code', () => {
187+
it('should accept discounted charge amount and create pending business operation', async () => {
188+
const apiResponse = await apiInstance.post('/billing/check', await buildPromoCheckRequest(PROMO_DISCOUNTED_CHARGE));
189+
const createdBusinessOperation = await businessOperationsCollection.findOne({
190+
transactionId: transactionId.toString(),
191+
});
192+
193+
expect(apiResponse.data.code).toBe(CheckCodes.SUCCESS);
194+
/**
195+
* /billing/check only validates payment and registers intent.
196+
* Business operation stays Pending until /billing/pay confirms the charge.
197+
*/
198+
expect(createdBusinessOperation?.status).toBe(BusinessOperationStatus.Pending);
199+
});
181200

182-
const apiResponse = await apiInstance.post('/billing/check', data);
201+
it('should return WRONG_AMOUNT when charge equals full plan price instead of promo discount', async () => {
202+
const apiResponse = await apiInstance.post('/billing/check', await buildPromoCheckRequest(PLAN_MONTHLY_CHARGE));
183203

184-
expect(apiResponse.data.code).toBe(CheckCodes.WRONG_AMOUNT);
204+
expect(apiResponse.data.code).toBe(CheckCodes.WRONG_AMOUNT);
205+
});
185206
});
186207
});
187208

188209
describe('/billing/pay', () => {
189-
let validPayRequestData: PayRequest;
190-
191-
beforeEach(async () => {
192-
await businessOperationsCollection.insertOne({
193-
transactionId: transactionId.toString(),
194-
type: BusinessOperationType.WorkspacePlanPurchase,
195-
status: BusinessOperationStatus.Pending,
196-
dtCreated: new Date(),
197-
payload: {
198-
workspaceId: workspace._id,
199-
amount: 75000,
200-
currency: Currency.RUB,
201-
userId: admin._id,
202-
tariffPlanId: planToChange._id,
203-
},
210+
describe('with promo code', () => {
211+
let validPayRequestData: PayRequest;
212+
213+
beforeEach(async () => {
214+
/**
215+
* /billing/pay expects check webhook to have already created a Pending operation
216+
* for the same transactionId — mirrors real CloudPayments two-step flow.
217+
*/
218+
await businessOperationsCollection.insertOne({
219+
transactionId: transactionId.toString(),
220+
type: BusinessOperationType.WorkspacePlanPurchase,
221+
status: BusinessOperationStatus.Pending,
222+
dtCreated: new Date(),
223+
payload: {
224+
workspaceId: workspace._id,
225+
amount: PROMO_DISCOUNTED_CHARGE * 100,
226+
currency: Currency.RUB,
227+
userId: admin._id,
228+
tariffPlanId: planToChange._id,
229+
},
230+
});
231+
232+
validPayRequestData = {
233+
Amount: PROMO_DISCOUNTED_CHARGE.toString(),
234+
CardExpDate: '06/25',
235+
CardFirstSix: '578946',
236+
CardLastFour: '5367',
237+
CardType: CardType.VISA,
238+
Currency: Currency.RUB,
239+
DateTime: new Date(),
240+
GatewayName: 'CodeX bank',
241+
OperationType: OperationType.PAYMENT,
242+
Status: OperationStatus.COMPLETED,
243+
TestMode: false,
244+
TransactionId: transactionId,
245+
Token: '123123',
246+
IssuerBankCountry: 'US',
247+
Data: JSON.stringify({
248+
checksum: await buildPromoChecksum(),
249+
}),
250+
};
204251
});
205252

206-
validPayRequestData = {
207-
Amount: '750',
208-
CardExpDate: '06/25',
209-
CardFirstSix: '578946',
210-
CardLastFour: '5367',
211-
CardType: CardType.VISA,
212-
Currency: Currency.RUB,
213-
DateTime: new Date(),
214-
GatewayName: 'CodeX bank',
215-
OperationType: OperationType.PAYMENT,
216-
Status: OperationStatus.COMPLETED,
217-
TestMode: false,
218-
TransactionId: transactionId,
219-
Token: '123123',
220-
IssuerBankCountry: 'US',
221-
Data: JSON.stringify({
222-
checksum: await buildPromoChecksum(),
223-
}),
224-
};
225-
});
253+
it('should change plan and record promo usage after successful payment', async () => {
254+
const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData);
255+
256+
const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id });
257+
const promoUsage = await promoCodeUsagesCollection.findOne({ promoCodeId: promoCode._id });
226258

227-
it('should change plan and record promo usage after successful payment', async () => {
228-
const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData);
229-
230-
const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id });
231-
const promoUsage = await promoCodeUsagesCollection.findOne({ promoCodeId: promoCode._id });
232-
233-
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
234-
expect(updatedWorkspace?.tariffPlanId.toString()).toBe(planToChange._id.toString());
235-
expect(promoUsage).toMatchObject({
236-
promoCodeId: promoCode._id,
237-
userId: admin._id.toString(),
238-
workspaceId: workspace._id,
239-
planId: planToChange._id,
240-
benefitType: 'percent_discount',
241-
originalAmount: 1000,
242-
finalAmount: 750,
243-
discountAmount: 250,
259+
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
260+
expect(updatedWorkspace?.tariffPlanId.toString()).toBe(planToChange._id.toString());
261+
expect(promoUsage).toMatchObject({
262+
promoCodeId: promoCode._id,
263+
userId: admin._id.toString(),
264+
workspaceId: workspace._id,
265+
planId: planToChange._id,
266+
benefitType: 'percent_discount',
267+
originalAmount: PLAN_MONTHLY_CHARGE,
268+
finalAmount: PROMO_DISCOUNTED_CHARGE,
269+
discountAmount: PLAN_MONTHLY_CHARGE - PROMO_DISCOUNTED_CHARGE,
270+
});
244271
});
245272
});
246273
});

0 commit comments

Comments
 (0)