Skip to content

Commit 26224d5

Browse files
authored
Merge pull request #461 from codex-team/master
Update prod
2 parents cec569a + 424edb0 commit 26224d5

File tree

9 files changed

+179
-27
lines changed

9 files changed

+179
-27
lines changed

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

src/billing/cloudpayments.ts

Lines changed: 61 additions & 11 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
*
@@ -201,7 +231,13 @@ export default class CloudPaymentsWebhooks {
201231
let member: ConfirmedMemberDBScheme;
202232
let plan: PlanDBScheme;
203233

204-
if (!data.workspaceId || !data.tariffPlanId || !data.userId) {
234+
if (data.isCardLinkOperation && (!data.userId || !data.workspaceId)) {
235+
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] Card linking – invalid data', body);
236+
237+
return;
238+
}
239+
240+
if (!data.isCardLinkOperation && (!data.userId || !data.workspaceId || !data.tariffPlanId)) {
205241
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body);
206242

207243
return;
@@ -235,6 +271,18 @@ export default class CloudPaymentsWebhooks {
235271
return;
236272
}
237273

274+
if (data.isCardLinkOperation) {
275+
telegram
276+
.sendMessage(`✅ [Billing / Check] Card linked for subscription workspace «${workspace.name}»`, TelegramBotURLs.Money)
277+
.catch(e => console.error('Error while sending message to Telegram: ' + e));
278+
279+
res.json({
280+
code: CheckCodes.SUCCESS,
281+
} as CheckResponse);
282+
283+
return;
284+
}
285+
238286
/**
239287
* Create business operation about creation of subscription
240288
*/
@@ -266,6 +314,7 @@ export default class CloudPaymentsWebhooks {
266314

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

271320
res.json({
@@ -720,9 +769,9 @@ export default class CloudPaymentsWebhooks {
720769
*
721770
* @param req - request with necessary data
722771
*/
723-
private async getDataFromRequest(req: express.Request): Promise<PlanProlongationData> {
772+
private async getDataFromRequest(req: express.Request): Promise<PaymentData> {
724773
const context = req.context;
725-
const body: CheckRequest = req.body;
774+
const body: CheckRequest | PayRequest | FailRequest = req.body;
726775

727776
/**
728777
* If Data is not presented in body means there is a recurring payment
@@ -752,6 +801,7 @@ export default class CloudPaymentsWebhooks {
752801
tariffPlanId: workspace.tariffPlanId.toString(),
753802
userId,
754803
shouldSaveCard: false,
804+
isCardLinkOperation: false,
755805
};
756806
}
757807

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
"""

src/utils/checksumService.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
import { PlanProlongationPayload } from '@hawk.so/types';
22
import jwt, { Secret } from 'jsonwebtoken';
33

4+
export type ChecksumData = PlanPurchaseChecksumData | CardLinkChecksumData;
5+
6+
interface PlanPurchaseChecksumData {
7+
/**
8+
* Workspace Identifier
9+
*/
10+
workspaceId: string;
11+
/**
12+
* Id of the user making the payment
13+
*/
14+
userId: string;
15+
/**
16+
* Workspace current plan id or plan id to change
17+
*/
18+
tariffPlanId: string;
19+
/**
20+
* If true, we will save user card
21+
*/
22+
shouldSaveCard: boolean;
23+
}
24+
25+
interface CardLinkChecksumData {
26+
/**
27+
* Workspace Identifier
28+
*/
29+
workspaceId: string;
30+
/**
31+
* Id of the user making the payment
32+
*/
33+
userId: string;
34+
/**
35+
* True if this is card linking operation – charging minimal amount of money to validate card info
36+
*/
37+
isCardLinkOperation: boolean;
38+
}
39+
440
/**
541
* Helper class for working with checksums
642
*/
@@ -10,7 +46,7 @@ class ChecksumService {
1046
*
1147
* @param data - data for processing billing request
1248
*/
13-
public async generateChecksum(data: PlanProlongationPayload): Promise<string> {
49+
public async generateChecksum(data: ChecksumData): Promise<string> {
1450
return jwt.sign(
1551
data,
1652
process.env.JWT_SECRET_BILLING_CHECKSUM as Secret,
@@ -23,20 +59,23 @@ class ChecksumService {
2359
*
2460
* @param checksum - checksum to parse
2561
*/
26-
public parseAndVerifyChecksum(checksum: string): PlanProlongationPayload {
27-
const payload = jwt.verify(checksum, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret) as PlanProlongationPayload;
28-
29-
/**
30-
* Filter unnecessary fields from JWT payload (e.g. "iat")
31-
*/
32-
const { tariffPlanId, workspaceId, userId, shouldSaveCard } = payload;
62+
public parseAndVerifyChecksum(checksum: string): ChecksumData {
63+
const payload = jwt.verify(checksum, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret) as ChecksumData;
3364

34-
return {
35-
tariffPlanId,
36-
workspaceId,
37-
userId,
38-
shouldSaveCard,
39-
};
65+
if ('isCardLinkOperation' in payload) {
66+
return {
67+
workspaceId: payload.workspaceId,
68+
userId: payload.userId,
69+
isCardLinkOperation: payload.isCardLinkOperation,
70+
};
71+
} else {
72+
return {
73+
workspaceId: payload.workspaceId,
74+
userId: payload.userId,
75+
tariffPlanId: payload.tariffPlanId,
76+
shouldSaveCard: payload.shouldSaveCard,
77+
};
78+
}
4079
}
4180
}
4281

0 commit comments

Comments
 (0)