Skip to content

Commit 02528cc

Browse files
github-actions[bot]e11syTatianaFomina
authored
Update prod (#467)
* types(notification-rule): respect new notification model * Bump version up to 1.1.2 * chore(dependencies): update hawk.so/types * Bump version up to 1.1.7 * chore (yarn) update yarn lock file * types(notifications): update typeDefs * types(resolvers): update projectNotifications types for resolvers * feat(models): add extra fields for update and create notification rule methods * types(notifications): improve notifications graphql schemas * imp(resolver): improved notification rules resolver * Return back business operation * Operation type * Bump version up to 1.1.10 * fix(resolvers): improve create rule data validation * imp(resolvers): add channels validation for rules in api resolvers * clean(resolvers): remove redundant validation * chore(): lint fix * Update types * Update package.json * bump package version * fix(): build fix --------- Co-authored-by: e11sy <130844513+e11sy@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tanya Fomina <fomina.tatianaaa@yandex.ru>
1 parent c875e51 commit 02528cc

File tree

7 files changed

+183
-72
lines changed

7 files changed

+183
-72
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.1.9",
3+
"version": "1.1.12",
44
"main": "index.ts",
55
"license": "UNLICENSED",
66
"scripts": {
@@ -37,7 +37,7 @@
3737
"@graphql-tools/schema": "^8.5.1",
3838
"@graphql-tools/utils": "^8.9.0",
3939
"@hawk.so/nodejs": "^3.1.1",
40-
"@hawk.so/types": "^0.1.21",
40+
"@hawk.so/types": "^0.1.26",
4141
"@types/amqp-connection-manager": "^2.0.4",
4242
"@types/bson": "^4.0.5",
4343
"@types/debug": "^4.1.5",

src/billing/cloudpayments.ts

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -205,36 +205,6 @@ export default class CloudPaymentsWebhooks {
205205
return `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${tariffPlan.name}`;
206206
}
207207

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-
238208
/**
239209
* Route to confirm the correctness of a user's payment
240210
* https://developers.cloudpayments.ru/#check
@@ -257,28 +227,33 @@ export default class CloudPaymentsWebhooks {
257227
return;
258228
}
259229

230+
/** Data validation */
260231
if (data.isCardLinkOperation) {
261-
this.checkCardLinkOperation(req, res, data);
232+
if (!data.userId || !data.workspaceId) {
233+
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the card linking request', req.body);
262234

263-
return;
235+
return;
236+
}
237+
} else {
238+
if (!data.userId || !data.workspaceId || !data.tariffPlanId) {
239+
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body);
240+
241+
return;
242+
}
264243
}
265244

266245
let workspace: WorkspaceModel;
267246
let member: ConfirmedMemberDBScheme;
268247
let plan: PlanDBScheme;
269-
270-
if (!data.userId || !data.workspaceId || !data.tariffPlanId) {
271-
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body);
272-
273-
return;
274-
}
248+
let planId: string;
275249

276250
const { workspaceId, userId, tariffPlanId } = data;
277251

