diff --git a/package.json b/package.json index 6db19b0b..8a2c557b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.4.12", + "version": "1.4.13", "main": "index.ts", "license": "BUSL-1.1", "scripts": { diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts index bcee09e0..5c4e7a4e 100644 --- a/src/billing/cloudpayments.ts +++ b/src/billing/cloudpayments.ts @@ -22,8 +22,7 @@ import { BusinessOperationType, ConfirmedMemberDBScheme, PayloadOfWorkspacePlanPurchase, - PlanDBScheme, - PlanProlongationPayload + PlanDBScheme } from '@hawk.so/types'; import WorkspaceModel from '../models/workspace'; import HawkCatcher from '@hawk.so/nodejs'; @@ -487,7 +486,7 @@ subscription id: ${body.SubscriptionId}`; */ private async fail(req: express.Request, res: express.Response): Promise { const body: FailRequest = req.body; - let data: PlanProlongationPayload; + let data: PaymentData; console.log('💎 CloudPayments /fail request', body); @@ -507,12 +506,29 @@ subscription id: ${body.SubscriptionId}`; * @todo handle card linking and update business operation status */ - if (!data.workspaceId || !data.userId || !data.tariffPlanId) { - this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No workspace or user id or plan id in request body`, body); + if (!data.workspaceId) { + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No workspace id in request body`, body); return; } + if (!data.userId) { + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No user id in request body`, body); + + return; + } + + /** + * In card linking mode tariff plan id is taken from workspace. + */ + if (!data.isCardLinkOperation) { + if (!data.tariffPlanId) { + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No tariffPlanId in request body`, body); + + return; + } + } + try { businessOperation = await this.getBusinessOperation(req, body.TransactionId.toString()); workspace = await this.getWorkspace(req, data.workspaceId); diff --git a/test/integration/cases/billing/fail.test.ts b/test/integration/cases/billing/fail.test.ts index be1fd4f2..3d4ace12 100644 --- a/test/integration/cases/billing/fail.test.ts +++ b/test/integration/cases/billing/fail.test.ts @@ -2,10 +2,11 @@ import { apiInstance } from '../../utils'; import { FailCodes, FailRequest } from '../../../../src/billing/types'; import { CardType, Currency, OperationType, ReasonCode, ReasonCodesTranscript } from '../../../../src/billing/types/enums'; import { Collection, ObjectId, Db } from 'mongodb'; -import { BusinessOperationDBScheme, BusinessOperationStatus, PlanDBScheme, BusinessOperationType, UserDBScheme, WorkspaceDBScheme, UserNotificationType, PlanProlongationPayload } from '@hawk.so/types'; +import { BusinessOperationDBScheme, BusinessOperationStatus, PlanDBScheme, BusinessOperationType, UserDBScheme, WorkspaceDBScheme, UserNotificationType } from '@hawk.so/types'; import { WorkerPaths } from '../../../../src/rabbitmq'; import { PaymentFailedNotificationTask, SenderWorkerTaskType } from '../../../../src/types/personalNotifications'; import checksumService from '../../../../src/utils/checksumService'; +import jwt, { Secret } from 'jsonwebtoken'; import type { Global } from '@jest/types'; declare var global: Global.Global; @@ -215,6 +216,35 @@ describe('Fail webhook', () => { expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); }); + + test('Should change business operation status to rejected for card linking payload without tariff plan id', async () => { + const apiResponse = await apiInstance.post('/billing/fail', { + ...validRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + isCardLinkOperation: true, + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + nextPaymentDate: new Date().toString(), + }), + cloudPayments: { + recurrent: { + interval: 'Month', + period: 1, + amount: 100, + startDate: new Date().toISOString(), + }, + }, + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Rejected); + }); }); describe('With invalid request', () => { @@ -252,5 +282,33 @@ describe('Fail webhook', () => { expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); }); + + test('Should not change business operation status for non-card-link payload without tariff plan id', async () => { + const invalidChecksum = jwt.sign({ + isCardLinkOperation: false, + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + shouldSaveCard: false, + nextPaymentDate: new Date().toString(), + }, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret, { expiresIn: '30m' }); + + const apiResponse = await apiInstance.post('/billing/fail', { + ...validRequest, + Data: JSON.stringify({ + checksum: invalidChecksum, + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, { + noAck: true, + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(message).toBeFalsy(); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); }); });