Skip to content

Commit

Permalink
Merge branch 'sms-service'
Browse files Browse the repository at this point in the history
  • Loading branch information
simlarsen committed Jun 9, 2023
2 parents f7ef0a8 + d0f7831 commit 7baafd0
Show file tree
Hide file tree
Showing 38 changed files with 2,210 additions and 116 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Mail",
"name": "Mail: Debug with Docker",
"localRoot": "${workspaceFolder}/Notification",
"name": "Notification: Debug with Docker",
"port": 9111,
"remoteRoot": "/usr/src/app",
"request": "attach",
Expand Down
37 changes: 37 additions & 0 deletions Common/Types/Permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ enum Permission {
CanEditMonitorProbe = 'CanEditMonitorProbe',
CanReadMonitorProbe = 'CanReadMonitorProbe',

CanCreateSmsLog = 'CanCreateSmsLog',
CanDeleteSmsLog = 'CanDeleteSmsLog',
CanEditSmsLog = 'CanEditSmsLog',
CanReadSmsLog = 'CanReadSmsLog',

CanCreateIncidentOwnerTeam = 'CanCreateIncidentOwnerTeam',
CanDeleteIncidentOwnerTeam = 'CanDeleteIncidentOwnerTeam',
CanEditIncidentOwnerTeam = 'CanEditIncidentOwnerTeam',
Expand Down Expand Up @@ -1668,6 +1673,38 @@ export class PermissionHelper {
isAccessControlPermission: false,
},

{
permission: Permission.CanCreateSmsLog,
title: 'Can Create SMS Log',
description: 'This permission can create SMS Log this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanDeleteSmsLog,
title: 'Can Delete SMS Log',
description:
'This permission can delete SMS Log of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanEditSmsLog,
title: 'Can Edit SMS Log',
description:
'This permission can edit SMS Log of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanReadSmsLog,
title: 'Can Read SMS Log',
description:
'This permission can read SMS Log of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},

{
permission: Permission.CanCreateMonitorProbe,
title: 'Can Create Monitor Probe',
Expand Down
7 changes: 7 additions & 0 deletions Common/Types/SmsStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enum SmsStatus {
Success = 'Success',
Error = 'Error',
LowBalance = 'Low Balance',
}

export default SmsStatus;
37 changes: 37 additions & 0 deletions CommonServer/Services/BillingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,16 @@ export class BillingService {
await this.stripe.paymentMethods.detach(paymentMethodId);
}

public static async hasPaymentMethods(
customerId: string
): Promise<boolean> {
if ((await this.getPaymentMethods(customerId)).length > 0) {
return true;
}

return false;
}

public static async getPaymentMethods(
customerId: string
): Promise<Array<PaymentMethod>> {
Expand Down Expand Up @@ -497,6 +507,33 @@ export class BillingService {
});
}

public static async genrateInvoiceAndChargeCustomer(
customerId: string,
itemText: string,
amountInUsd: number
): Promise<void> {
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
customer: customerId,
auto_advance: true, // do not automatically charge.
collection_method: 'charge_automatically',
});

if (!invoice || !invoice.id) {
throw new APIException('Invoice not generated.');
}

await this.stripe.invoiceItems.create({
invoice: invoice.id,
amount: amountInUsd * 100,
description: itemText,
customer: customerId,
});

await this.stripe.invoices.finalizeInvoice(invoice.id!);

await this.payInvoice(customerId, invoice.id!);
}