278252
try {
279253
workspace = await this.getWorkspace(req, workspaceId);
280254
member = await this.getMember(userId, workspace);
281-
plan = await this.getPlan(req, tariffPlanId);
255+
planId = data.isCardLinkOperation ? workspace.tariffPlanId.toString() : tariffPlanId;
256+
plan = await this.getPlan(req, planId);
282257
} catch (e) {
283258
const error = e as Error;
284259

@@ -307,7 +282,7 @@ export default class CloudPaymentsWebhooks {
307282
try {
308283
await context.factories.businessOperationsFactory.create<PayloadOfWorkspacePlanPurchase>({
309284
transactionId: body.TransactionId.toString(),
310-
type: BusinessOperationType.WorkspacePlanPurchase,
285+
type: data.isCardLinkOperation ? BusinessOperationType.CardLinkCharge : BusinessOperationType.WorkspacePlanPurchase,
311286
status: BusinessOperationStatus.Pending,
312287
payload: {
313288
workspaceId: workspace._id,
@@ -361,16 +336,19 @@ export default class CloudPaymentsWebhooks {
361336
return;
362337
}
363338

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-
}
339+
/** Data validation */
340+
if (data.isCardLinkOperation) {
341+
if (!data.userId || !data.workspaceId) {
342+
this.sendError(res, PayCodes.SUCCESS, '[Billing / Pay] No workspace or user id in request body', req.body);
369343

370-
if (!data.isCardLinkOperation && (!data.workspaceId || !data.tariffPlanId || !data.userId)) {
371-
this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] No workspace, tariff plan or user id in request body`, body);
344+
return;
345+
}
346+
} else {
347+
if (!data.workspaceId || !data.tariffPlanId || !data.userId) {
348+
this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] No workspace, tariff plan or user id in request body`, body);
372349

373-
return;
350+
return;
351+
}
374352
}
375353

376354
let businessOperation;
@@ -384,7 +362,6 @@ export default class CloudPaymentsWebhooks {
384362
workspace = await this.getWorkspace(req, data.workspaceId);
385363
user = await this.getUser(req, data.userId);
386364
planId = data.isCardLinkOperation ? workspace.tariffPlanId.toString() : data.tariffPlanId;
387-
388365
tariffPlan = await this.getPlan(req, planId);
389366
} catch (e) {
390367
const error = e as Error;

src/models/project.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface ProjectNotificationsRuleDBScheme {
2424
uidAdded: ObjectId;
2525

2626
/**
27-
* Receive type: 'ALL' or 'ONLY_NEW'
27+
* Receive type: 'SEEN_MORE' or 'ONLY_NEW'
2828
*/
2929
whatToReceive: ReceiveTypes;
3030

@@ -42,6 +42,16 @@ export interface ProjectNotificationsRuleDBScheme {
4242
* Available channels to receive
4343
*/
4444
channels: NotificationsChannelsDBScheme;
45+
46+
/**
47+
* If this number of events is reached in the eventThresholdPeriod, the rule will be triggered
48+
*/
49+
threshold?: number;
50+
51+
/**
52+
* Size of period (in milliseconds) to count events to compare to rule threshold
53+
*/
54+
thresholdPeriod?: number;
4555
}
4656

4757
/**
@@ -51,7 +61,7 @@ export enum ReceiveTypes {
5161
/**
5262
* All notifications
5363
*/
54-
ALL = 'ALL',
64+
SEEN_MORE = 'SEEN_MORE',
5565

5666
/**
5767
* Only first occurrence
@@ -69,7 +79,7 @@ export interface CreateProjectNotificationsRulePayload {
6979
isEnabled: true;
7080

7181
/**
72-
* Receive type: 'ALL' or 'ONLY_NEW'
82+
* Receive type: 'SEEN_MORE' or 'ONLY_NEW'
7383
*/
7484
whatToReceive: ReceiveTypes;
7585

@@ -92,6 +102,16 @@ export interface CreateProjectNotificationsRulePayload {
92102
* Available channels to receive
93103
*/
94104
channels: NotificationsChannelsDBScheme;
105+
106+
/**
107+
* If this number of events is reached in the eventThresholdPeriod, the rule will be triggered
108+
*/
109+
threshold?: number;
110+
111+
/**
112+
* Size of period (in milliseconds) to count events to compare to rule threshold
113+
*/
114+
thresholdPeriod?: number;
95115
}
96116

97117
/**
@@ -127,6 +147,16 @@ interface UpdateProjectNotificationsRulePayload {
127147
* Available channels to receive
128148
*/
129149
channels: NotificationsChannelsDBScheme;
150+
151+
/**
152+
* If this number of events is reached in the eventThresholdPeriod, the rule will be triggered
153+
*/
154+
threshold?: number;
155+
156+
/**
157+
* Size of period (in milliseconds) to count events to compare to rule threshold
158+
*/
159+
thresholdPeriod?: number;
130160
}
131161

132162
/**
@@ -232,6 +262,11 @@ export default class ProjectModel extends AbstractModel<ProjectDBScheme> impleme
232262
excluding: payload.excluding,
233263
};
234264

265+
if (rule.whatToReceive === ReceiveTypes.SEEN_MORE) {
266+
rule.threshold = payload.threshold;
267+
rule.thresholdPeriod = payload.thresholdPeriod;
268+
}
269+
235270
await this.collection.updateOne({
236271
_id: this._id,
237272
},
@@ -261,6 +296,11 @@ export default class ProjectModel extends AbstractModel<ProjectDBScheme> impleme
261296
excluding: payload.excluding,
262297
};
263298

299+
if (rule.whatToReceive === ReceiveTypes.SEEN_MORE) {
300+
rule.threshold = payload.threshold;
301+
rule.thresholdPeriod = payload.thresholdPeriod;
302+
}
303+
264304
const result = await this.collection.findOneAndUpdate(
265305
{
266306
_id: this._id,

src/resolvers/projectNotifications.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ interface CreateProjectNotificationsRuleMutationPayload {
3939
* Available channels to receive
4040
*/
4141
channels: NotificationsChannelsDBScheme;
42+
43+
/**
44+
* Threshold to receive notification
45+
*/
46+
threshold: number;
47+
48+
/**
49+
* Period to receive notification
50+
*/
51+
thresholdPeriod: number;
4252
}
4353

4454
/**
@@ -67,16 +77,50 @@ interface ProjectNotificationsRulePointer {
6777
}
6878

6979
/**
70-
* Return true if all passed channels are empty
71-
* @param channels - project notifications channels
80+
* Returns true is threshold and threshold period are valid
81+
* @param threshold - threshold of the notification rule to be checked
82+
* @param thresholdPeriod - threshold period of the notification rule to be checked
7283
*/
73-
function isChannelsEmpty(channels: NotificationsChannelsDBScheme): boolean {
74-
const notEmptyChannels = Object.entries(channels)
75-
.filter(([_, channel]) => {
76-
return (channel as NotificationsChannelSettingsDBScheme).endpoint.replace(/\s+/, '').trim().length !== 0;
77-
});
84+
function validateNotificationsRuleTresholdAndPeriod(
85+
threshold: ProjectNotificationsRuleDBScheme['threshold'],
86+
thresholdPeriod: ProjectNotificationsRuleDBScheme['thresholdPeriod']
87+
): string | null {
88+
const validThresholdPeriods = [60_000, 3_600_000, 86_400_000, 604_800_000];
89+
90+
if (thresholdPeriod === undefined || !validThresholdPeriods.includes(thresholdPeriod)) {
91+
return 'Threshold period should be one of the following: 60000, 3600000, 86400000, 604800000';
92+
}
93+
94+
if (threshold === undefined || threshold < 1) {
95+
return 'Threshold should be greater than 0';
96+
}
97+
98+
return null;
99+
}
78100

79-
return notEmptyChannels.length === 0;
101+
/**
102+
* Return true if all passed channels are filled with correct endpoints
103+
*/
104+
function validateNotificationsRuleChannels(channels: NotificationsChannelsDBScheme): string | null {
105+
if (channels.email!.isEnabled) {
106+
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(channels.email!.endpoint)) {
107+
return 'Invalid email endpoint passed';
108+
}
109+
}
110+
111+
if (channels.slack!.isEnabled) {
112+
if (!/^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9]+\/[A-Za-z0-9]+\/[A-Za-z0-9]+$/.test(channels.slack!.endpoint)) {
113+
return 'Invalid slack endpoint passed';
114+
}
115+
}
116+
117+
if (channels.telegram!.isEnabled) {
118+
if (!/^https:\/\/notify\.bot\.codex\.so\/u\/[A-Za-z0-9]+$/.test(channels.telegram!.endpoint)) {
119+
return 'Invalid telegram endpoint passed';
120+
}
121+
}
122+
123+
return null;
80124
}
81125

82126
/**
@@ -102,8 +146,18 @@ export default {
102146
throw new ApolloError('No project with such id');
103147
}
104148

105-
if (isChannelsEmpty(input.channels)) {
106-
throw new UserInputError('At least one channel is required');
149+
const channelsValidationResult = validateNotificationsRuleChannels(input.channels);
150+
151+
if (channelsValidationResult !== null) {
152+
throw new UserInputError(channelsValidationResult);
153+
}
154+
155+
if (input.whatToReceive === ReceiveTypes.SEEN_MORE) {
156+
const thresholdValidationResult = validateNotificationsRuleTresholdAndPeriod(input.threshold, input.thresholdPeriod);
157+
158+
if (thresholdValidationResult !== null) {
159+
throw new UserInputError(thresholdValidationResult);
160+
}
107161
}
108162

109163
return project.createNotificationsRule({
@@ -130,8 +184,18 @@ export default {
130184
throw new ApolloError('No project with such id');
131185
}
132186

133-
if (isChannelsEmpty(input.channels)) {
134-
throw new UserInputError('At least one channel is required');
187+
const channelsValidationResult = validateNotificationsRuleChannels(input.channels);
188+
189+
if (channelsValidationResult !== null) {
190+
throw new UserInputError(channelsValidationResult);
191+
}
192+
193+
if (input.whatToReceive === ReceiveTypes.SEEN_MORE) {
194+
const thresholdValidationResult = validateNotificationsRuleTresholdAndPeriod(input.threshold, input.thresholdPeriod);
195+
196+
if (thresholdValidationResult !== null) {
197+
throw new UserInputError(thresholdValidationResult);
198+
}
135199
}
136200

137201
return project.updateNotificationsRule(input);

src/typeDefs/projectNotifications.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export default gql`
1111
ONLY_NEW
1212
1313
"""
14-
Receive all events
14+
Receive all events that reached threshold in period
1515
"""
16-
ALL
16+
SEEN_MORE
1717
}
1818
1919
"""
@@ -49,5 +49,15 @@ export default gql`
4949
Notification channels to recieve events
5050
"""
5151
channels: NotificationsChannels
52+
53+
"""
54+
Threshold to receive notification
55+
"""
56+
threshold: Int
57+
58+
"""
59+
Period to receive notification
60+
"""
61+
thresholdPeriod: Int
5262
}
5363
`;

0 commit comments

Comments
 (0)