Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest-mongodb-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
dbName: 'hawk',
},
binary: {
version: '4.2.13',
version: '6.0.2',
skipMD5: true,
},
autoStart: false,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.2.3",
"version": "1.2.4",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down
28 changes: 18 additions & 10 deletions src/billing/cloudpayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
return router;
}

/**

Check warning on line 91 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Generates invoice id for payment
*
* @param tariffPlan - tariff plan to generate invoice id
Expand Down Expand Up @@ -205,7 +205,7 @@
telegram.sendMessage(`✅ [Billing / Check] All checks passed successfully «${workspace.name}»`, TelegramBotURLs.Money)
.catch(e => console.error('Error while sending message to Telegram: ' + e));

HawkCatcher.send(new Error('[Billing / Check] All checks passed successfully'), body as any);

Check warning on line 208 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

res.json({
code: CheckCodes.SUCCESS,
Expand Down Expand Up @@ -272,7 +272,10 @@

try {
await businessOperation.setStatus(BusinessOperationStatus.Confirmed);
await workspace.changePlan(tariffPlan._id);

if (!data.isCardLinkOperation) {
await workspace.changePlan(tariffPlan._id);
}

const subscriptionId = body.SubscriptionId;

Expand Down Expand Up @@ -339,17 +342,22 @@
* }
*/

try {
await publish('cron-tasks', 'cron-tasks/limiter', JSON.stringify({
type: 'unblock-workspace',
workspaceId: data.workspaceId,
}));
} catch (e) {
const error = e as Error;
/**
* If it is not a card linking operation then unblock workspace
*/
if (!data.isCardLinkOperation) {
try {
await publish('cron-tasks', 'cron-tasks/limiter', JSON.stringify({
type: 'unblock-workspace',
workspaceId: data.workspaceId,
}));
} catch (e) {
const error = e as Error;

this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending task to limiter worker ${error.toString()}`, body);
this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending task to limiter worker ${error.toString()}`, body);

return;
return;
}
}

