Skip to content

Commit 988ae1b

Browse files
authored
Merge branch 'master' into update-notification-rule-types
2 parents a35446e + 9247ff5 commit 988ae1b

9 files changed

Lines changed: 209 additions & 30 deletions

File tree

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.1.7",
3+
"version": "1.1.10",
44
"main": "index.ts",
55
"license": "UNLICENSED",
66
"scripts": {

src/billing/cloudpayments.ts

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ import { PaymentData } from './types/paymentData';
4343
import cloudPaymentsApi from '../utils/cloudPaymentsApi';
4444
import PlanModel from '../models/plan';
4545
import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments';
46+
import { ComposePaymentPayload } from './types/composePaymentPayload';
4647

47-
/**
48-
* Custom data of the plan prolongation request
49-
*/
50-
type PlanProlongationData = PlanProlongationPayload & PaymentData;
48+
interface ComposePaymentRequest extends express.Request {
49+
query: ComposePaymentPayload & { [key: string]: any };
50+
context: import('../types/graphql').ResolverContextBase;
51+
};
5152

5253
/**
5354
* Class for describing the logic of payment routes
@@ -99,8 +100,8 @@ export default class CloudPaymentsWebhooks {
99100
* @param req — Express request object
100101
* @param res - Express response object
101102
*/
102-
private async composePayment(req: express.Request, res: express.Response): Promise<void> {
103-
const { workspaceId, tariffPlanId, shouldSaveCard } = req.query as Record<string, string>;
103+
private async composePayment(req: ComposePaymentRequest, res: express.Response): Promise<void> {
104+
const { workspaceId, tariffPlanId, shouldSaveCard } = req.query;
104105
const userId = req.context.user.id;
105106

106107
if (!workspaceId || !tariffPlanId || !userId) {
@@ -134,15 +135,23 @@ export default class CloudPaymentsWebhooks {
134135
}
135136
const invoiceId = this.generateInvoiceId(tariffPlan, workspace);
136137

138+
const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !this.isPlanExpired(workspace);
139+
137140
let checksum;
138141

139142
try {
140-
checksum = await checksumService.generateChecksum({
143+
const checksumData = isCardLinkOperation ? {
144+
isCardLinkOperation: true,
145+
workspaceId: workspace._id.toString(),
146+
userId: userId,
147+
} : {
141148
workspaceId: workspace._id.toString(),
142149
userId: userId,
143150
tariffPlanId: tariffPlan._id.toString(),
144151
shouldSaveCard: shouldSaveCard === 'true',
145-
});
152+
};
153+
154+
checksum = await checksumService.generateChecksum(checksumData);
146155
} catch (e) {
147156
const error = e as Error;
148157

@@ -158,11 +167,32 @@ export default class CloudPaymentsWebhooks {
158167
name: tariffPlan.name,
159168
monthlyCharge: tariffPlan.monthlyCharge,
160169
},
170+
isCardLinkOperation,
161171
currency: 'RUB',
162172
checksum,
163173
});
164174
}
165175

176+
/**
177+
* Returns true if workspace's plan is expired
178+
* @param workspace - workspace to check
179+
*/
180+
private isPlanExpired(workspace: WorkspaceModel): boolean {
181+
const lastChargeDate = new Date(workspace.lastChargeDate);
182+
183+
let planExpiracyDate;
184+
185+
if (workspace.isDebug) {
186+
planExpiracyDate = lastChargeDate.setDate(lastChargeDate.getDate() + 1);
187+
} else {
188+
planExpiracyDate = lastChargeDate.setMonth(lastChargeDate.getMonth() + 1);
189+
}
190+
191+
const isPlanExpired = planExpiracyDate < Date.now();
192+
193+
return isPlanExpired;
194+
}
195+
166196
/**
167197
* Generates invoice id for payment
168198
*
@@ -175,6 +205,36 @@ export default class CloudPaymentsWebhooks {
175205
return `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${tariffPlan.name}`;
176206
}
177207

208+
/**
209+
* Confirms the correctness of a user's payment for card linking
210+
* @param req - express request
211+
* @param res - express response
212+
* @param data - payment data receinved from checksum and request payload
213+
*/
214+
private async checkCardLinkOperation(req: express.Request, res: express.Response, data: PaymentData): Promise<void> {
215+
if (data.isCardLinkOperation && (!data.userId || !data.workspaceId)) {
216+
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] Card linking – invalid data', req.body);
217+
218+
return;
219+
}
220+
221+
try {
222+
const workspace = await this.getWorkspace(req, data.workspaceId);
223+
224+
telegram
225+
.sendMessage(`✅ [Billing / Check] Card linked for subscription workspace «${workspace.name}»`, TelegramBotURLs.Money)
226+
.catch(e => console.error('Error while sending message to Telegram: ' + e));
227+
228+
res.json({
229+
code: CheckCodes.SUCCESS,
230+
} as CheckResponse);
231+
} catch (e) {
232+
const error = e as Error;
233+
234+
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, `[Billing / Check] ${error.toString()}`, req.body);
235+
}
236+
}
237+
178238
/**
179239
* Route to confirm the correctness of a user's payment
180240
* https://developers.cloudpayments.ru/#check
@@ -197,11 +257,17 @@ export default class CloudPaymentsWebhooks {
197257
return;
198258
}
199259

260+
if (data.isCardLinkOperation) {
261+
this.checkCardLinkOperation(req, res, data);
262+
263+
return;
264+
}
265+
200266
let workspace: WorkspaceModel;
201267
let member: ConfirmedMemberDBScheme;
202268
let plan: PlanDBScheme;
203269

204-
if (!data.workspaceId || !data.tariffPlanId || !data.userId) {
270+
if (!data.userId || !data.workspaceId || !data.tariffPlanId) {
205271
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body);
206272

207273
return;
@@ -266,6 +332,7 @@ export default class CloudPaymentsWebhooks {
266332

267333
telegram.sendMessage(`✅ [Billing / Check] All checks passed successfully «${workspace.name}»`, TelegramBotURLs.Money)
268334
.catch(e => console.error('Error while sending message to Telegram: ' + e));
335+
269336
HawkCatcher.send(new Error('[Billing / Check] All checks passed successfully'), body as any);
270337

271338
res.json({
@@ -294,7 +361,13 @@ export default class CloudPaymentsWebhooks {
294361
return;
295362
}
296363

297-
if (!data.workspaceId || !data.tariffPlanId || !data.userId) {
364+
if (data.isCardLinkOperation && (!data.userId || !data.workspaceId)) {
365+
this.sendError(res, PayCodes.SUCCESS, '[Billing / Pay] No workspace or user id in request body', req.body);
366+
367+
return;
368+
}
369+
370+
if (!data.isCardLinkOperation && (!data.workspaceId || !data.tariffPlanId || !data.userId)) {
298371
this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] No workspace, tariff plan or user id in request body`, body);
299372

300373
return;
@@ -304,12 +377,15 @@ export default class CloudPaymentsWebhooks {
304377
let workspace;
305378
let tariffPlan;
306379
let user;
380+
let planId;
307381

308382
try {
309383
businessOperation = await this.getBusinessOperation(req, body.TransactionId.toString());
310384
workspace = await this.getWorkspace(req, data.workspaceId);
311-
tariffPlan = await this.getPlan(req, data.tariffPlanId);
312385
user = await this.getUser(req, data.userId);
386+
planId = data.isCardLinkOperation ? workspace.tariffPlanId.toString() : data.tariffPlanId;
387+
388+
tariffPlan = await this.getPlan(req, planId);
313389
} catch (e) {
314390
const error = e as Error;
315391

@@ -407,7 +483,7 @@ export default class CloudPaymentsWebhooks {
407483
type: SenderWorkerTaskType.PaymentSuccess,
408484
payload: {
409485
workspaceId: data.workspaceId,
410-
tariffPlanId: data.tariffPlanId,
486+
tariffPlanId: planId,
411487
userId: data.userId,
412488
},
413489
};
@@ -720,9 +796,9 @@ export default class CloudPaymentsWebhooks {
720796
*
721797
* @param req - request with necessary data
722798
*/
723-
private async getDataFromRequest(req: express.Request): Promise<PlanProlongationData> {
799+
private async getDataFromRequest(req: express.Request): Promise<PaymentData> {
724800
const context = req.context;
725-
const body: CheckRequest = req.body;
801+
const body: CheckRequest | PayRequest | FailRequest = req.body;
726802

727803
/**
728804
* If Data is not presented in body means there is a recurring payment
@@ -752,6 +828,7 @@ export default class CloudPaymentsWebhooks {
752828
tariffPlanId: workspace.tariffPlanId.toString(),
753829
userId,
754830
shouldSaveCard: false,
831+
isCardLinkOperation: false,
755832
};
756833
}
757834

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface ComposePaymentPayload {
2+
/**
3+
* Workspace Identifier
4+
*/
5+
workspaceId: string;
6+
/**
7+
* Id of the user making the payment
8+
*/
9+
userId: string;
10+
/**
11+
* Workspace current plan id or plan id to change
12+
*/
13+
tariffPlanId: string;
14+
/**
15+
* If true, we will save user card
16+
*/
17+
shouldSaveCard: 'true' | 'false';
18+
}

src/billing/types/paymentData.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,24 @@ export interface PaymentData {
4040
* Data for Cloudpayments needs
4141
*/
4242
cloudPayments?: CloudPaymentsSettings;
43+
/**
44+
* Workspace Identifier
45+
*/
46+
workspaceId: string;
47+
/**
48+
* Id of the user making the payment
49+
*/
50+
userId: string;
51+
/**
52+
* Workspace current plan id or plan id to change
53+
*/
54+
tariffPlanId: string;
55+
/**
56+
* If true, we will save user card
57+
*/
58+
shouldSaveCard: boolean;
59+
/**
60+
* True if this is card linking operation – charging minimal amount of money to validate card info
61+
*/
62+
isCardLinkOperation: boolean;
4363
}

src/models/eventsFactory.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,21 @@ class EventsFactory extends Factory {
159159
filters = {}
160160
) {
161161
limit = this.validateLimit(limit);
162-
sort = sort === 'BY_COUNT' ? 'count' : 'lastRepetitionTime';
162+
163+
switch (sort) {
164+
case 'BY_COUNT':
165+
sort = 'count';
166+
break;
167+
case 'BY_DATE':
168+
sort = 'lastRepetitionTime';
169+
break;
170+
case 'BY_AFFECTED_USERS':
171+
sort = 'affectedUsers';
172+
break;
173+
default:
174+
sort = 'lastRepetitionTime';
175+
break;
176+
}
163177

164178
const pipeline = [
165179
{

src/resolvers/billingNew.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ export default {
138138
*/
139139
async payWithCard(_obj: undefined, args: PayWithCardArgs, { factories, user }: ResolverContextWithUser): Promise<any> {
140140
const paymentData = checksumService.parseAndVerifyChecksum(args.input.checksum);
141+
142+
if (!('tariffPlanId' in paymentData)) {
143+
throw new UserInputError('Invalid checksum');
144+
}
145+
141146
const fullUserInfo = await factories.usersFactory.findById(user.id);
142147

143148
const workspace = await factories.workspacesFactory.findById(paymentData.workspaceId);

src/typeDefs/event.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,11 @@ type DailyEventInfo {
392392
Last event occurrence timestamp
393393
"""
394394
lastRepetitionTime: Float!
395+
396+
"""
397+
How many users catch this error per day
398+
"""
399+
affectedUsers: Int
395400
}
396401
397402
type Subscription {

src/typeDefs/project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Possible events order
88
enum EventsSortOrder {
99
BY_DATE
1010
BY_COUNT
11+
BY_AFFECTED_USERS
1112
}
1213
1314
"""

0 commit comments

Comments
 (0)