Skip to content

Commit 7f02035

Browse files
committed
Merge branch 'fix/card-linking' into stage
2 parents 62200f4 + 7237efa commit 7f02035

File tree

8 files changed

+369
-26
lines changed

8 files changed

+369
-26
lines changed

.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ JWT_SECRET_REFRESH_TOKEN=abacaba
2020
# JWT secret for user's access token
2121
JWT_SECRET_ACCESS_TOKEN=belarus
2222

23+
# max document could for one read request to the database
24+
MAX_DB_READ_BATCH_SIZE=100000
2325

2426
# JWT secret for signing tokens for processing billing requests
2527
JWT_SECRET_BILLING_CHECKSUM=checksum_secret

jest-mongodb-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module.exports = {
55
dbName: 'hawk',
66
},
77
binary: {
8-
version: '4.2.13',
8+
version: '6.0.2',
99
skipMD5: true,
1010
},
1111
autoStart: false,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.0",
3+
"version": "1.2.4",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/billing/cloudpayments.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,10 @@ export default class CloudPaymentsWebhooks {
272272

273273
try {
274274
await businessOperation.setStatus(BusinessOperationStatus.Confirmed);
275-
await workspace.changePlan(tariffPlan._id);
275+
276+
if (!data.isCardLinkOperation) {
277+
await workspace.changePlan(tariffPlan._id);
278+
}
276279

277280
const subscriptionId = body.SubscriptionId;
278281

@@ -350,14 +353,13 @@ export default class CloudPaymentsWebhooks {
350353
}));
351354
} catch (e) {
352355
const error = e as Error;
353-
356+
354357
this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending task to limiter worker ${error.toString()}`, body);
355-
358+
356359
return;
357360
}
358361
}
359362

360-
361363
try {
362364
// todo: add plan-prolongation notification if it was a payment by subscription
363365
const senderWorkerTask: PaymentSuccessNotificationTask = {

src/models/eventsFactory.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { getMidnightWithTimezoneOffset, getUTCMidnight } from '../utils/dates';
2-
import { groupBy } from '../utils/grouper';
32
import safe from 'safe-regex';
43

54
const Factory = require('./modelFactory');
@@ -8,6 +7,8 @@ const Event = require('../models/event');
87
const { ObjectID } = require('mongodb');
98
const { composeEventPayloadByRepetition } = require('../utils/merge');
109

10+
const MAX_DB_READ_BATCH_SIZE = Number(process.env.MAX_DB_READ_BATCH_SIZE);
11+
1112
/**
1213
* @typedef {import('mongodb').UpdateWriteOpResult} UpdateWriteOpResult
1314
*/
@@ -423,24 +424,35 @@ class EventsFactory extends Factory {
423424
};
424425
}
425426

426-
let dailyEvents = await this.getCollection(this.TYPES.DAILY_EVENTS)
427-
.find(options)
428-
.toArray();
427+
const dailyEventsCursor = await this.getCollection(this.TYPES.DAILY_EVENTS)
428+
.find(options, {
429+
projection: {
430+
lastRepetitionTime: 1,
431+
groupingTimestamp: 1,
432+
count: 1,
433+
},
434+
})
435+
.batchSize(MAX_DB_READ_BATCH_SIZE);
429436

430-
/**
431-
* Convert UTC midnight to midnights in user's timezone
432-
*/
433-
dailyEvents = dailyEvents.map((item) => {
434-
return Object.assign({}, item, {
435-
groupingTimestamp: getMidnightWithTimezoneOffset(item.lastRepetitionTime, item.groupingTimestamp, timezoneOffset),
436-
});
437-
});
437+
const groupedCounts = {};
438438

439-
/**
440-
* Group events using 'groupingTimestamp:NNNNNNNN' key
441-
* @type {ProjectChartItem[]}
442-
*/
443-
const groupedData = groupBy('groupingTimestamp')(dailyEvents);
439+
for await (const item of dailyEventsCursor) {
440+
const groupingTimestamp = getMidnightWithTimezoneOffset(
441+
item.lastRepetitionTime,
442+
item.groupingTimestamp,
443+
timezoneOffset
444+
);
445+
446+
const key = `groupingTimestamp:${groupingTimestamp}`;
447+
const current = groupedCounts[key] || 0;
448+
449+
if (item.count === undefined || item.count === null) {
450+
console.warn(`Missing 'count' field for daily event with key ${key}. Defaulting to 0.`);
451+
groupedCounts[key] = current;
452+
} else {
453+
groupedCounts[key] = current + item.count;
454+
}
455+
}
444456

445457
/**
446458
* Now fill all requested days
@@ -451,11 +463,16 @@ class EventsFactory extends Factory {
451463
const now = new Date();
452464
const day = new Date(now.setDate(now.getDate() - i));
453465
const dayMidnight = getUTCMidnight(day) / 1000;
454-
const groupedEvents = groupedData[`groupingTimestamp:${dayMidnight}`];
466+
467+
let groupedCount = groupedCounts[`groupingTimestamp:${dayMidnight}`];
468+
469+
if (!groupedCount) {
470+
groupedCount = 0;
471+
}
455472

456473
result.push({
457474
timestamp: dayMidnight,
458-
count: groupedEvents ? groupedEvents.reduce((sum, value) => sum + value.count, 0) : 0,
475+
count: groupedCount,
459476
});
460477
}
461478

src/resolvers/billingNew.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ export default {
109109
const now = new Date();
110110
const invoiceId = `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${plan.name}`;
111111

112-
const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired();
112+
let isCardLinkOperation = false;
113+
114+
if (workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired() && !workspace.isBlocked) {
115+
isCardLinkOperation = true;
116+
}
113117

114118
// Calculate next payment date
115119
const lastChargeDate = workspace.lastChargeDate ? new Date(workspace.lastChargeDate) : now;

test/integration/cases/billing/pay.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,26 @@ describe('Pay webhook', () => {
337337
expect(updatedWorkspace?.billingPeriodEventsCount).toBe(0);
338338
});
339339

340+
test('Should not reset events counter in workspace if it is a card linking operation', async () => {
341+
const apiResponse = await apiInstance.post('/billing/pay', {
342+
...validPayRequestData,
343+
Data: JSON.stringify({
344+
checksum: await checksumService.generateChecksum({
345+
...paymentSuccessPayload,
346+
isCardLinkOperation: true,
347+
nextPaymentDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toString(), // next day
348+
}),
349+
}),
350+
});
351+
352+
const notUpdatedWorkspace = await workspacesCollection.findOne({
353+
_id: workspace._id,
354+
});
355+
356+
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
357+
expect(notUpdatedWorkspace?.billingPeriodEventsCount).not.toBe(0);
358+
});
359+
340360
test('Should reset last charge date in workspace', async () => {
341361
const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData);
342362

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

398+
test('Should not send task to limiter worker if it is a card linking operation', async () => {
399+
const apiResponse = await apiInstance.post('/billing/pay', {
400+
...validPayRequestData,
401+
Data: JSON.stringify({
402+
checksum: await checksumService.generateChecksum({
403+
...paymentSuccessPayload,
404+
isCardLinkOperation: true,
405+
nextPaymentDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toString(), // next day
406+
}),
407+
}),
408+
});
409+
410+
const message = await global.rabbitChannel.get('cron-tasks/limiter', {
411+
noAck: true,
412+
});
413+
414+
expect(message).toBeFalsy();
415+
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
416+
});
417+
378418
// test('Should associate an account with a workspace if the workspace did not have one', async () => {
379419
// /**
380420
// * Remove accountId from existed workspace
@@ -479,6 +519,8 @@ describe('Pay webhook', () => {
479519
expect(updatedUser?.bankCards?.shift()).toMatchObject(expectedCard);
480520
expect(apiResponse.data.code).toBe(PayCodes.SUCCESS);
481521
});
522+
523+
482524
});
483525

484526
describe('With invalid request', () => {

0 commit comments

Comments
 (0)