try {
Expand Down Expand Up @@ -547,7 +555,7 @@

this.handleSendingToTelegramError(telegram.sendMessage(`❌ [Billing / Fail] Transaction failed for «${workspace.name}»`, TelegramBotURLs.Money));

HawkCatcher.send(new Error('[Billing / Fail] Transaction failed'), body as any);

Check warning on line 558 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

res.json({
code: FailCodes.SUCCESS,
Expand Down Expand Up @@ -729,7 +737,7 @@
* @param errorText - error description
* @param backtrace - request data and error data
*/
private sendError(res: express.Response, errorCode: CheckCodes | PayCodes | FailCodes | RecurrentCodes, errorText: string, backtrace: { [key: string]: any }): void {

Check warning on line 740 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type
res.json({
code: errorCode,
});
Expand Down Expand Up @@ -794,7 +802,7 @@
promise.catch(e => console.error('Error while sending message to Telegram: ' + e));
}

/**

Check warning on line 805 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Parses body and returns card data
* @param request - request body to parse
*/
Expand Down
17 changes: 16 additions & 1 deletion src/resolvers/billingNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,22 @@
const now = new Date();
const invoiceId = `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${plan.name}`;

const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired();
let isCardLinkOperation = false;


Check failure on line 114 in src/resolvers/billingNew.ts

View workflow job for this annotation

GitHub Actions / ESlint

More than 1 blank line not allowed
/**
* We need to only link card and not pay for the whole plan in case
* 1. We are paying for the same plan and
* 2. Plan is not expired and
* 3. Workspace is not blocked
*/
if (
workspace.tariffPlanId.toString() === tariffPlanId && // 1
!workspace.isTariffPlanExpired() && // 2
!workspace.isBlocked // 3
) {
isCardLinkOperation = true;
}

// Calculate next payment date
const lastChargeDate = workspace.lastChargeDate ? new Date(workspace.lastChargeDate) : now;
Expand Down
42 changes: 42 additions & 0 deletions test/integration/cases/billing/pay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,26 @@ describe('Pay webhook', () => {
expect(updatedWorkspace?.billingPeriodEventsCount).toBe(0);
});

test('Should not reset events counter in workspace if it is a card linking operation', async () => {
const apiResponse = await apiInstance.post('/billing/pay', {
...validPayRequestData,
Data: JSON.stringify({
checksum: await checksumService.generateChecksum({
...paymentSuccessPayload,
isCardLinkOperation: true,
nextPaymentDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toString(), // next day
}),
}),
});

const notUpdatedWorkspace = await workspacesCollection.findOne({
_id: workspace._id,
});

expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
expect(notUpdatedWorkspace?.billingPeriodEventsCount).not.toBe(0);
});

test('Should reset last charge date in workspace', async () => {
const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData);

Expand Down Expand Up @@ -375,6 +395,26 @@ describe('Pay webhook', () => {
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
});

test('Should not send task to limiter worker if it is a card linking operation', async () => {
const apiResponse = await apiInstance.post('/billing/pay', {
...validPayRequestData,
Data: JSON.stringify({
checksum: await checksumService.generateChecksum({
...paymentSuccessPayload,
isCardLinkOperation: true,
nextPaymentDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toString(), // next day
}),
}),
});

const message = await global.rabbitChannel.get('cron-tasks/limiter', {
noAck: true,
});

expect(message).toBeFalsy();
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
});

// test('Should associate an account with a workspace if the workspace did not have one', async () => {
// /**
// * Remove accountId from existed workspace
Expand Down Expand Up @@ -479,6 +519,8 @@ describe('Pay webhook', () => {
expect(updatedUser?.bankCards?.shift()).toMatchObject(expectedCard);
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
});


});

describe('With invalid request', () => {
Expand Down
208 changes: 208 additions & 0 deletions test/resolvers/billingNew.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import '../../src/env-test';
import { ObjectId } from 'mongodb';
import { PlanDBScheme, WorkspaceDBScheme } from '@hawk.so/types';
import billingNewResolver from '../../src/resolvers/billingNew';
import { ResolverContextWithUser } from '../../src/types/graphql';

// Set environment variables for test
process.env.JWT_SECRET_BILLING_CHECKSUM = 'checksum_secret';
process.env.JWT_SECRET_ACCESS_TOKEN = 'belarus';
process.env.JWT_SECRET_REFRESH_TOKEN = 'abacaba';
process.env.JWT_SECRET_PROJECT_TOKEN = 'qwerty';

/**
* Creates test data and mocks for composePayment tests
*/
function createComposePaymentTestSetup(options: {
isTariffPlanExpired?: boolean;
isBlocked?: boolean;
lastChargeDate?: Date;
planMonthlyCharge?: number;
planCurrency?: string;
}) {
const {
isTariffPlanExpired = false,
isBlocked = false,
lastChargeDate = new Date(),
planMonthlyCharge = 1000,
planCurrency = 'RUB'
} = options;

const userId = new ObjectId().toString();
const workspaceId = new ObjectId().toString();
const planId = new ObjectId().toString();

const plan: PlanDBScheme = {
_id: new ObjectId(planId),
name: 'Test Plan',
monthlyCharge: planMonthlyCharge,
monthlyChargeCurrency: planCurrency,
eventsLimit: 1000,
isDefault: false,
isHidden: false,
};

const workspace: WorkspaceDBScheme = {
_id: new ObjectId(workspaceId),
name: 'Test Workspace',
accountId: 'test-account-id',
balance: 0,
billingPeriodEventsCount: 0,
isBlocked,
lastChargeDate,
tariffPlanId: new ObjectId(planId),
inviteHash: 'test-invite-hash',
subscriptionId: undefined,
};

// Mock workspaces factory
const mockWorkspacesFactory = {
findById: jest.fn().mockResolvedValue({
...workspace,
getMemberInfo: jest.fn().mockResolvedValue({ isAdmin: true }),
isTariffPlanExpired: jest.fn().mockReturnValue(isTariffPlanExpired),
isBlocked,
}),
};

// Mock plans factory
const mockPlansFactory = {
findById: jest.fn().mockResolvedValue(plan),
};

const mockContext: ResolverContextWithUser = {
user: {
id: userId,
accessTokenExpired: false,
},
factories: {
workspacesFactory: mockWorkspacesFactory as any,
plansFactory: mockPlansFactory as any,
usersFactory: {} as any,
projectsFactory: {} as any,
businessOperationsFactory: {} as any,
},
};

return {
userId,
workspaceId,
planId,
plan,
workspace,
mockContext,
mockWorkspacesFactory,
mockPlansFactory,
};
}

describe('GraphQLBillingNew', () => {
describe('composePayment', () => {
it('should return isCardLinkOperation = false in case of expired tariff plan', async () => {
// Create 2 months ago date
const expiredDate = new Date();
expiredDate.setMonth(expiredDate.getMonth() - 2);

const { mockContext, planId, workspaceId } = createComposePaymentTestSetup({
isTariffPlanExpired: true,
isBlocked: false,
lastChargeDate: expiredDate,
});

// Call composePayment resolver
const result = await billingNewResolver.Query.composePayment(
undefined,
{
input: {
workspaceId,
tariffPlanId: planId,
shouldSaveCard: false,
},
},
mockContext
);

expect(result.isCardLinkOperation).toBe(false);
expect(result.plan.monthlyCharge).toBe(1000);
expect(result.currency).toBe('RUB');

// Check that nextPaymentDate is one month from now
const oneMonthFromNow = new Date();

oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1);

const oneMonthFromNowStr = oneMonthFromNow.toISOString().split('T')[0];
const nextPaymentDateStr = result.nextPaymentDate.toISOString().split('T')[0];

expect(nextPaymentDateStr).toBe(oneMonthFromNowStr);
});

it('should return isCardLinkOperation = true in case of active tariff plan', async () => {
// Create 2 days ago date
const lastChargeDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);

const { mockContext, planId, workspaceId, workspace } = createComposePaymentTestSetup({
isTariffPlanExpired: false,
isBlocked: false,
lastChargeDate,
});

const result = await billingNewResolver.Query.composePayment(
undefined,
{
input: {
workspaceId,
tariffPlanId: planId,
shouldSaveCard: false,
},
},
mockContext
);

expect(result.isCardLinkOperation).toBe(true);
expect(result.plan.monthlyCharge).toBe(1000);
expect(result.currency).toBe('RUB');

const oneMonthFromLastChargeDate = new Date(workspace.lastChargeDate);
oneMonthFromLastChargeDate.setMonth(oneMonthFromLastChargeDate.getMonth() + 1);

const oneMonthFromLastChargeDateStr = oneMonthFromLastChargeDate.toISOString().split('T')[0];
const nextPaymentDateStr = result.nextPaymentDate.toISOString().split('T')[0];
expect(nextPaymentDateStr).toBe(oneMonthFromLastChargeDateStr);
});

it('should return isCardLinkOperation = false in case of blocked workspace', async () => {
const { mockContext, planId, workspaceId } = createComposePaymentTestSetup({
isTariffPlanExpired: false,
isBlocked: true,
lastChargeDate: new Date(),
});

const result = await billingNewResolver.Query.composePayment(
undefined,
{
input: {
workspaceId,
tariffPlanId: planId,
shouldSaveCard: false,
},
},
mockContext
);

expect(result.isCardLinkOperation).toBe(false);
expect(result.plan.monthlyCharge).toBe(1000);
expect(result.currency).toBe('RUB');

// Check that nextPaymentDate is one month from now
const oneMonthFromNow = new Date();

oneMonthFromNow.setMonth(oneMonthFromNow.getMonth() + 1);

const oneMonthFromNowStr = oneMonthFromNow.toISOString().split('T')[0];
const nextPaymentDateStr = result.nextPaymentDate.toISOString().split('T')[0];

expect(nextPaymentDateStr).toBe(oneMonthFromNowStr);
});
});
})
Loading