From 674f7a13ec0c7fc16b2651ba4263dae2239b7d3f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Thu, 9 Oct 2025 22:09:19 +0300 Subject: [PATCH 01/10] Add condition --- src/billing/cloudpayments.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts index 9d81b983..e65fbd1e 100644 --- a/src/billing/cloudpayments.ts +++ b/src/billing/cloudpayments.ts @@ -339,19 +339,25 @@ export default class CloudPaymentsWebhooks { * } */ - 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); - - return; + /** + * 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); + + return; + } } + try { // todo: add plan-prolongation notification if it was a payment by subscription const senderWorkerTask: PaymentSuccessNotificationTask = { From 9fb1d6667e53f66702c37eae5f55c29a0b3d1aaf Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:10:48 +0000 Subject: [PATCH 02/10] Bump version up to 1.1.43 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4b8a889..e0577e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.42", + "version": "1.1.43", "main": "index.ts", "license": "BUSL-1.1", "scripts": { From 57795b8196825ce807afc3d8b17181555932137f Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 19:04:45 +0300 Subject: [PATCH 03/10] Add test and condition --- src/billing/cloudpayments.ts | 5 ++- test/integration/cases/billing/pay.test.ts | 42 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts index e65fbd1e..de2328fd 100644 --- a/src/billing/cloudpayments.ts +++ b/src/billing/cloudpayments.ts @@ -272,7 +272,10 @@ export default class CloudPaymentsWebhooks { try { await businessOperation.setStatus(BusinessOperationStatus.Confirmed); - await workspace.changePlan(tariffPlan._id); + + if (!data.isCardLinkOperation) { + await workspace.changePlan(tariffPlan._id); + } const subscriptionId = body.SubscriptionId; diff --git a/test/integration/cases/billing/pay.test.ts b/test/integration/cases/billing/pay.test.ts index b61f61e0..2dae594f 100644 --- a/test/integration/cases/billing/pay.test.ts +++ b/test/integration/cases/billing/pay.test.ts @@ -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); @@ -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 @@ -479,6 +519,8 @@ describe('Pay webhook', () => { expect(updatedUser?.bankCards?.shift()).toMatchObject(expectedCard); expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); }); + + }); describe('With invalid request', () => { From bf7bd3821f943fbbbc59af2fdd0df2a7e04fac03 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 19:10:24 +0300 Subject: [PATCH 04/10] Add isBlocked check --- src/resolvers/billingNew.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/resolvers/billingNew.ts b/src/resolvers/billingNew.ts index c2aa1d2f..fd70162c 100644 --- a/src/resolvers/billingNew.ts +++ b/src/resolvers/billingNew.ts @@ -109,7 +109,11 @@ export default { 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; + + if (workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired() && !workspace.isBlocked) { + isCardLinkOperation = true; + } // Calculate next payment date const lastChargeDate = workspace.lastChargeDate ? new Date(workspace.lastChargeDate) : now; From 05fa6eea55523f056104c2b130f958e7683180ed Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 19:13:07 +0300 Subject: [PATCH 05/10] Lint --- src/billing/cloudpayments.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts index de2328fd..58090ffc 100644 --- a/src/billing/cloudpayments.ts +++ b/src/billing/cloudpayments.ts @@ -272,7 +272,7 @@ export default class CloudPaymentsWebhooks { try { await businessOperation.setStatus(BusinessOperationStatus.Confirmed); - + if (!data.isCardLinkOperation) { await workspace.changePlan(tariffPlan._id); } @@ -353,14 +353,13 @@ export default class CloudPaymentsWebhooks { })); } catch (e) { const error = e as Error; - + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending task to limiter worker ${error.toString()}`, body); - + return; } } - try { // todo: add plan-prolongation notification if it was a payment by subscription const senderWorkerTask: PaymentSuccessNotificationTask = { From 7237efa53e28285259b720858e3bde1fd7604736 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 20:02:36 +0300 Subject: [PATCH 06/10] Add tests --- jest-mongodb-config.js | 2 +- test/resolvers/billingNew.test.ts | 276 ++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 test/resolvers/billingNew.test.ts diff --git a/jest-mongodb-config.js b/jest-mongodb-config.js index 27e27beb..8a0508d4 100644 --- a/jest-mongodb-config.js +++ b/jest-mongodb-config.js @@ -5,7 +5,7 @@ module.exports = { dbName: 'hawk', }, binary: { - version: '4.2.13', + version: '6.0.2', skipMD5: true, }, autoStart: false, diff --git a/test/resolvers/billingNew.test.ts b/test/resolvers/billingNew.test.ts new file mode 100644 index 00000000..c2e8fd30 --- /dev/null +++ b/test/resolvers/billingNew.test.ts @@ -0,0 +1,276 @@ +import '../../src/env-test'; +import { ObjectId } from 'mongodb'; +import { PlanDBScheme, WorkspaceDBScheme, UserDBScheme } from '@hawk.so/types'; +import billingNewResolver from '../../src/resolvers/billingNew'; +import { ResolverContextWithUser } from '../../src/types/graphql'; + +// Мокаем telegram модуль +// jest.mock('../../src/utils/telegram', () => ({ +// sendMessage: jest.fn().mockResolvedValue(undefined), +// TelegramBotURLs: { +// Base: 'base', +// Money: 'money', +// }, +// })); + +// Устанавливаем переменные окружения для теста +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'; + +describe('GraphQLBillingNew', () => { + describe('composePayment', () => { + it('should return isCardLinkOperation = false in case of expired tariff plan', async () => { + 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: 1000, + monthlyChargeCurrency: 'RUB', + eventsLimit: 1000, + isDefault: false, + isHidden: false, + }; + + // Workspace with expired tariff plan + const expiredDate = new Date(); + expiredDate.setMonth(expiredDate.getMonth() - 2); + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(workspaceId), + name: 'Test Workspace', + accountId: 'test-account-id', + balance: 0, + billingPeriodEventsCount: 0, + isBlocked: false, + lastChargeDate: expiredDate, + 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(true), // План истек + isBlocked: false, + }), + }; + + // 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, + }, + }; + + // 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 () => { + 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: 1000, + monthlyChargeCurrency: 'RUB', + eventsLimit: 1000, + isDefault: false, + isHidden: false, + }; + + const lastChargeDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // Last charge date is 2 days ago + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(workspaceId), + name: 'Test Workspace', + accountId: 'test-account-id', + balance: 0, + billingPeriodEventsCount: 0, + isBlocked: false, + lastChargeDate, + tariffPlanId: new ObjectId(planId), + inviteHash: 'test-invite-hash', + subscriptionId: undefined, + }; + + const mockWorkspacesFactory = { + findById: jest.fn().mockResolvedValue({ + ...workspace, + getMemberInfo: jest.fn().mockResolvedValue({ isAdmin: true }), + isTariffPlanExpired: jest.fn().mockReturnValue(false), + }), + }; + + 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, + }, + }; + + 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(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 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: 1000, + monthlyChargeCurrency: 'RUB', + 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: true, + lastChargeDate: new Date(), + tariffPlanId: new ObjectId(planId), + inviteHash: 'test-invite-hash', + subscriptionId: undefined, + }; + + const mockWorkspacesFactory = { + findById: jest.fn().mockResolvedValue({ + ...workspace, + getMemberInfo: jest.fn().mockResolvedValue({ isAdmin: true }), + isTariffPlanExpired: jest.fn().mockReturnValue(false), + }), + }; + + + 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, + }, + }; + + 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); + }); + }); +}) From 4456afe9bb473bf534c23208f596f24e6eb3bcd4 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 20:31:53 +0300 Subject: [PATCH 07/10] Cleanup --- test/resolvers/billingNew.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/resolvers/billingNew.test.ts b/test/resolvers/billingNew.test.ts index c2e8fd30..ee4069ab 100644 --- a/test/resolvers/billingNew.test.ts +++ b/test/resolvers/billingNew.test.ts @@ -4,15 +4,6 @@ import { PlanDBScheme, WorkspaceDBScheme, UserDBScheme } from '@hawk.so/types'; import billingNewResolver from '../../src/resolvers/billingNew'; import { ResolverContextWithUser } from '../../src/types/graphql'; -// Мокаем telegram модуль -// jest.mock('../../src/utils/telegram', () => ({ -// sendMessage: jest.fn().mockResolvedValue(undefined), -// TelegramBotURLs: { -// Base: 'base', -// Money: 'money', -// }, -// })); - // Устанавливаем переменные окружения для теста process.env.JWT_SECRET_BILLING_CHECKSUM = 'checksum_secret'; process.env.JWT_SECRET_ACCESS_TOKEN = 'belarus'; From cd5098719d80dde6a3a48162b2b68d557c5a0f9c Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 21:03:13 +0300 Subject: [PATCH 08/10] Add comment and simplify test --- src/resolvers/billingNew.ts | 13 +- test/resolvers/billingNew.test.ts | 261 ++++++++++++------------------ 2 files changed, 113 insertions(+), 161 deletions(-) diff --git a/src/resolvers/billingNew.ts b/src/resolvers/billingNew.ts index fd70162c..784d86a3 100644 --- a/src/resolvers/billingNew.ts +++ b/src/resolvers/billingNew.ts @@ -111,7 +111,18 @@ export default { let isCardLinkOperation = false; - if (workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired() && !workspace.isBlocked) { + + /** + * 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; } diff --git a/test/resolvers/billingNew.test.ts b/test/resolvers/billingNew.test.ts index ee4069ab..d2fe8990 100644 --- a/test/resolvers/billingNew.test.ts +++ b/test/resolvers/billingNew.test.ts @@ -1,77 +1,113 @@ import '../../src/env-test'; import { ObjectId } from 'mongodb'; -import { PlanDBScheme, WorkspaceDBScheme, UserDBScheme } from '@hawk.so/types'; +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 () => { - 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: 1000, - monthlyChargeCurrency: 'RUB', - eventsLimit: 1000, - isDefault: false, - isHidden: false, - }; - - // Workspace with expired tariff plan + // Create 2 months ago date const expiredDate = new Date(); expiredDate.setMonth(expiredDate.getMonth() - 2); - const workspace: WorkspaceDBScheme = { - _id: new ObjectId(workspaceId), - name: 'Test Workspace', - accountId: 'test-account-id', - balance: 0, - billingPeriodEventsCount: 0, + const { mockContext, planId, workspaceId } = createComposePaymentTestSetup({ + isTariffPlanExpired: true, isBlocked: false, lastChargeDate: expiredDate, - 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(true), // План истек - isBlocked: false, - }), - }; - - // 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, - }, - }; + }); // Call composePayment resolver const result = await billingNewResolver.Query.composePayment( @@ -102,61 +138,14 @@ describe('GraphQLBillingNew', () => { }); it('should return isCardLinkOperation = true in case of active tariff plan', async () => { - const userId = new ObjectId().toString(); - const workspaceId = new ObjectId().toString(); - const planId = new ObjectId().toString(); - + // Create 2 days ago date + const lastChargeDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); - const plan: PlanDBScheme = { - _id: new ObjectId(planId), - name: 'Test Plan', - monthlyCharge: 1000, - monthlyChargeCurrency: 'RUB', - eventsLimit: 1000, - isDefault: false, - isHidden: false, - }; - - const lastChargeDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // Last charge date is 2 days ago - - const workspace: WorkspaceDBScheme = { - _id: new ObjectId(workspaceId), - name: 'Test Workspace', - accountId: 'test-account-id', - balance: 0, - billingPeriodEventsCount: 0, + const { mockContext, planId, workspaceId, workspace } = createComposePaymentTestSetup({ + isTariffPlanExpired: false, isBlocked: false, lastChargeDate, - tariffPlanId: new ObjectId(planId), - inviteHash: 'test-invite-hash', - subscriptionId: undefined, - }; - - const mockWorkspacesFactory = { - findById: jest.fn().mockResolvedValue({ - ...workspace, - getMemberInfo: jest.fn().mockResolvedValue({ isAdmin: true }), - isTariffPlanExpired: jest.fn().mockReturnValue(false), - }), - }; - - 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, - }, - }; + }); const result = await billingNewResolver.Query.composePayment( undefined, @@ -174,7 +163,7 @@ describe('GraphQLBillingNew', () => { expect(result.plan.monthlyCharge).toBe(1000); expect(result.currency).toBe('RUB'); - const oneMonthFromLastChargeDate = new Date(lastChargeDate); + const oneMonthFromLastChargeDate = new Date(workspace.lastChargeDate); oneMonthFromLastChargeDate.setMonth(oneMonthFromLastChargeDate.getMonth() + 1); const oneMonthFromLastChargeDateStr = oneMonthFromLastChargeDate.toISOString().split('T')[0]; @@ -183,59 +172,11 @@ describe('GraphQLBillingNew', () => { }); it('should return isCardLinkOperation = false in case of blocked workspace', async () => { - 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: 1000, - monthlyChargeCurrency: 'RUB', - eventsLimit: 1000, - isDefault: false, - isHidden: false, - }; - - const workspace: WorkspaceDBScheme = { - _id: new ObjectId(workspaceId), - name: 'Test Workspace', - accountId: 'test-account-id', - balance: 0, - billingPeriodEventsCount: 0, + const { mockContext, planId, workspaceId } = createComposePaymentTestSetup({ + isTariffPlanExpired: false, isBlocked: true, lastChargeDate: new Date(), - tariffPlanId: new ObjectId(planId), - inviteHash: 'test-invite-hash', - subscriptionId: undefined, - }; - - const mockWorkspacesFactory = { - findById: jest.fn().mockResolvedValue({ - ...workspace, - getMemberInfo: jest.fn().mockResolvedValue({ isAdmin: true }), - isTariffPlanExpired: jest.fn().mockReturnValue(false), - }), - }; - - - 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, - }, - }; + }); const result = await billingNewResolver.Query.composePayment( undefined, From 4c1e6f5f2fcbdbcbc363aac0510387af6f95c29e Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 23:24:33 +0300 Subject: [PATCH 09/10] Lint --- src/resolvers/billingNew.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/resolvers/billingNew.ts b/src/resolvers/billingNew.ts index 784d86a3..82fe2ab3 100644 --- a/src/resolvers/billingNew.ts +++ b/src/resolvers/billingNew.ts @@ -111,7 +111,6 @@ export default { let isCardLinkOperation = false; - /** * We need to only link card and not pay for the whole plan in case * 1. We are paying for the same plan and From ac04e0dc541c7228916394611f9507f66d8fda28 Mon Sep 17 00:00:00 2001 From: Tanya Fomina Date: Sun, 12 Oct 2025 23:30:16 +0300 Subject: [PATCH 10/10] Update test --- test/resolvers/billingNew.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/resolvers/billingNew.test.ts b/test/resolvers/billingNew.test.ts index d2fe8990..a42fafa3 100644 --- a/test/resolvers/billingNew.test.ts +++ b/test/resolvers/billingNew.test.ts @@ -123,8 +123,6 @@ describe('GraphQLBillingNew', () => { ); 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(); @@ -160,8 +158,6 @@ describe('GraphQLBillingNew', () => { ); 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); @@ -191,8 +187,6 @@ describe('GraphQLBillingNew', () => { ); 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();