Skip to content

Commit a0199b9

Browse files
committed
Handle card-link fail cases in CloudPayments
Update CloudPayments /fail handling to support card-linking flows: replace PlanProlongationPayload with PaymentData, validate workspaceId and userId separately, and allow missing tariffPlanId when isCardLinkOperation is true (tariff is taken from workspace). Adjust error responses accordingly and keep previous validations for non-card-link operations. Tests updated to cover both scenarios: add a test that ensures a card-linking fail without tariffPlanId marks the business operation as Rejected, and a test that a non-card-link payload without tariffPlanId does not change the operation (remains Pending). Also remove an unused import and add jwt import for checksum generation in tests.
1 parent afaf103 commit a0199b9

2 files changed

Lines changed: 82 additions & 6 deletions

File tree

src/billing/cloudpayments.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ import {
2222
BusinessOperationType,
2323
ConfirmedMemberDBScheme,
2424
PayloadOfWorkspacePlanPurchase,
25-
PlanDBScheme,
26-
PlanProlongationPayload
25+
PlanDBScheme
2726
} from '@hawk.so/types';
2827
import WorkspaceModel from '../models/workspace';
2928
import HawkCatcher from '@hawk.so/nodejs';
@@ -487,7 +486,7 @@ subscription id: ${body.SubscriptionId}`;
487486
*/
488487
private async fail(req: express.Request, res: express.Response): Promise<void> {
489488
const body: FailRequest = req.body;
490-
let data: PlanProlongationPayload;
489+
let data: PaymentData;
491490

492491
console.log('💎 CloudPayments /fail request', body);
493492

@@ -507,12 +506,29 @@ subscription id: ${body.SubscriptionId}`;
507506
* @todo handle card linking and update business operation status
508507
*/
509508

510-
if (!data.workspaceId || !data.userId || !data.tariffPlanId) {
511-
this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No workspace or user id or plan id in request body`, body);
509+
if (!data.workspaceId) {
510+
this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No workspace id in request body`, body);
512511

513512
return;
514513
}
515514

515+
if (!data.userId) {
516+
this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No user id in request body`, body);
517+
518+
return;
519+
}
520+
521+
/**
522+
* In card linking mode tariff plan id is taken from workspace.
523+
*/
524+
if (!data.isCardLinkOperation) {
525+
if (!data.tariffPlanId) {
526+
this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No plan id in request body`, body);
527+
528+
return;
529+
}
530+
}
531+
516532
try {
517533
businessOperation = await this.getBusinessOperation(req, body.TransactionId.toString());
518534
workspace = await this.getWorkspace(req, data.workspaceId);

test/integration/cases/billing/fail.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { apiInstance } from '../../utils';
22
import { FailCodes, FailRequest } from '../../../../src/billing/types';
33
import { CardType, Currency, OperationType, ReasonCode, ReasonCodesTranscript } from '../../../../src/billing/types/enums';
44
import { Collection, ObjectId, Db } from 'mongodb';
5-
import { BusinessOperationDBScheme, BusinessOperationStatus, PlanDBScheme, BusinessOperationType, UserDBScheme, WorkspaceDBScheme, UserNotificationType, PlanProlongationPayload } from '@hawk.so/types';
5+
import { BusinessOperationDBScheme, BusinessOperationStatus, PlanDBScheme, BusinessOperationType, UserDBScheme, WorkspaceDBScheme, UserNotificationType } from '@hawk.so/types';
66
import { WorkerPaths } from '../../../../src/rabbitmq';
77
import { PaymentFailedNotificationTask, SenderWorkerTaskType } from '../../../../src/types/personalNotifications';
88
import checksumService from '../../../../src/utils/checksumService';
9+
import jwt, { Secret } from 'jsonwebtoken';
910
import type { Global } from '@jest/types';
1011

1112
declare var global: Global.Global;
@@ -51,6 +52,7 @@ const tariffPlan: PlanDBScheme = {
5152
};
5253

5354
const planProlongationPayload = {
55+
isCardLinkOperation: false,
5456
userId: user._id.toString(),
5557
workspaceId: workspace._id.toString(),
5658
tariffPlanId: tariffPlan._id.toString(),
@@ -215,6 +217,35 @@ describe('Fail webhook', () => {
215217
expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask);
216218
expect(apiResponse.data.code).toBe(FailCodes.SUCCESS);
217219
});
220+
221+
test('Should change business operation status to rejected for card linking payload without tariff plan id', async () => {
222+
const apiResponse = await apiInstance.post('/billing/fail', {
223+
...validRequest,
224+
Data: JSON.stringify({
225+
checksum: await checksumService.generateChecksum({
226+
isCardLinkOperation: true,
227+
userId: user._id.toString(),
228+
workspaceId: workspace._id.toString(),
229+
nextPaymentDate: new Date().toString(),
230+
}),
231+
cloudPayments: {
232+
recurrent: {
233+
interval: 'Month',
234+
period: 1,
235+
amount: 100,
236+
startDate: new Date().toISOString(),
237+
},
238+
},
239+
}),
240+
});
241+
242+
const updatedBusinessOperation = await businessOperationsCollection.findOne({
243+
transactionId: transactionId.toString(),
244+
});
245+
246+
expect(apiResponse.data.code).toBe(FailCodes.SUCCESS);
247+
expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Rejected);
248+
});
218249
});
219250

220251
describe('With invalid request', () => {
@@ -236,6 +267,7 @@ describe('Fail webhook', () => {
236267
...validRequest,
237268
Data: JSON.stringify({
238269
checksum: await checksumService.generateChecksum({
270+
isCardLinkOperation: false,
239271
userId: '',
240272
workspaceId: workspace._id.toString(),
241273
tariffPlanId: tariffPlan._id.toString(),
@@ -252,5 +284,33 @@ describe('Fail webhook', () => {
252284
expect(apiResponse.data.code).toBe(FailCodes.SUCCESS);
253285
expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending);
254286
});
287+
288+
test('Should not change business operation status for non-card-link payload without tariff plan id', async () => {
289+
const invalidChecksum = jwt.sign({
290+
isCardLinkOperation: false,
291+
userId: user._id.toString(),
292+
workspaceId: workspace._id.toString(),
293+
shouldSaveCard: false,
294+
nextPaymentDate: new Date().toString(),
295+
}, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret, { expiresIn: '30m' });
296+
297+
const apiResponse = await apiInstance.post('/billing/fail', {
298+
...validRequest,
299+
Data: JSON.stringify({
300+
checksum: invalidChecksum,
301+
}),
302+
});
303+
304+
const updatedBusinessOperation = await businessOperationsCollection.findOne({
305+
transactionId: transactionId.toString(),
306+
});
307+
const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, {
308+
noAck: true,
309+
});
310+
311+
expect(apiResponse.data.code).toBe(FailCodes.SUCCESS);
312+
expect(message).toBeFalsy();
313+
expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending);
314+
});
255315
});
256316
});

0 commit comments

Comments
 (0)