public static async payInvoice(
customerId: string,
invoiceId: string
Expand Down
24 changes: 24 additions & 0 deletions CommonServer/Services/DatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ class DatabaseService<TBaseModel extends BaseModel> {
private model!: TBaseModel;
private modelName!: string;

private _hardDeleteItemByColumnName: string = '';
public get hardDeleteItemByColumnName(): string {
return this._hardDeleteItemByColumnName;
}
public set hardDeleteItemByColumnName(v: string) {
this._hardDeleteItemByColumnName = v;
}

private _hardDeleteItemsOlderThanDays: number = 0;
public get hardDeleteItemsOlderThanDays(): number {
return this._hardDeleteItemsOlderThanDays;
}
public set hardDeleteItemsOlderThanDays(v: number) {
this._hardDeleteItemsOlderThanDays = v;
}

public constructor(
modelType: { new (): TBaseModel },
postgresDatabase?: PostgresDatabase
Expand All @@ -92,6 +108,14 @@ class DatabaseService<TBaseModel extends BaseModel> {
}
}

public hardDeleteItemsOlderThanInDays(
columnName: string,
olderThan: number
): void {
this.hardDeleteItemByColumnName = columnName;
this.hardDeleteItemsOlderThanDays = olderThan;
}

public getModel(): TBaseModel {
return this.model;
}
Expand Down
5 changes: 5 additions & 0 deletions CommonServer/Services/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ import WorkflowService from './WorkflowService';
import WorkflowVariablesService from './WorkflowVariableService';
import WorkflowLogService from './WorkflowLogService';

// SMS Log Servce
import SmsLogService from './SmsLogService';

export default [
UserService,
ProbeService,
Expand Down Expand Up @@ -118,4 +121,6 @@ export default [
WorkflowService,
WorkflowVariablesService,
WorkflowLogService,

SmsLogService,
];
112 changes: 112 additions & 0 deletions CommonServer/Services/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { IsBillingEnabled } from '../Config';
import ObjectID from 'Common/Types/ObjectID';
import Project from 'Model/Models/Project';
import ProjectService from './ProjectService';
import BillingService from './BillingService';
import logger from '../Utils/Logger';
import BadDataException from 'Common/Types/Exception/BadDataException';

export default class NotificationService {
public static async rechargeIfBalanceIsLow(
projectId: ObjectID,
options?: {
autoRechargeSmsOrCallByBalanceInUSD: number;
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: number;
enableAutoRechargeSmsOrCallBalance: boolean;
}
): Promise<number> {
let project: Project | null = null;
if (projectId && IsBillingEnabled) {
// check payment methods.

project = await ProjectService.findOneById({
id: projectId,
select: {
smsOrCallCurrentBalanceInUSDCents: true,
enableAutoRechargeSmsOrCallBalance: true,
enableSmsNotifications: true,
autoRechargeSmsOrCallByBalanceInUSD: true,
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true,
paymentProviderCustomerId: true,
},
props: {
isRoot: true,
},
});

const autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: number =
options?.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ||
project?.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ||
0;
const autoRechargeSmsOrCallByBalanceInUSD: number =
options?.autoRechargeSmsOrCallByBalanceInUSD ||
project?.autoRechargeSmsOrCallByBalanceInUSD ||
0;

const enableAutoRechargeSmsOrCallBalance: boolean = options
? options.enableAutoRechargeSmsOrCallBalance
: project?.enableAutoRechargeSmsOrCallBalance || false;

if (!project) {
return 0;
}

if (
!(await BillingService.hasPaymentMethods(
project.paymentProviderCustomerId!
))
) {
throw new BadDataException(
'No payment methods found for the project. Please add a payment method in project settings to continue.'
);
}

if (
enableAutoRechargeSmsOrCallBalance &&
autoRechargeSmsOrCallByBalanceInUSD &&
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD
) {
if (
(project.smsOrCallCurrentBalanceInUSDCents || 0) / 100 <
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD
) {
try {
// recharge balance
const updatedAmount: number = Math.floor(
(project.smsOrCallCurrentBalanceInUSDCents || 0) +
autoRechargeSmsOrCallByBalanceInUSD * 100
);

// If the recharge is succcessful, then update the project balance.
await BillingService.genrateInvoiceAndChargeCustomer(
project.paymentProviderCustomerId!,
'SMS or Call Balance Recharge',
autoRechargeSmsOrCallByBalanceInUSD
);

await ProjectService.updateOneById({
data: {
smsOrCallCurrentBalanceInUSDCents:
updatedAmount,
},
id: project.id!,
props: {
isRoot: true,
},
});

project.smsOrCallCurrentBalanceInUSDCents =
updatedAmount;

// TODO: Send an email on successful recharge.
} catch (err) {
// TODO: if the recharge fails, then send email to the user.
logger.error(err);
}
}
}
}

return project?.smsOrCallCurrentBalanceInUSDCents || 0;
}
}
16 changes: 16 additions & 0 deletions CommonServer/Services/ProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import AllMeteredPlans from '../Types/Billing/MeteredPlan/AllMeteredPlans';
import AccessTokenService from './AccessTokenService';
import SubscriptionStatus from 'Common/Types/Billing/SubscriptionStatus';
import User from 'Model/Models/User';
import NotificationService from './NotificationService';

export class Service extends DatabaseService<Model> {
public constructor(postgresDatabase?: PostgresDatabase) {
Expand Down Expand Up @@ -119,6 +120,21 @@ export class Service extends DatabaseService<Model> {
updateBy: UpdateBy<Model>
): Promise<OnUpdate<Model>> {
if (IsBillingEnabled) {
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
await NotificationService.rechargeIfBalanceIsLow(
new ObjectID(updateBy.query._id! as string),
{
autoRechargeSmsOrCallByBalanceInUSD: updateBy.data
.autoRechargeSmsOrCallByBalanceInUSD as number,
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD:
updateBy.data
.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD as number,
enableAutoRechargeSmsOrCallBalance: updateBy.data
.enableAutoRechargeSmsOrCallBalance as boolean,
}
);
}

if (updateBy.data.paymentProviderPlanId) {
// payment provider id changed.
const project: Model | null = await this.findOneById({
Expand Down
12 changes: 12 additions & 0 deletions CommonServer/Services/SmsLogService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
import Model from 'Model/Models/SmsLog';
import DatabaseService from './DatabaseService';

export class Service extends DatabaseService<Model> {
public constructor(postgresDatabase?: PostgresDatabase) {
super(Model, postgresDatabase);
this.hardDeleteItemsOlderThanInDays('createdAt', 30);
}
}

export default new Service();
41 changes: 41 additions & 0 deletions CommonServer/Services/SmsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import EmptyResponseData from 'Common/Types/API/EmptyResponse';
import HTTPResponse from 'Common/Types/API/HTTPResponse';
import Route from 'Common/Types/API/Route';
import URL from 'Common/Types/API/URL';
import { JSONObject } from 'Common/Types/JSON';
import API from 'Common/Utils/API';
import { NotificationHostname } from '../Config';
import Protocol from 'Common/Types/API/Protocol';
import ClusterKeyAuthorization from '../Middleware/ClusterKeyAuthorization';
import Phone from 'Common/Types/Phone';
import ObjectID from 'Common/Types/ObjectID';

export default class SmsService {
public static async sendSms(
to: Phone,
message: string,
options: {
projectId?: ObjectID | undefined; // project id for sms log
from?: Phone; // from phone number
}
): Promise<HTTPResponse<EmptyResponseData>> {
const body: JSONObject = {
to: to.toString(),
message,
from: options.from?.toString(),
projectId: options.projectId?.toString(),
};

return await API.post<EmptyResponseData>(
new URL(
Protocol.HTTP,
NotificationHostname,
new Route('/sms/send')
),
body,
{
...ClusterKeyAuthorization.getClusterKeyHeaders(),
}
);
}
}
1 change: 1 addition & 0 deletions CommonServer/Services/WorkflowLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import DatabaseService from './DatabaseService';
export class Service extends DatabaseService<Model> {
public constructor(postgresDatabase?: PostgresDatabase) {
super(Model, postgresDatabase);
this.hardDeleteItemsOlderThanInDays('createdAt', 30);
}
}
export default new Service();
Loading

0 comments on commit 7baafd0

Please sign in to comment.