From fd3bc4dd8660d19bd1c41795ef83413083cca06d Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 14:41:09 +0100 Subject: [PATCH 01/27] add new fields in project --- Model/Models/Project.ts | 141 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/Model/Models/Project.ts b/Model/Models/Project.ts index fbca2731665..897524fba8a 100644 --- a/Model/Models/Project.ts +++ b/Model/Models/Project.ts @@ -525,4 +525,145 @@ export default class Model extends TenantModel { unique: false, }) public currentActiveMonitorsCount?: number = undefined; + + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'SMS or Call Current Balance', + description: 'Balance in USD for SMS or Call', + }) + @Column({ + type: ColumnType.Number, + nullable: false, + unique: false, + default: 0, + }) + public smsOrCallCurrentBalanceInUSD?: number = undefined; + + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + ], + update: [ + Permission.ProjectOwner, + Permission.CanManageProjectBilling, + ], + }) + @TableColumn({ + type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'Auto Recharge Amount', + description: 'Auto recharge amount in USD for SMS or Call', + }) + @Column({ + type: ColumnType.Number, + nullable: false, + unique: false, + default: 0, + }) + public autoRechargeSmsOrCallCurrentBalanceInUSD?: number = undefined; + + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + Permission.ProjectUser, + ], + update: [ + Permission.ProjectOwner, + Permission.CanManageProjectBilling, + ], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Enable SMS Notifications', + description: 'Enable SMS notificaitons for this project.', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public enableSmsNotifications?: boolean = undefined; + + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + Permission.ProjectUser, + ], + update: [ + Permission.ProjectOwner, + Permission.CanManageProjectBilling, + ], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Enable Call Notifications', + description: 'Enable call notificaitons for this project.', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public enableCallNotifications?: boolean = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + Permission.ProjectUser, + ], + update: [ + Permission.ProjectOwner, + Permission.CanManageProjectBilling, + ], + }) + @TableColumn({ + required: true, + isDefaultValueColumn: true, + type: TableColumnType.Boolean, + title: 'Enable auto recharge SMS or Call balance', + description: 'Enable auto recharge SMS or Call balance for this project.', + }) + @Column({ + nullable: false, + default: false, + type: ColumnType.Boolean, + }) + public enableAutoRechargeSmsOrCallBalance?: boolean = undefined; + } From c2fe4d3e5e7bc3a6dc98734aecb9206b2f62cd80 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 14:59:44 +0100 Subject: [PATCH 02/27] add sms log models. --- Common/Types/Permission.ts | 43 ++++++ Common/Types/SmsStatus.ts | 6 + Model/Models/Index.ts | 5 + Model/Models/SmsLog.ts | 299 +++++++++++++++++++++++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 Common/Types/SmsStatus.ts create mode 100644 Model/Models/SmsLog.ts diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index 10a8ea32029..a2fabcd7178 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -73,6 +73,12 @@ enum Permission { CanEditMonitorProbe = 'CanEditMonitorProbe', CanReadMonitorProbe = 'CanReadMonitorProbe', + + CanCreateSmsLog = 'CanCreateSmsLog', + CanDeleteSmsLog = 'CanDeleteSmsLog', + CanEditSmsLog = 'CanEditSmsLog', + CanReadSmsLog = 'CanReadSmsLog', + CanCreateIncidentOwnerTeam = 'CanCreateIncidentOwnerTeam', CanDeleteIncidentOwnerTeam = 'CanDeleteIncidentOwnerTeam', CanEditIncidentOwnerTeam = 'CanEditIncidentOwnerTeam', @@ -1668,6 +1674,43 @@ 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', diff --git a/Common/Types/SmsStatus.ts b/Common/Types/SmsStatus.ts new file mode 100644 index 00000000000..9c074dee948 --- /dev/null +++ b/Common/Types/SmsStatus.ts @@ -0,0 +1,6 @@ +enum SmsStatus { + Success = "Success", + Error = "Error", +} + +export default SmsStatus; \ No newline at end of file diff --git a/Model/Models/Index.ts b/Model/Models/Index.ts index 0afc9207077..2fa92e6987c 100644 --- a/Model/Models/Index.ts +++ b/Model/Models/Index.ts @@ -85,6 +85,9 @@ import WorkflowLog from './WorkflowLog'; //SSO import ProjectSSO from './ProjectSso'; +// SMS +import SmsLog from './SmsLog'; + export default [ User, Probe, @@ -156,4 +159,6 @@ export default [ StatusPageOwnerTeam, StatusPageOwnerUser, + + SmsLog ]; diff --git a/Model/Models/SmsLog.ts b/Model/Models/SmsLog.ts new file mode 100644 index 00000000000..914bc48c6a2 --- /dev/null +++ b/Model/Models/SmsLog.ts @@ -0,0 +1,299 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; +import BaseModel from 'Common/Models/BaseModel'; +import Project from './Project'; +import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint'; +import SmsStatus from 'Common/Types/SmsStatus'; +import Route from 'Common/Types/API/Route'; +import TableColumnType from 'Common/Types/Database/TableColumnType'; +import TableColumn from 'Common/Types/Database/TableColumn'; +import ColumnType from 'Common/Types/Database/ColumnType'; +import ObjectID from 'Common/Types/ObjectID'; +import ColumnLength from 'Common/Types/Database/ColumnLength'; +import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl'; +import Permission from 'Common/Types/Permission'; +import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl'; +import TenantColumn from 'Common/Types/Database/TenantColumn'; +import TableMetadata from 'Common/Types/Database/TableMetadata'; +import EnableWorkflow from 'Common/Types/Model/EnableWorkflow'; +import IconProp from 'Common/Types/Icon/IconProp'; +import EnableDocumentation from 'Common/Types/Model/EnableDocumentation'; +import Phone from 'Common/Types/Phone'; + +@EnableDocumentation() +@TenantColumn('projectId') +@TableAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + delete: [ + + ], + update: [ + + ], +}) +@CrudApiEndpoint(new Route('/sms-log')) +@Entity({ + name: 'SmsLog', +}) +@EnableWorkflow({ + create: true, + delete: false, + update: false, + read: true, +}) +@TableMetadata({ + tableName: 'SmsLog', + singularName: 'SMS Log', + pluralName: 'SMS Logs', + icon: IconProp.SMS, + tableDescription: + 'Logs of all the SMS sent out to all users and subscribers for this project.', +}) +export default class SmsLog extends BaseModel { + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CanCreateSmsLog, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [], + }) + @TableColumn({ + manyToOneRelationColumn: 'projectId', + type: TableColumnType.Entity, + modelType: Project, + title: 'Project', + description: + 'Relation to Project Resource in which this object belongs', + }) + @ManyToOne( + (_type: string) => { + return Project; + }, + { + eager: false, + nullable: true, + onDelete: 'CASCADE', + orphanedRowAction: 'nullify', + } + ) + @JoinColumn({ name: 'projectId' }) + public project?: Project = undefined; + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CanCreateSmsLog, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [], + }) + @Index() + @TableColumn({ + type: TableColumnType.ObjectID, + required: true, + canReadOnPopulate: true, + title: 'Project ID', + description: + 'ID of your OneUptime Project in which this object belongs', + }) + @Column({ + type: ColumnType.ObjectID, + nullable: false, + transformer: ObjectID.getDatabaseTransformer(), + }) + public projectId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @Index() + @TableColumn({ + required: true, + type: TableColumnType.Phone, + title: 'To Number', + description: 'Phone Number SMS was sent to', + canReadOnPopulate: false, + }) + @Column({ + nullable: false, + type: ColumnType.Phone, + length: ColumnLength.Phone, + }) + public toNumber?: Phone = undefined; + + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @Index() + @TableColumn({ + required: true, + type: TableColumnType.Phone, + title: 'From Number', + description: 'Phone Number SMS was sent from', + canReadOnPopulate: false, + }) + @Column({ + nullable: false, + type: ColumnType.Phone, + length: ColumnLength.Phone, + }) + public fromNumber?: Phone = undefined; + + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.LongText, + title: 'SMS Text', + description: 'Text content of the message', + canReadOnPopulate: false, + }) + @Column({ + nullable: false, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public smsText?: string = undefined; + + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @TableColumn({ + required: false, + type: TableColumnType.LongText, + title: 'Error Message', + description: 'Error Message (if any)', + canReadOnPopulate: false, + }) + @Column({ + nullable: true, + type: ColumnType.LongText, + length: ColumnLength.LongText, + }) + public errorMessage?: string = undefined; + + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.ShortText, + title: 'Status of the SMS', + description: 'Status of the SMS sent', + canReadOnPopulate: false, + }) + @Column({ + nullable: false, + type: ColumnType.ShortText, + length: ColumnLength.ShortText, + }) + public status?: SmsStatus = undefined; + + @ColumnAccessControl({ + create: [ + + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadSmsLog, + ], + update: [ + + ], + }) + @TableColumn({ + required: true, + type: TableColumnType.Number, + title: 'SMS Cost', + description: 'SMS Cost in USD Cents', + canReadOnPopulate: false, + }) + @Column({ + nullable: false, + type: ColumnType.Number, + }) + public smsCostInUSDCents?: number = undefined; + +} From 6c10f812262ba74096bdacbd4284bd670d51c294 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 19:17:16 +0100 Subject: [PATCH 03/27] add call and sms settings. --- Dashboard/src/App.tsx | 128 ++++++---- Dashboard/src/Pages/Settings/CallSms.tsx | 289 ++++++++++++++++++++++ Dashboard/src/Pages/Settings/SideMenu.tsx | 45 ++-- Dashboard/src/Pages/Settings/SmsLog.tsx | 233 +++++++++++++++++ Dashboard/src/Utils/PageMap.ts | 4 + Dashboard/src/Utils/RouteMap.ts | 10 + Model/Models/Project.ts | 78 +++--- 7 files changed, 661 insertions(+), 126 deletions(-) create mode 100644 Dashboard/src/Pages/Settings/CallSms.tsx create mode 100644 Dashboard/src/Pages/Settings/SmsLog.tsx diff --git a/Dashboard/src/App.tsx b/Dashboard/src/App.tsx index cc3018822b6..a1f2bf43a25 100644 --- a/Dashboard/src/App.tsx +++ b/Dashboard/src/App.tsx @@ -94,6 +94,8 @@ import SettingsDomains from './Pages/Settings/Domains'; import SettingsIncidentSeverity from './Pages/Settings/IncidentSeverity'; import SettingsBilling from './Pages/Settings/Billing'; import SettingsSSO from './Pages/Settings/SSO'; +import SettingsSmsLog from './Pages/Settings/SmsLog'; +import SettingsCallSms from './Pages/Settings/CallSms'; import SettingsInvoices from './Pages/Settings/Invoices'; import MonitorCustomFields from './Pages/Settings/MonitorCustomFields'; import StatusPageCustomFields from './Pages/Settings/StatusPageCustomFields'; @@ -348,7 +350,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.HOME_NOT_OPERATIONAL_MONITORS + PageMap.HOME_NOT_OPERATIONAL_MONITORS ] as Route } /> @@ -368,8 +370,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS + PageMap + .HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -397,7 +399,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITORS_INOPERATIONAL + PageMap.MONITORS_INOPERATIONAL ] as Route } /> @@ -453,7 +455,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_STATUS_TIMELINE + PageMap.MONITOR_VIEW_STATUS_TIMELINE ] as Route } /> @@ -485,7 +487,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_INCIDENTS + PageMap.MONITOR_VIEW_INCIDENTS ] as Route } /> @@ -503,7 +505,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_CUSTOM_FIELDS + PageMap.MONITOR_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -683,7 +685,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_DELETE + PageMap.STATUS_PAGE_VIEW_DELETE ] as Route } /> @@ -701,7 +703,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_BRANDING + PageMap.STATUS_PAGE_VIEW_BRANDING ] as Route } /> @@ -719,7 +721,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_HTML_CSS + PageMap.STATUS_PAGE_VIEW_CUSTOM_HTML_CSS ] as Route } /> @@ -737,7 +739,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_ADVANCED_OPTIONS + PageMap.STATUS_PAGE_VIEW_ADVANCED_OPTIONS ] as Route } /> @@ -755,7 +757,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_FIELDS + PageMap.STATUS_PAGE_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -772,7 +774,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_OWNERS + PageMap.STATUS_PAGE_VIEW_OWNERS ] as Route } /> @@ -804,7 +806,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS ] as Route } /> @@ -822,8 +824,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .STATUS_PAGE_VIEW_AUTHENTICATION_SETTINGS + PageMap + .STATUS_PAGE_VIEW_AUTHENTICATION_SETTINGS ] as Route } /> @@ -841,7 +843,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_SMTP + PageMap.STATUS_PAGE_VIEW_CUSTOM_SMTP ] as Route } /> @@ -859,7 +861,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_SETTINGS + PageMap.STATUS_PAGE_VIEW_SETTINGS ] as Route } /> @@ -877,7 +879,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_PRIVATE_USERS + PageMap.STATUS_PAGE_VIEW_PRIVATE_USERS ] as Route } /> @@ -895,7 +897,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS ] as Route } /> @@ -913,7 +915,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_HEADER_STYLE + PageMap.STATUS_PAGE_VIEW_HEADER_STYLE ] as Route } /> @@ -931,7 +933,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE + PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE ] as Route } /> @@ -949,7 +951,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_NAVBAR_STYLE + PageMap.STATUS_PAGE_VIEW_NAVBAR_STYLE ] as Route } /> @@ -967,7 +969,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS ] as Route } /> @@ -985,7 +987,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_EMBEDDED + PageMap.STATUS_PAGE_VIEW_EMBEDDED ] as Route } /> @@ -1003,7 +1005,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_RESOURCES + PageMap.STATUS_PAGE_VIEW_RESOURCES ] as Route } /> @@ -1021,7 +1023,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_DOMAINS + PageMap.STATUS_PAGE_VIEW_DOMAINS ] as Route } /> @@ -1038,7 +1040,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_GROUPS + PageMap.STATUS_PAGE_VIEW_GROUPS ] as Route } /> @@ -1056,7 +1058,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_ANNOUNCEMENTS + PageMap.STATUS_PAGE_VIEW_ANNOUNCEMENTS ] as Route } /> @@ -1124,7 +1126,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_VIEW_STATE_TIMELINE + PageMap.INCIDENT_VIEW_STATE_TIMELINE ] as Route } /> @@ -1141,7 +1143,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_INTERNAL_NOTE + PageMap.INCIDENT_INTERNAL_NOTE ] as Route } /> @@ -1159,7 +1161,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_VIEW_CUSTOM_FIELDS + PageMap.INCIDENT_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -1207,7 +1209,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_EVENTS + PageMap.SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -1225,7 +1227,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.ONGOING_SCHEDULED_MAINTENANCE_EVENTS + PageMap.ONGOING_SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -1243,7 +1245,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW + PageMap.SCHEDULED_MAINTENANCE_VIEW ] as Route } /> @@ -1261,8 +1263,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SCHEDULED_MAINTENANCE_VIEW_CUSTOM_FIELDS + PageMap + .SCHEDULED_MAINTENANCE_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -1280,7 +1282,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW_DELETE + PageMap.SCHEDULED_MAINTENANCE_VIEW_DELETE ] as Route } /> @@ -1298,7 +1300,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW_OWNERS + PageMap.SCHEDULED_MAINTENANCE_VIEW_OWNERS ] as Route } /> @@ -1316,8 +1318,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SCHEDULED_MAINTENANCE_VIEW_STATE_TIMELINE + PageMap + .SCHEDULED_MAINTENANCE_VIEW_STATE_TIMELINE ] as Route } /> @@ -1335,7 +1337,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_INTERNAL_NOTE + PageMap.SCHEDULED_MAINTENANCE_INTERNAL_NOTE ] as Route } /> @@ -1353,7 +1355,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_PUBLIC_NOTE + PageMap.SCHEDULED_MAINTENANCE_PUBLIC_NOTE ] as Route } /> @@ -1384,6 +1386,26 @@ const App: FunctionComponent = () => { } /> + + } + /> + + + } + /> + { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_MONITORS_STATUS + PageMap.SETTINGS_MONITORS_STATUS ] as Route } /> @@ -1433,7 +1455,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENTS_STATE + PageMap.SETTINGS_INCIDENTS_STATE ] as Route } /> @@ -1451,7 +1473,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_SCHEDULED_MAINTENANCE_STATE + PageMap.SETTINGS_SCHEDULED_MAINTENANCE_STATE ] as Route } /> @@ -1479,7 +1501,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENTS_SEVERITY + PageMap.SETTINGS_INCIDENTS_SEVERITY ] as Route } /> @@ -1549,7 +1571,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_MONITOR_CUSTOM_FIELDS + PageMap.SETTINGS_MONITOR_CUSTOM_FIELDS ] as Route } /> @@ -1567,7 +1589,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_STATUS_PAGE_CUSTOM_FIELDS + PageMap.SETTINGS_STATUS_PAGE_CUSTOM_FIELDS ] as Route } /> @@ -1585,8 +1607,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SETTINGS_SCHEDULED_MAINTENANCE_CUSTOM_FIELDS + PageMap + .SETTINGS_SCHEDULED_MAINTENANCE_CUSTOM_FIELDS ] as Route } /> @@ -1604,7 +1626,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENT_CUSTOM_FIELDS + PageMap.SETTINGS_INCIDENT_CUSTOM_FIELDS ] as Route } /> @@ -1634,7 +1656,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_BILLING_INVOICES + PageMap.SETTINGS_BILLING_INVOICES ] as Route } /> diff --git a/Dashboard/src/Pages/Settings/CallSms.tsx b/Dashboard/src/Pages/Settings/CallSms.tsx new file mode 100644 index 00000000000..f280ec887c4 --- /dev/null +++ b/Dashboard/src/Pages/Settings/CallSms.tsx @@ -0,0 +1,289 @@ +import Project from 'Model/Models/Project'; +import Route from 'Common/Types/API/Route'; +import IconProp from 'Common/Types/Icon/IconProp'; +import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; +import Page from 'CommonUI/src/Components/Page/Page'; +import React, { FunctionComponent, ReactElement } from 'react'; +import PageMap from '../../Utils/PageMap'; +import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; +import DashboardNavigation from '../../Utils/Navigation'; +import PageComponentProps from '../PageComponentProps'; +import DashboardSideMenu from './SideMenu'; +import FieldType from 'CommonUI/src/Components/Types/FieldType'; +import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; + +const Settings: FunctionComponent = ( + _props: PageComponentProps +): ReactElement => { + return ( + } + > + {/* API Key View */} + + + + + + + ); +}; + +export default Settings; diff --git a/Dashboard/src/Pages/Settings/SideMenu.tsx b/Dashboard/src/Pages/Settings/SideMenu.tsx index 099b73cef0a..c7bee6755ab 100644 --- a/Dashboard/src/Pages/Settings/SideMenu.tsx +++ b/Dashboard/src/Pages/Settings/SideMenu.tsx @@ -30,30 +30,9 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => { }} icon={IconProp.Label} /> - {/* */} + - {/* - - - */} + { icon={IconProp.Team} /> - + { }} icon={IconProp.Email} /> + + = ( + _props: PageComponentProps +): ReactElement => { + const [showVerificationModal, setShowVerificationModal] = + useState(false); + const [error, setError] = useState(''); + const [currentVerificationDomain, setCurrentVerificationDomain] = + useState(null); + const [refreshToggle, setRefreshToggle] = useState(false); + const [isVerificationLoading, setIsVerificationLoading] = + useState(false); + + useEffect(() => { + setError(''); + }, [showVerificationModal]); + + return ( + } + > + + modelType={Domain} + showViewIdButton={true} + name="Settings > Domain" + query={{ + projectId: DashboardNavigation.getProjectId()?.toString(), + }} + id="domains-table" + isDeleteable={true} + isEditable={false} + isCreateable={true} + cardProps={{ + icon: IconProp.Globe, + title: 'Domains', + description: + 'Please list the domains you own here. This will help you to connect them to Status Page.', + }} + refreshToggle={refreshToggle} + noItemsMessage={'No domains found.'} + viewPageRoute={Navigation.getCurrentRoute()} + actionButtons={[ + { + title: 'Verify Domain', + buttonStyleType: ButtonStyleType.SUCCESS_OUTLINE, + icon: IconProp.Check, + isVisible: (item: JSONObject): boolean => { + if (item['isVerified']) { + return false; + } + + return true; + }, + onClick: async ( + item: JSONObject, + onCompleteAction: Function, + onError: (err: Error) => void + ) => { + try { + setCurrentVerificationDomain(item); + setShowVerificationModal(true); + + onCompleteAction(); + } catch (err) { + onCompleteAction(); + onError(err as Error); + } + }, + }, + ]} + formFields={[ + { + field: { + domain: true, + }, + title: 'Domain', + fieldType: FormFieldSchemaType.Domain, + required: true, + placeholder: 'acme-inc.com', + validation: { + minLength: 2, + }, + }, + ]} + selectMoreFields={{ + domainVerificationText: true, + }} + showRefreshButton={true} + showFilterButton={true} + columns={[ + { + field: { + domain: true, + }, + title: 'Domain', + type: FieldType.Text, + isFilterable: true, + }, + { + field: { + isVerified: true, + }, + title: 'Verified', + type: FieldType.Boolean, + isFilterable: true, + }, + ]} + /> + {showVerificationModal && currentVerificationDomain ? ( + + + Please add TXT record to your domain. Details of + the TXT records are: + +
+
+ + Record Type: TXT + +
+ + Name: @ or{' '} + {currentVerificationDomain[ + 'domain' + ]?.toString()} + +
+ + Content: + {(currentVerificationDomain[ + 'domainVerificationText' + ] as string) || ''} + +
+
+ + Please note: Some domain changes might take 72 + hours to propogate. + + + } + submitButtonText={'Verify Domain'} + onClose={() => { + setShowVerificationModal(false); + setError(''); + }} + isLoading={isVerificationLoading} + onSubmit={async () => { + try { + setIsVerificationLoading(true); + setError(''); + // verify domain. + await ModelAPI.updateById( + Domain, + new ObjectID( + currentVerificationDomain['_id'] + ? currentVerificationDomain[ + '_id' + ].toString() + : '' + ), + { + isVerified: true, + }, + undefined + ); + setIsVerificationLoading(false); + setShowVerificationModal(false); + setRefreshToggle(!refreshToggle); + } catch (err) { + setError(API.getFriendlyMessage(err)); + setIsVerificationLoading(false); + } + }} + /> + ) : ( + <> + )} +
+ ); +}; + +export default Domains; diff --git a/Dashboard/src/Utils/PageMap.ts b/Dashboard/src/Utils/PageMap.ts index 21932b6f14c..a3e9b0191c3 100644 --- a/Dashboard/src/Utils/PageMap.ts +++ b/Dashboard/src/Utils/PageMap.ts @@ -140,6 +140,10 @@ enum PageMap { WORKFLOW_BUILDER = 'WORKFLOW_BUILDER', WORKFLOW_LOGS = 'WORKFLOW_LOGS', WORKFLOW_VARIABLES = 'WORKFLOW_VARIABLES', + + // SMS and Call + SETTINGS_CALL_SMS = 'SETTINGS_CALL_SMS', + SETTINGS_SMS_LOGS = 'SETTINGS_SMS_LOGS', } export default PageMap; diff --git a/Dashboard/src/Utils/RouteMap.ts b/Dashboard/src/Utils/RouteMap.ts index a5b3e100e00..11ece69513b 100644 --- a/Dashboard/src/Utils/RouteMap.ts +++ b/Dashboard/src/Utils/RouteMap.ts @@ -276,6 +276,16 @@ const RouteMap: Dictionary = { `/dashboard/${RouteParams.ProjectID}/settings/danger-zone` ), + //SMS and Call + [PageMap.SETTINGS_CALL_SMS]: new Route( + `/dashboard/${RouteParams.ProjectID}/settings/notification` + ), + + [PageMap.SETTINGS_SMS_LOGS]: new Route( + `/dashboard/${RouteParams.ProjectID}/settings/sms-logs` + ), + + //api keys. [PageMap.SETTINGS_APIKEYS]: new Route( `/dashboard/${RouteParams.ProjectID}/settings/api-keys` diff --git a/Model/Models/Project.ts b/Model/Models/Project.ts index 897524fba8a..9f4b746d9e6 100644 --- a/Model/Models/Project.ts +++ b/Model/Models/Project.ts @@ -347,53 +347,6 @@ export default class Model extends TenantModel { }) public deletedByUserId?: ObjectID = undefined; - @ColumnAccessControl({ - create: [], - read: [ - Permission.ProjectOwner, - Permission.ProjectAdmin, - Permission.ProjectMember, - Permission.CanReadProject, - Permission.UnAuthorizedSsoUser, - Permission.ProjectUser, - ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - Permission.CanEditProject, - ], - }) - @TableColumn({ - required: true, - type: TableColumnType.Boolean, - isDefaultValueColumn: true, - }) - @Column({ - type: ColumnType.Boolean, - nullable: false, - unique: false, - default: false, - }) - public alertsEnabled?: boolean = undefined; - - @ColumnAccessControl({ - create: [], - read: [Permission.ProjectOwner, Permission.CanManageProjectBilling], - update: [], - }) - @TableColumn({ - required: true, - type: TableColumnType.SmallPositiveNumber, - isDefaultValueColumn: true, - }) - @Column({ - type: ColumnType.SmallPositiveNumber, - nullable: false, - unique: false, - default: 0, - }) - public alertAccountBalance?: number = undefined; - @ColumnAccessControl({ create: [], read: [], @@ -573,9 +526,36 @@ export default class Model extends TenantModel { type: ColumnType.Number, nullable: false, unique: false, - default: 0, + default: 20, + }) + public autoRechargeSmsOrCallByBalanceInUSD?: number = undefined; + + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.CanReadProject, + Permission.UnAuthorizedSsoUser, + ], + update: [ + Permission.ProjectOwner, + Permission.CanManageProjectBilling, + ], + }) + @TableColumn({ + type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'Auto Recharge when current balance falls to', + description: 'Auto recharge is triggered when current balance falls to this amount in USD for SMS or Call', + }) + @Column({ + type: ColumnType.Number, + nullable: false, + unique: false, + default: 10, }) - public autoRechargeSmsOrCallCurrentBalanceInUSD?: number = undefined; + public autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD?: number = undefined; @ColumnAccessControl({ From aaa3b9af3e9f2414e09fd165530874b453ab0fef Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 19:26:42 +0100 Subject: [PATCH 04/27] make logs page work --- Dashboard/src/Pages/Settings/CallSms.tsx | 4 +- Dashboard/src/Pages/Settings/SmsLog.tsx | 299 +++++++++-------------- 2 files changed, 113 insertions(+), 190 deletions(-) diff --git a/Dashboard/src/Pages/Settings/CallSms.tsx b/Dashboard/src/Pages/Settings/CallSms.tsx index f280ec887c4..6ba317daaf9 100644 --- a/Dashboard/src/Pages/Settings/CallSms.tsx +++ b/Dashboard/src/Pages/Settings/CallSms.tsx @@ -26,13 +26,13 @@ const Settings: FunctionComponent = ( ), }, { - title: 'Project Settings', + title: 'Settings', to: RouteUtil.populateRouteParams( RouteMap[PageMap.SETTINGS] as Route ), }, { - title: 'Call & SMS Settings', + title: 'Call & SMS', to: RouteUtil.populateRouteParams( RouteMap[PageMap.SETTINGS_CALL_SMS] as Route ), diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index a5362264ece..6b76498de75 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -1,47 +1,28 @@ import Route from 'Common/Types/API/Route'; -import Page from 'CommonUI/src/Components/Page/Page'; -import React, { - FunctionComponent, - ReactElement, - useEffect, - useState, -} from 'react'; +import ModelPage from 'CommonUI/src/Components/Page/ModelPage'; +import React, { FunctionComponent, ReactElement, useState } from 'react'; import PageMap from '../../Utils/PageMap'; import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import PageComponentProps from '../PageComponentProps'; -import DashboardSideMenu from './SideMenu'; -import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import FieldType from 'CommonUI/src/Components/Types/FieldType'; -import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import IconProp from 'Common/Types/Icon/IconProp'; -import Domain from 'Model/Models/Domain'; -import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; -import { JSONObject } from 'Common/Types/JSON'; -import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI'; -import ObjectID from 'Common/Types/ObjectID'; -import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal'; +import SmsLog from 'Model/Models/WorkflowLog'; +import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import DashboardNavigation from '../../Utils/Navigation'; -import Navigation from 'CommonUI/src/Utils/Navigation'; -import API from 'CommonUI/src/Utils/API/API'; +import { JSONObject } from 'Common/Types/JSON'; +import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; +import Modal, { ModalWidth } from 'CommonUI/src/Components/Modal/Modal'; +import DashboardSideMenu from './SideMenu'; -const Domains: FunctionComponent = ( +const SMSLogs: FunctionComponent = ( _props: PageComponentProps ): ReactElement => { - const [showVerificationModal, setShowVerificationModal] = - useState(false); - const [error, setError] = useState(''); - const [currentVerificationDomain, setCurrentVerificationDomain] = - useState(null); - const [refreshToggle, setRefreshToggle] = useState(false); - const [isVerificationLoading, setIsVerificationLoading] = - useState(false); - useEffect(() => { - setError(''); - }, [showVerificationModal]); + const [showViewSmsTextModal, setShowViewSmsTextModal] = useState(false); + const [smsText, setSmsText] = useState(''); return ( - = ( ), }, { - title: 'Domains', + title: 'Call & SMS', to: RouteUtil.populateRouteParams( - RouteMap[PageMap.SETTINGS_DOMAINS] as Route + RouteMap[PageMap.SETTINGS_CALL_SMS] as Route ), }, ]} sideMenu={} > - - modelType={Domain} - showViewIdButton={true} - name="Settings > Domain" - query={{ - projectId: DashboardNavigation.getProjectId()?.toString(), - }} - id="domains-table" - isDeleteable={true} - isEditable={false} - isCreateable={true} - cardProps={{ - icon: IconProp.Globe, - title: 'Domains', - description: - 'Please list the domains you own here. This will help you to connect them to Status Page.', - }} - refreshToggle={refreshToggle} - noItemsMessage={'No domains found.'} - viewPageRoute={Navigation.getCurrentRoute()} - actionButtons={[ - { - title: 'Verify Domain', - buttonStyleType: ButtonStyleType.SUCCESS_OUTLINE, - icon: IconProp.Check, - isVisible: (item: JSONObject): boolean => { - if (item['isVerified']) { - return false; - } - - return true; - }, - onClick: async ( - item: JSONObject, - onCompleteAction: Function, - onError: (err: Error) => void - ) => { - try { - setCurrentVerificationDomain(item); - setShowVerificationModal(true); + <> + + modelType={SmsLog} + id="sms-logs-table" + isDeleteable={false} + isEditable={false} + isCreateable={false} + name="SMS Logs" + query={{ + projectId: + DashboardNavigation.getProjectId()?.toString(), + }} + selectMoreFields={{ + smsText: true, + }} + actionButtons={[ + { + title: 'View SMS Text', + buttonStyleType: ButtonStyleType.NORMAL, + icon: IconProp.List, + onClick: async ( + item: JSONObject, + onCompleteAction: Function + ) => { + setSmsText(item['smsText'] as string); + setShowViewSmsTextModal(true); onCompleteAction(); - } catch (err) { - onCompleteAction(); - onError(err as Error); - } + }, }, - }, - ]} - formFields={[ - { - field: { - domain: true, + ]} + isViewable={false} + cardProps={{ + icon: IconProp.Logs, + title: 'SMS Logs', + description: + 'Logs of all the SMS sent by this project in the last 30 days.', + }} + noItemsMessage={ + 'Looks like no SMS is sent by this project in the last 30 days.' + } + showRefreshButton={true} + showFilterButton={true} + columns={[ + { + field: { + _id: true, + }, + title: 'Log ID', + type: FieldType.Text, + isFilterable: true, }, - title: 'Domain', - fieldType: FormFieldSchemaType.Domain, - required: true, - placeholder: 'acme-inc.com', - validation: { - minLength: 2, + { + field: { + fromNumber: true, + }, + isFilterable: true, + + title: 'From Number', + type: FieldType.Phone, + }, - }, - ]} - selectMoreFields={{ - domainVerificationText: true, - }} - showRefreshButton={true} - showFilterButton={true} - columns={[ - { - field: { - domain: true, + { + field: { + toNumber: true, + }, + isFilterable: true, + + title: 'To Number', + type: FieldType.Phone, + }, - title: 'Domain', - type: FieldType.Text, - isFilterable: true, - }, - { - field: { - isVerified: true, + { + field: { + createdAt: true, + }, + title: 'Sent at', + type: FieldType.DateTime, + isFilterable: true, }, - title: 'Verified', - type: FieldType.Boolean, - isFilterable: true, - }, - ]} - /> - {showVerificationModal && currentVerificationDomain ? ( - - - Please add TXT record to your domain. Details of - the TXT records are: - -
-
- - Record Type: TXT - -
- - Name: @ or{' '} - {currentVerificationDomain[ - 'domain' - ]?.toString()} - -
- - Content: - {(currentVerificationDomain[ - 'domainVerificationText' - ] as string) || ''} - -
-
- - Please note: Some domain changes might take 72 - hours to propogate. - - - } - submitButtonText={'Verify Domain'} - onClose={() => { - setShowVerificationModal(false); - setError(''); - }} - isLoading={isVerificationLoading} - onSubmit={async () => { - try { - setIsVerificationLoading(true); - setError(''); - // verify domain. - await ModelAPI.updateById( - Domain, - new ObjectID( - currentVerificationDomain['_id'] - ? currentVerificationDomain[ - '_id' - ].toString() - : '' - ), - { - isVerified: true, - }, - undefined - ); - setIsVerificationLoading(false); - setShowVerificationModal(false); - setRefreshToggle(!refreshToggle); - } catch (err) { - setError(API.getFriendlyMessage(err)); - setIsVerificationLoading(false); - } - }} + ]} /> - ) : ( - <> - )} -
+ + {showViewSmsTextModal && ( + { + setShowViewSmsTextModal(false); + }} + submitButtonText={'Close'} + submitButtonStyleType={ButtonStyleType.NORMAL} + > +
+ +
{smsText}
; + +
+
+ )} + + ); }; -export default Domains; +export default SMSLogs; From 844611d1763f33c4497d8230e33c7c4233fc0c1f Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 19:38:11 +0100 Subject: [PATCH 05/27] add service --- CommonServer/Services/SmsLogService.ts | 11 +++++++++++ Dashboard/src/Pages/Settings/SmsLog.tsx | 8 ++++---- DashboardAPI/Index.ts | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 CommonServer/Services/SmsLogService.ts diff --git a/CommonServer/Services/SmsLogService.ts b/CommonServer/Services/SmsLogService.ts new file mode 100644 index 00000000000..9ec43b332b0 --- /dev/null +++ b/CommonServer/Services/SmsLogService.ts @@ -0,0 +1,11 @@ +import PostgresDatabase from '../Infrastructure/PostgresDatabase'; +import Model from 'Model/Models/SmsLog'; +import DatabaseService from './DatabaseService'; + +export class Service extends DatabaseService { + public constructor(postgresDatabase?: PostgresDatabase) { + super(Model, postgresDatabase); + } +} + +export default new Service(); diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index 6b76498de75..f5f40d7ad65 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -1,18 +1,18 @@ import Route from 'Common/Types/API/Route'; -import ModelPage from 'CommonUI/src/Components/Page/ModelPage'; import React, { FunctionComponent, ReactElement, useState } from 'react'; import PageMap from '../../Utils/PageMap'; import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import PageComponentProps from '../PageComponentProps'; import FieldType from 'CommonUI/src/Components/Types/FieldType'; import IconProp from 'Common/Types/Icon/IconProp'; -import SmsLog from 'Model/Models/WorkflowLog'; +import SmsLog from 'Model/Models/SmsLog'; import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import DashboardNavigation from '../../Utils/Navigation'; import { JSONObject } from 'Common/Types/JSON'; import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; import Modal, { ModalWidth } from 'CommonUI/src/Components/Modal/Modal'; import DashboardSideMenu from './SideMenu'; +import Page from 'CommonUI/src/Components/Page/Page'; const SMSLogs: FunctionComponent = ( _props: PageComponentProps @@ -22,7 +22,7 @@ const SMSLogs: FunctionComponent = ( const [smsText, setSmsText] = useState(''); return ( - = ( )} - + ); }; diff --git a/DashboardAPI/Index.ts b/DashboardAPI/Index.ts index bfe5822c685..aed02f5c6bd 100755 --- a/DashboardAPI/Index.ts +++ b/DashboardAPI/Index.ts @@ -75,6 +75,11 @@ import ProjectSSOService, { Service as ProjectSSOServiceType, } from 'CommonServer/Services/ProjectSsoService'; +import SmsLog from 'Model/Models/SmsLog'; +import SmsLogService, { + Service as SmsLogServiceType, +} from 'CommonServer/Services/SmsLogService'; + import StatusPageSSO from 'Model/Models/StatusPageSso'; import StatusPageSSOService, { Service as StatusPageSSOServiceType, @@ -603,6 +608,15 @@ app.use( ).getRouter() ); + +app.use( + `/${APP_NAME.toLocaleLowerCase()}`, + new BaseAPI( + SmsLog, + SmsLogService + ).getRouter() +); + app.use( `/${APP_NAME.toLocaleLowerCase()}`, new BaseAPI( From 08bd171b2fee287695a67147a3d635cc0292e20a Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 19:47:51 +0100 Subject: [PATCH 06/27] fix sms --- Dashboard/src/Pages/Settings/SmsLog.tsx | 76 +++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index f5f40d7ad65..219f07e0294 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -13,6 +13,9 @@ import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; import Modal, { ModalWidth } from 'CommonUI/src/Components/Modal/Modal'; import DashboardSideMenu from './SideMenu'; import Page from 'CommonUI/src/Components/Page/Page'; +import Pill from 'CommonUI/src/Components/Pill/Pill'; +import SmsStatus from 'Common/Types/SmsStatus'; +import { Green, Red } from 'Common/Types/BrandColors'; const SMSLogs: FunctionComponent = ( _props: PageComponentProps @@ -20,6 +23,8 @@ const SMSLogs: FunctionComponent = ( const [showViewSmsTextModal, setShowViewSmsTextModal] = useState(false); const [smsText, setSmsText] = useState(''); + const [smsModelTitle, setSmsModalTitle] = useState(''); + const [smsModelDescription, setSmsModalDescription] = useState(''); return ( = ( }} selectMoreFields={{ smsText: true, + errorMessage: true }} actionButtons={[ { @@ -71,6 +77,31 @@ const SMSLogs: FunctionComponent = ( onCompleteAction: Function ) => { setSmsText(item['smsText'] as string); + setSmsModalDescription('Contents of the SMS message'); + setSmsModalTitle('SMS Text'); + setShowViewSmsTextModal(true); + + onCompleteAction(); + }, + }, + { + title: 'View Error', + buttonStyleType: ButtonStyleType.NORMAL, + icon: IconProp.Error, + isVisible: (item: JSONObject) => { + if(item['status'] === SmsStatus.Error) { + return true; + } + + return false; + }, + onClick: async ( + item: JSONObject, + onCompleteAction: Function + ) => { + setSmsText(item['errorMessage'] as string); + setSmsModalDescription('Here is more information about the error.'); + setSmsModalTitle('Error'); setShowViewSmsTextModal(true); onCompleteAction(); @@ -106,7 +137,7 @@ const SMSLogs: FunctionComponent = ( title: 'From Number', type: FieldType.Phone, - + }, { field: { @@ -116,7 +147,7 @@ const SMSLogs: FunctionComponent = ( title: 'To Number', type: FieldType.Phone, - + }, { field: { @@ -126,13 +157,46 @@ const SMSLogs: FunctionComponent = ( type: FieldType.DateTime, isFilterable: true, }, + { + field: { + status: true, + }, + title: 'Status', + type: FieldType.Text, + getElement: (item: JSONObject): ReactElement => { + if (item['status']) { + return ( + + ); + } + + return <>; + }, + isFilterable: true, + }, ]} /> {showViewSmsTextModal && ( { @@ -142,9 +206,9 @@ const SMSLogs: FunctionComponent = ( submitButtonStyleType={ButtonStyleType.NORMAL} >
- +
{smsText}
; - +
)} From 82849c28ce1e8ad20109b401ddd6eba6e5bf3ff7 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 7 Jun 2023 19:51:41 +0100 Subject: [PATCH 07/27] fix lint --- Common/Types/Permission.ts | 8 +- Common/Types/SmsStatus.ts | 6 +- Dashboard/src/App.tsx | 114 +++++++++++----------- Dashboard/src/Pages/Settings/CallSms.tsx | 98 ++++++++++--------- Dashboard/src/Pages/Settings/SideMenu.tsx | 1 - Dashboard/src/Pages/Settings/SmsLog.tsx | 37 +++---- Dashboard/src/Utils/RouteMap.ts | 1 - DashboardAPI/Index.ts | 6 +- Model/Models/Index.ts | 2 +- Model/Models/Project.ts | 52 ++++------ Model/Models/SmsLog.ts | 65 +++--------- 11 files changed, 170 insertions(+), 220 deletions(-) diff --git a/Common/Types/Permission.ts b/Common/Types/Permission.ts index a2fabcd7178..36bd6530b33 100644 --- a/Common/Types/Permission.ts +++ b/Common/Types/Permission.ts @@ -73,7 +73,6 @@ enum Permission { CanEditMonitorProbe = 'CanEditMonitorProbe', CanReadMonitorProbe = 'CanReadMonitorProbe', - CanCreateSmsLog = 'CanCreateSmsLog', CanDeleteSmsLog = 'CanDeleteSmsLog', CanEditSmsLog = 'CanEditSmsLog', @@ -1674,13 +1673,10 @@ export class PermissionHelper { isAccessControlPermission: false, }, - - { permission: Permission.CanCreateSmsLog, title: 'Can Create SMS Log', - description: - 'This permission can create SMS Log this project.', + description: 'This permission can create SMS Log this project.', isAssignableToTenant: true, isAccessControlPermission: false, }, @@ -1709,8 +1705,6 @@ export class PermissionHelper { isAccessControlPermission: false, }, - - { permission: Permission.CanCreateMonitorProbe, title: 'Can Create Monitor Probe', diff --git a/Common/Types/SmsStatus.ts b/Common/Types/SmsStatus.ts index 9c074dee948..2ba384a4b6e 100644 --- a/Common/Types/SmsStatus.ts +++ b/Common/Types/SmsStatus.ts @@ -1,6 +1,6 @@ enum SmsStatus { - Success = "Success", - Error = "Error", + Success = 'Success', + Error = 'Error', } -export default SmsStatus; \ No newline at end of file +export default SmsStatus; diff --git a/Dashboard/src/App.tsx b/Dashboard/src/App.tsx index a1f2bf43a25..27819142770 100644 --- a/Dashboard/src/App.tsx +++ b/Dashboard/src/App.tsx @@ -350,7 +350,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.HOME_NOT_OPERATIONAL_MONITORS + PageMap.HOME_NOT_OPERATIONAL_MONITORS ] as Route } /> @@ -370,8 +370,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS + PageMap + .HOME_ONGOING_SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -399,7 +399,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITORS_INOPERATIONAL + PageMap.MONITORS_INOPERATIONAL ] as Route } /> @@ -455,7 +455,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_STATUS_TIMELINE + PageMap.MONITOR_VIEW_STATUS_TIMELINE ] as Route } /> @@ -487,7 +487,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_INCIDENTS + PageMap.MONITOR_VIEW_INCIDENTS ] as Route } /> @@ -505,7 +505,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.MONITOR_VIEW_CUSTOM_FIELDS + PageMap.MONITOR_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -685,7 +685,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_DELETE + PageMap.STATUS_PAGE_VIEW_DELETE ] as Route } /> @@ -703,7 +703,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_BRANDING + PageMap.STATUS_PAGE_VIEW_BRANDING ] as Route } /> @@ -721,7 +721,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_HTML_CSS + PageMap.STATUS_PAGE_VIEW_CUSTOM_HTML_CSS ] as Route } /> @@ -739,7 +739,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_ADVANCED_OPTIONS + PageMap.STATUS_PAGE_VIEW_ADVANCED_OPTIONS ] as Route } /> @@ -757,7 +757,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_FIELDS + PageMap.STATUS_PAGE_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -774,7 +774,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_OWNERS + PageMap.STATUS_PAGE_VIEW_OWNERS ] as Route } /> @@ -806,7 +806,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS ] as Route } /> @@ -824,8 +824,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .STATUS_PAGE_VIEW_AUTHENTICATION_SETTINGS + PageMap + .STATUS_PAGE_VIEW_AUTHENTICATION_SETTINGS ] as Route } /> @@ -843,7 +843,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_CUSTOM_SMTP + PageMap.STATUS_PAGE_VIEW_CUSTOM_SMTP ] as Route } /> @@ -861,7 +861,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_SETTINGS + PageMap.STATUS_PAGE_VIEW_SETTINGS ] as Route } /> @@ -879,7 +879,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_PRIVATE_USERS + PageMap.STATUS_PAGE_VIEW_PRIVATE_USERS ] as Route } /> @@ -897,7 +897,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS ] as Route } /> @@ -915,7 +915,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_HEADER_STYLE + PageMap.STATUS_PAGE_VIEW_HEADER_STYLE ] as Route } /> @@ -933,7 +933,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE + PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE ] as Route } /> @@ -951,7 +951,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_NAVBAR_STYLE + PageMap.STATUS_PAGE_VIEW_NAVBAR_STYLE ] as Route } /> @@ -969,7 +969,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS + PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS ] as Route } /> @@ -987,7 +987,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_EMBEDDED + PageMap.STATUS_PAGE_VIEW_EMBEDDED ] as Route } /> @@ -1005,7 +1005,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_RESOURCES + PageMap.STATUS_PAGE_VIEW_RESOURCES ] as Route } /> @@ -1023,7 +1023,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_DOMAINS + PageMap.STATUS_PAGE_VIEW_DOMAINS ] as Route } /> @@ -1040,7 +1040,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_GROUPS + PageMap.STATUS_PAGE_VIEW_GROUPS ] as Route } /> @@ -1058,7 +1058,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.STATUS_PAGE_VIEW_ANNOUNCEMENTS + PageMap.STATUS_PAGE_VIEW_ANNOUNCEMENTS ] as Route } /> @@ -1126,7 +1126,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_VIEW_STATE_TIMELINE + PageMap.INCIDENT_VIEW_STATE_TIMELINE ] as Route } /> @@ -1143,7 +1143,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_INTERNAL_NOTE + PageMap.INCIDENT_INTERNAL_NOTE ] as Route } /> @@ -1161,7 +1161,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.INCIDENT_VIEW_CUSTOM_FIELDS + PageMap.INCIDENT_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -1209,7 +1209,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_EVENTS + PageMap.SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -1227,7 +1227,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.ONGOING_SCHEDULED_MAINTENANCE_EVENTS + PageMap.ONGOING_SCHEDULED_MAINTENANCE_EVENTS ] as Route } /> @@ -1245,7 +1245,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW + PageMap.SCHEDULED_MAINTENANCE_VIEW ] as Route } /> @@ -1263,8 +1263,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SCHEDULED_MAINTENANCE_VIEW_CUSTOM_FIELDS + PageMap + .SCHEDULED_MAINTENANCE_VIEW_CUSTOM_FIELDS ] as Route } /> @@ -1282,7 +1282,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW_DELETE + PageMap.SCHEDULED_MAINTENANCE_VIEW_DELETE ] as Route } /> @@ -1300,7 +1300,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_VIEW_OWNERS + PageMap.SCHEDULED_MAINTENANCE_VIEW_OWNERS ] as Route } /> @@ -1318,8 +1318,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SCHEDULED_MAINTENANCE_VIEW_STATE_TIMELINE + PageMap + .SCHEDULED_MAINTENANCE_VIEW_STATE_TIMELINE ] as Route } /> @@ -1337,7 +1337,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_INTERNAL_NOTE + PageMap.SCHEDULED_MAINTENANCE_INTERNAL_NOTE ] as Route } /> @@ -1355,7 +1355,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SCHEDULED_MAINTENANCE_PUBLIC_NOTE + PageMap.SCHEDULED_MAINTENANCE_PUBLIC_NOTE ] as Route } /> @@ -1391,7 +1391,9 @@ const App: FunctionComponent = () => { element={ } /> @@ -1401,7 +1403,9 @@ const App: FunctionComponent = () => { element={ } /> @@ -1437,7 +1441,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_MONITORS_STATUS + PageMap.SETTINGS_MONITORS_STATUS ] as Route } /> @@ -1455,7 +1459,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENTS_STATE + PageMap.SETTINGS_INCIDENTS_STATE ] as Route } /> @@ -1473,7 +1477,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_SCHEDULED_MAINTENANCE_STATE + PageMap.SETTINGS_SCHEDULED_MAINTENANCE_STATE ] as Route } /> @@ -1501,7 +1505,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENTS_SEVERITY + PageMap.SETTINGS_INCIDENTS_SEVERITY ] as Route } /> @@ -1571,7 +1575,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_MONITOR_CUSTOM_FIELDS + PageMap.SETTINGS_MONITOR_CUSTOM_FIELDS ] as Route } /> @@ -1589,7 +1593,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_STATUS_PAGE_CUSTOM_FIELDS + PageMap.SETTINGS_STATUS_PAGE_CUSTOM_FIELDS ] as Route } /> @@ -1607,8 +1611,8 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap - .SETTINGS_SCHEDULED_MAINTENANCE_CUSTOM_FIELDS + PageMap + .SETTINGS_SCHEDULED_MAINTENANCE_CUSTOM_FIELDS ] as Route } /> @@ -1626,7 +1630,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_INCIDENT_CUSTOM_FIELDS + PageMap.SETTINGS_INCIDENT_CUSTOM_FIELDS ] as Route } /> @@ -1656,7 +1660,7 @@ const App: FunctionComponent = () => { {...commonPageProps} pageRoute={ RouteMap[ - PageMap.SETTINGS_BILLING_INVOICES + PageMap.SETTINGS_BILLING_INVOICES ] as Route } /> diff --git a/Dashboard/src/Pages/Settings/CallSms.tsx b/Dashboard/src/Pages/Settings/CallSms.tsx index 6ba317daaf9..7c8ad15a28b 100644 --- a/Dashboard/src/Pages/Settings/CallSms.tsx +++ b/Dashboard/src/Pages/Settings/CallSms.tsx @@ -45,11 +45,11 @@ const Settings: FunctionComponent = ( name="Current Balance" cardProps={{ title: 'Current Balance', - description: "Here is your current call and SMS balance for this project.", + description: + 'Here is your current call and SMS balance for this project.', icon: IconProp.Billing, }} isEditable={false} - modelDetailProps={{ modelType: Project, id: 'current-balance', @@ -60,8 +60,9 @@ const Settings: FunctionComponent = ( }, fieldType: FieldType.Number, title: 'SMS or Call Current Balance', - description: 'This is your current balance for SMS or Call. It is in USD. ', - placeholder: "0 USD", + description: + 'This is your current balance for SMS or Call. It is in USD. ', + placeholder: '0 USD', }, ], modelId: DashboardNavigation.getProjectId()?.toString(), @@ -72,7 +73,8 @@ const Settings: FunctionComponent = ( name="Enable Notifications" cardProps={{ title: 'Enable Notifications', - description: "Enable Call and SMS notifications for this project.", + description: + 'Enable Call and SMS notifications for this project.', icon: IconProp.Notification, }} isEditable={true} @@ -83,7 +85,8 @@ const Settings: FunctionComponent = ( enableCallNotifications: true, }, title: 'Enable Call Notifications', - description: 'Enable Call notifications for this project. This will be used for alerting users by phone call.', + description: + 'Enable Call notifications for this project. This will be used for alerting users by phone call.', fieldType: FormFieldSchemaType.Toggle, required: false, }, @@ -92,7 +95,8 @@ const Settings: FunctionComponent = ( enableSmsNotifications: true, }, title: 'Enable SMS Notifications', - description: 'Enable SMS notifications for this project. This will be used for alerting users by sending an SMS.', + description: + 'Enable SMS notifications for this project. This will be used for alerting users by sending an SMS.', fieldType: FormFieldSchemaType.Toggle, required: false, }, @@ -107,8 +111,9 @@ const Settings: FunctionComponent = ( }, fieldType: FieldType.Boolean, title: 'Enable Call Notifications', - placeholder: "Not Enabled", - description: 'Enable Call notifications for this project. This will be used for alerting users by phone call.', + placeholder: 'Not Enabled', + description: + 'Enable Call notifications for this project. This will be used for alerting users by phone call.', }, { field: { @@ -116,8 +121,9 @@ const Settings: FunctionComponent = ( }, fieldType: FieldType.Boolean, title: 'Enable SMS Notifications', - placeholder: "Not Enabled", - description: 'Enable SMS notifications for this project. This will be used for alerting users by SMS.', + placeholder: 'Not Enabled', + description: + 'Enable SMS notifications for this project. This will be used for alerting users by SMS.', }, ], modelId: DashboardNavigation.getProjectId()?.toString(), @@ -128,7 +134,8 @@ const Settings: FunctionComponent = ( name="Auto Recharge" cardProps={{ title: 'Auto Recharge', - description: "Enable Auto Recharge for call and SMS balance. This will make sure you always have enough balance for sending SMS or making calls.", + description: + 'Enable Auto Recharge for call and SMS balance. This will make sure you always have enough balance for sending SMS or making calls.', icon: IconProp.Billing, }} isEditable={true} @@ -139,7 +146,8 @@ const Settings: FunctionComponent = ( enableAutoRechargeSmsOrCallBalance: true, }, title: 'Enable Auto Recharge', - description: 'Enable Auto Recharge. This will be used for sending an SMS or Call.', + description: + 'Enable Auto Recharge. This will be used for sending an SMS or Call.', fieldType: FormFieldSchemaType.Toggle, required: false, }, @@ -148,102 +156,103 @@ const Settings: FunctionComponent = ( autoRechargeSmsOrCallByBalanceInUSD: true, }, title: 'Auto Recharge Balance by (in USD)', - description: 'Amount of balance to be recharged when the balance is low. It is in USD. ', + description: + 'Amount of balance to be recharged when the balance is low. It is in USD. ', fieldType: FormFieldSchemaType.Dropdown, dropdownOptions: [ { value: 10, - label: "10 USD", + label: '10 USD', }, { value: 20, - label: "20 USD", + label: '20 USD', }, { value: 25, - label: "25 USD", + label: '25 USD', }, { value: 50, - label: "50 USD", + label: '50 USD', }, { value: 75, - label: "75 USD", + label: '75 USD', }, { value: 100, - label: "100 USD", + label: '100 USD', }, { value: 200, - label: "200 USD", + label: '200 USD', }, { value: 500, - label: "500 USD", + label: '500 USD', }, { value: 500, - label: "500 USD", + label: '500 USD', }, { value: 1000, - label: "1000 USD", + label: '1000 USD', }, - ], required: true, }, { field: { - autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: + true, }, title: 'Auto Recharge when balance falls to (in USD)', - description: 'Trigger auto recharge when balance falls to this amount. It is in USD. ', + description: + 'Trigger auto recharge when balance falls to this amount. It is in USD. ', fieldType: FormFieldSchemaType.Dropdown, dropdownOptions: [ { value: 10, - label: "10 USD", + label: '10 USD', }, { value: 20, - label: "20 USD", + label: '20 USD', }, { value: 25, - label: "25 USD", + label: '25 USD', }, { value: 50, - label: "50 USD", + label: '50 USD', }, { value: 75, - label: "75 USD", + label: '75 USD', }, { value: 100, - label: "100 USD", + label: '100 USD', }, { value: 200, - label: "200 USD", + label: '200 USD', }, { value: 500, - label: "500 USD", + label: '500 USD', }, { value: 500, - label: "500 USD", + label: '500 USD', }, { value: 1000, - label: "1000 USD", + label: '1000 USD', }, - ], required: true, }, @@ -258,9 +267,9 @@ const Settings: FunctionComponent = ( }, fieldType: FieldType.Boolean, title: 'Auto Recharge Balance by (in USD)', - description: 'Amount of balance to be recharged when the balance is low. It is in USD. ', - placeholder: "Not Enabled", - + description: + 'Amount of balance to be recharged when the balance is low. It is in USD. ', + placeholder: 'Not Enabled', }, { field: { @@ -268,15 +277,16 @@ const Settings: FunctionComponent = ( }, fieldType: FieldType.Text, title: 'Auto Recharge by (in USD)', - placeholder: "0 USD", + placeholder: '0 USD', }, { field: { - autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: + true, }, fieldType: FieldType.Text, title: 'Trigger auto recharge if balance falls below (in USD)', - placeholder: "0 USD", + placeholder: '0 USD', }, ], modelId: DashboardNavigation.getProjectId()?.toString(), diff --git a/Dashboard/src/Pages/Settings/SideMenu.tsx b/Dashboard/src/Pages/Settings/SideMenu.tsx index c7bee6755ab..9879d8dfaa6 100644 --- a/Dashboard/src/Pages/Settings/SideMenu.tsx +++ b/Dashboard/src/Pages/Settings/SideMenu.tsx @@ -30,7 +30,6 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => { }} icon={IconProp.Label} /> -
diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index 219f07e0294..fbd4ba2247b 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -20,8 +20,8 @@ import { Green, Red } from 'Common/Types/BrandColors'; const SMSLogs: FunctionComponent = ( _props: PageComponentProps ): ReactElement => { - - const [showViewSmsTextModal, setShowViewSmsTextModal] = useState(false); + const [showViewSmsTextModal, setShowViewSmsTextModal] = + useState(false); const [smsText, setSmsText] = useState(''); const [smsModelTitle, setSmsModalTitle] = useState(''); const [smsModelDescription, setSmsModalDescription] = useState(''); @@ -65,7 +65,7 @@ const SMSLogs: FunctionComponent = ( }} selectMoreFields={{ smsText: true, - errorMessage: true + errorMessage: true, }} actionButtons={[ { @@ -77,7 +77,9 @@ const SMSLogs: FunctionComponent = ( onCompleteAction: Function ) => { setSmsText(item['smsText'] as string); - setSmsModalDescription('Contents of the SMS message'); + setSmsModalDescription( + 'Contents of the SMS message' + ); setSmsModalTitle('SMS Text'); setShowViewSmsTextModal(true); @@ -89,7 +91,7 @@ const SMSLogs: FunctionComponent = ( buttonStyleType: ButtonStyleType.NORMAL, icon: IconProp.Error, isVisible: (item: JSONObject) => { - if(item['status'] === SmsStatus.Error) { + if (item['status'] === SmsStatus.Error) { return true; } @@ -100,7 +102,9 @@ const SMSLogs: FunctionComponent = ( onCompleteAction: Function ) => { setSmsText(item['errorMessage'] as string); - setSmsModalDescription('Here is more information about the error.'); + setSmsModalDescription( + 'Here is more information about the error.' + ); setSmsModalTitle('Error'); setShowViewSmsTextModal(true); @@ -137,7 +141,6 @@ const SMSLogs: FunctionComponent = ( title: 'From Number', type: FieldType.Phone, - }, { field: { @@ -147,7 +150,6 @@ const SMSLogs: FunctionComponent = ( title: 'To Number', type: FieldType.Phone, - }, { field: { @@ -169,19 +171,12 @@ const SMSLogs: FunctionComponent = ( ); } @@ -206,9 +201,7 @@ const SMSLogs: FunctionComponent = ( submitButtonStyleType={ButtonStyleType.NORMAL} >
-
{smsText}
; -
)} diff --git a/Dashboard/src/Utils/RouteMap.ts b/Dashboard/src/Utils/RouteMap.ts index 11ece69513b..70cedc4ca5f 100644 --- a/Dashboard/src/Utils/RouteMap.ts +++ b/Dashboard/src/Utils/RouteMap.ts @@ -285,7 +285,6 @@ const RouteMap: Dictionary = { `/dashboard/${RouteParams.ProjectID}/settings/sms-logs` ), - //api keys. [PageMap.SETTINGS_APIKEYS]: new Route( `/dashboard/${RouteParams.ProjectID}/settings/api-keys` diff --git a/DashboardAPI/Index.ts b/DashboardAPI/Index.ts index aed02f5c6bd..3b8e98311d0 100755 --- a/DashboardAPI/Index.ts +++ b/DashboardAPI/Index.ts @@ -608,13 +608,9 @@ app.use( ).getRouter() ); - app.use( `/${APP_NAME.toLocaleLowerCase()}`, - new BaseAPI( - SmsLog, - SmsLogService - ).getRouter() + new BaseAPI(SmsLog, SmsLogService).getRouter() ); app.use( diff --git a/Model/Models/Index.ts b/Model/Models/Index.ts index 2fa92e6987c..9156980fb50 100644 --- a/Model/Models/Index.ts +++ b/Model/Models/Index.ts @@ -160,5 +160,5 @@ export default [ StatusPageOwnerTeam, StatusPageOwnerUser, - SmsLog + SmsLog, ]; diff --git a/Model/Models/Project.ts b/Model/Models/Project.ts index 9f4b746d9e6..22f381abf45 100644 --- a/Model/Models/Project.ts +++ b/Model/Models/Project.ts @@ -479,7 +479,6 @@ export default class Model extends TenantModel { }) public currentActiveMonitorsCount?: number = undefined; - @ColumnAccessControl({ create: [], read: [ @@ -492,7 +491,10 @@ export default class Model extends TenantModel { update: [], }) @TableColumn({ - type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'SMS or Call Current Balance', + type: TableColumnType.Number, + isDefaultValueColumn: true, + required: true, + title: 'SMS or Call Current Balance', description: 'Balance in USD for SMS or Call', }) @Column({ @@ -503,7 +505,6 @@ export default class Model extends TenantModel { }) public smsOrCallCurrentBalanceInUSD?: number = undefined; - @ColumnAccessControl({ create: [], read: [ @@ -513,13 +514,13 @@ export default class Model extends TenantModel { Permission.CanReadProject, Permission.UnAuthorizedSsoUser, ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - ], + update: [Permission.ProjectOwner, Permission.CanManageProjectBilling], }) @TableColumn({ - type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'Auto Recharge Amount', + type: TableColumnType.Number, + isDefaultValueColumn: true, + required: true, + title: 'Auto Recharge Amount', description: 'Auto recharge amount in USD for SMS or Call', }) @Column({ @@ -530,7 +531,6 @@ export default class Model extends TenantModel { }) public autoRechargeSmsOrCallByBalanceInUSD?: number = undefined; - @ColumnAccessControl({ create: [], read: [ @@ -540,14 +540,15 @@ export default class Model extends TenantModel { Permission.CanReadProject, Permission.UnAuthorizedSsoUser, ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - ], + update: [Permission.ProjectOwner, Permission.CanManageProjectBilling], }) @TableColumn({ - type: TableColumnType.Number, isDefaultValueColumn: true, required: true, title: 'Auto Recharge when current balance falls to', - description: 'Auto recharge is triggered when current balance falls to this amount in USD for SMS or Call', + type: TableColumnType.Number, + isDefaultValueColumn: true, + required: true, + title: 'Auto Recharge when current balance falls to', + description: + 'Auto recharge is triggered when current balance falls to this amount in USD for SMS or Call', }) @Column({ type: ColumnType.Number, @@ -557,7 +558,6 @@ export default class Model extends TenantModel { }) public autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD?: number = undefined; - @ColumnAccessControl({ create: [], read: [ @@ -568,10 +568,7 @@ export default class Model extends TenantModel { Permission.UnAuthorizedSsoUser, Permission.ProjectUser, ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - ], + update: [Permission.ProjectOwner, Permission.CanManageProjectBilling], }) @TableColumn({ required: true, @@ -587,7 +584,6 @@ export default class Model extends TenantModel { }) public enableSmsNotifications?: boolean = undefined; - @ColumnAccessControl({ create: [], read: [ @@ -598,10 +594,7 @@ export default class Model extends TenantModel { Permission.UnAuthorizedSsoUser, Permission.ProjectUser, ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - ], + update: [Permission.ProjectOwner, Permission.CanManageProjectBilling], }) @TableColumn({ required: true, @@ -627,17 +620,15 @@ export default class Model extends TenantModel { Permission.UnAuthorizedSsoUser, Permission.ProjectUser, ], - update: [ - Permission.ProjectOwner, - Permission.CanManageProjectBilling, - ], + update: [Permission.ProjectOwner, Permission.CanManageProjectBilling], }) @TableColumn({ required: true, isDefaultValueColumn: true, type: TableColumnType.Boolean, title: 'Enable auto recharge SMS or Call balance', - description: 'Enable auto recharge SMS or Call balance for this project.', + description: + 'Enable auto recharge SMS or Call balance for this project.', }) @Column({ nullable: false, @@ -645,5 +636,4 @@ export default class Model extends TenantModel { type: ColumnType.Boolean, }) public enableAutoRechargeSmsOrCallBalance?: boolean = undefined; - } diff --git a/Model/Models/SmsLog.ts b/Model/Models/SmsLog.ts index 914bc48c6a2..e57170b87fc 100644 --- a/Model/Models/SmsLog.ts +++ b/Model/Models/SmsLog.ts @@ -22,21 +22,15 @@ import Phone from 'Common/Types/Phone'; @EnableDocumentation() @TenantColumn('projectId') @TableAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - delete: [ - - ], - update: [ - - ], + delete: [], + update: [], }) @CrudApiEndpoint(new Route('/sms-log')) @Entity({ @@ -124,18 +118,14 @@ export default class SmsLog extends BaseModel { public projectId?: ObjectID = undefined; @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @Index() @TableColumn({ @@ -152,20 +142,15 @@ export default class SmsLog extends BaseModel { }) public toNumber?: Phone = undefined; - @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @Index() @TableColumn({ @@ -182,20 +167,15 @@ export default class SmsLog extends BaseModel { }) public fromNumber?: Phone = undefined; - @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @TableColumn({ required: true, @@ -211,20 +191,15 @@ export default class SmsLog extends BaseModel { }) public smsText?: string = undefined; - @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @TableColumn({ required: false, @@ -240,20 +215,15 @@ export default class SmsLog extends BaseModel { }) public errorMessage?: string = undefined; - @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @TableColumn({ required: true, @@ -270,18 +240,14 @@ export default class SmsLog extends BaseModel { public status?: SmsStatus = undefined; @ColumnAccessControl({ - create: [ - - ], + create: [], read: [ Permission.ProjectOwner, Permission.ProjectAdmin, Permission.ProjectMember, Permission.CanReadSmsLog, ], - update: [ - - ], + update: [], }) @TableColumn({ required: true, @@ -295,5 +261,4 @@ export default class SmsLog extends BaseModel { type: ColumnType.Number, }) public smsCostInUSDCents?: number = undefined; - } From 9a1e27db5c38e154345a86dd69d5d249f00c3499 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 11:58:58 +0100 Subject: [PATCH 08/27] send sms --- Model/Models/Project.ts | 2 +- Notification/.env.tpl | 4 + Notification/Config.ts | 5 + Notification/Services/SmsService.ts | 116 ++++++ Notification/package-lock.json | 575 +++++++++++++++++++++++++++- Notification/package.json | 3 +- config.tpl.env | 7 + 7 files changed, 703 insertions(+), 9 deletions(-) diff --git a/Model/Models/Project.ts b/Model/Models/Project.ts index 22f381abf45..e33615abf1c 100644 --- a/Model/Models/Project.ts +++ b/Model/Models/Project.ts @@ -503,7 +503,7 @@ export default class Model extends TenantModel { unique: false, default: 0, }) - public smsOrCallCurrentBalanceInUSD?: number = undefined; + public smsOrCallCurrentBalanceInUSDCents?: number = undefined; @ColumnAccessControl({ create: [], diff --git a/Notification/.env.tpl b/Notification/.env.tpl index e37a03a497d..f548d55b372 100644 --- a/Notification/.env.tpl +++ b/Notification/.env.tpl @@ -7,3 +7,7 @@ SMTP_FROM_NAME={{ .Env.SMTP_FROM_NAME }} SMTP_IS_SECURE={{ .Env.SMTP_IS_SECURE }} SMTP_HOST={{ .Env.SMTP_HOST }} SENDGRID_API_KEY={{ .Env.SENDGRID_API_KEY }} +TWILIO_ACCOUNT_SID={{ .Env.TWILIO_ACCOUNT_SID }} +TWILIO_AUTH_TOKEN={{ .Env.TWILIO_AUTH_TOKEN }} +TWILIO_PHONE_NUMBER={{ .Env.TWILIO_PHONE_NUMBER }} +SMS_DEFAULT_COST_IN_CENTS={{ .Env.SMS_DEFAULT_COST_IN_CENTS }} \ No newline at end of file diff --git a/Notification/Config.ts b/Notification/Config.ts index 1f51eaaf99d..5df7eb98ff7 100644 --- a/Notification/Config.ts +++ b/Notification/Config.ts @@ -26,4 +26,9 @@ export const InternalSmtpFromEmail: Email = new Email( export const InternalSmtpFromName: string = process.env['INTERNAL_SMTP_NAME'] || ''; +export const TwilioAccountSid: string = process.env['TWILIO_ACCOUNT_SID'] || ''; +export const TwilioAuthToken: string = process.env['TWILIO_AUTH_TOKEN'] || ''; +export const TwilioPhoneNumber: string = process.env['TWILIO_PHONE_NUMBER'] || ''; +export const SMSDefaultCostInCents: number = process.env['SMS_DEFAULT_COST_IN_CENTS'] ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) : 0; + export const SendGridApiKey: string = process.env['SENDGRID_API_KEY'] || ''; diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index e69de29bb2d..eae646743fd 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -0,0 +1,116 @@ +import ObjectID from "Common/Types/ObjectID"; +import Phone from "Common/Types/Phone"; +import { SMSDefaultCostInCents, TwilioAccountSid, TwilioAuthToken, TwilioPhoneNumber } from "../Config"; +import Twilio from 'twilio'; +import BadDataException from "Common/Types/Exception/BadDataException"; +import SmsLog from "Model/Models/SmsLog"; +import SmsStatus from "Common/Types/SmsStatus"; +import { IsBillingEnabled } from "CommonServer/Config"; +import SmsLogService from "CommonServer/Services/SmsLogService" +import ProjectService from "CommonServer/Services/ProjectService"; +import Project from "Model/Models/Project"; + +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 { + + if (!TwilioAccountSid) { + throw new BadDataException('TwilioAccountSid is not configured'); + } + + if (!TwilioAuthToken) { + throw new BadDataException('TwilioAuthToken is not configured'); + } + + + if (!TwilioPhoneNumber) { + throw new BadDataException('TwilioPhoneNumber is not configured'); + } + + const client: Twilio.Twilio = Twilio(TwilioAccountSid, TwilioAuthToken); + + const smsLog: SmsLog = new SmsLog(); + smsLog.toNumber = to; + smsLog.fromNumber = options.from; + smsLog.smsText = message; + + if (options.projectId) { + smsLog.projectId = options.projectId; + } + + let project: Project | null = null ; + + try { + + // make sure project has enough balance. + + if (options.projectId && IsBillingEnabled) { + project = await ProjectService.findOneById({ + id: options.projectId, + select: { + smsOrCallCurrentBalanceInUSDCents: true, + enableAutoRechargeSmsOrCallBalance: true + }, + props: { + isRoot: true + } + }); + + if (!project) { + throw new BadDataException(`Project ${options.projectId.toString()} not found.`); + } + + if (!project.smsOrCallCurrentBalanceInUSDCents) { + throw new BadDataException(`Project ${options.projectId.toString()} does not have enough SMS balance.`); + } + + if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) { + throw new BadDataException(`Project does not have enough balance to send SMS. Current balance is ${project.smsOrCallCurrentBalanceInUSDCents} cents. Required balance is ${SMSDefaultCostInCents} cents to send this SMS.`); + } + + } + + await client.messages + .create({ + body: message, + to: to.toString(), + from: options && options.from ? options.from.toString() : TwilioPhoneNumber.toString(), // From a valid Twilio number + }); + + + smsLog.status = SmsStatus.Success; + smsLog.errorMessage = ""; + + if (IsBillingEnabled && project) { + smsLog.smsCostInUSDCents = SMSDefaultCostInCents; + + project.smsOrCallCurrentBalanceInUSDCents = Math.floor(project.smsOrCallCurrentBalanceInUSDCents! - SMSDefaultCostInCents); + + await ProjectService.updateOneById({ + data: { + smsOrCallCurrentBalanceInUSDCents: project.smsOrCallCurrentBalanceInUSDCents + }, + id: project.id!, + props: { + isRoot: true + } + }); + } + + } catch (e: any) { + smsLog.status = SmsStatus.Error; + smsLog.errorMessage = e && e.message ? e.message.toString() : e.toString(); + } + + if (options.projectId) { + await SmsLogService.create({ + data: smsLog, + props: { + isRoot: true + } + }); + } + } +} \ No newline at end of file diff --git a/Notification/package-lock.json b/Notification/package-lock.json index b3c6cfc3b56..8546732bb44 100644 --- a/Notification/package-lock.json +++ b/Notification/package-lock.json @@ -1,11 +1,11 @@ { - "name": "mail", + "name": "notification", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "mail", + "name": "notification", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -20,7 +20,8 @@ "Model": "file:../Model", "nodemailer": "^6.7.3", "nodemailer-express-handlebars": "^5.0.0", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "twilio": "^4.11.2" }, "devDependencies": { "nodemon": "^2.0.20" @@ -36,6 +37,7 @@ "@types/uuid": "^8.3.4", "axios": "^0.26.1", "crypto-js": "^4.1.1", + "json5": "^2.2.3", "moment": "^2.29.2", "moment-timezone": "^0.5.40", "nanoid": "^3.3.2", @@ -74,6 +76,7 @@ "bullmq": "^3.6.6", "Common": "file:../Common", "cors": "^2.8.5", + "cron-parser": "^4.8.1", "dotenv": "^16.0.0", "ejs": "^3.1.8", "express": "^4.17.3", @@ -261,6 +264,38 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -341,6 +376,23 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -417,6 +469,11 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -442,6 +499,14 @@ "node": ">=0.3.1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ejs": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", @@ -546,6 +611,25 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -620,6 +704,17 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -628,6 +723,61 @@ "node": ">=8" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -707,6 +857,70 @@ "node": ">=10" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -738,8 +952,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/neo-async": { "version": "2.6.2", @@ -835,6 +1048,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -861,6 +1082,25 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -873,6 +1113,35 @@ "node": ">=8.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -882,6 +1151,19 @@ "semver": "bin/semver" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -988,6 +1270,24 @@ } } }, + "node_modules/twilio": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.11.2.tgz", + "integrity": "sha512-Lxms8EuB+8nozem4xUHFkNcimHYiGVEJEdYTkZLRcirLJXRte/vMEQyBnbZAtas3A3Zy42PLpfMrmeG9aEk+wA==", + "dependencies": { + "axios": "^0.26.1", + "dayjs": "^1.8.29", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.0", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "url-parse": "^1.5.9", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/typescript": { "version": "4.9.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", @@ -1019,6 +1319,15 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -1034,6 +1343,19 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -1160,6 +1482,29 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1225,6 +1570,20 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1275,6 +1634,7 @@ "axios": "^0.26.1", "crypto-js": "^4.1.1", "jest": "^27.5.1", + "json5": "^2.2.3", "moment": "^2.29.2", "moment-timezone": "^0.5.40", "nanoid": "^3.3.2", @@ -1311,6 +1671,7 @@ "bullmq": "^3.6.6", "Common": "file:../Common", "cors": "^2.8.5", + "cron-parser": "^4.8.1", "dotenv": "^16.0.0", "ejs": "^3.1.8", "express": "^4.17.3", @@ -1345,6 +1706,11 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -1364,6 +1730,14 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ejs": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", @@ -1434,6 +1808,22 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, "glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -1490,11 +1880,53 @@ "wordwrap": "^1.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1556,6 +1988,59 @@ "minimatch": "^3.0.4" } }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1588,8 +2073,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "neo-async": { "version": "2.6.2", @@ -1659,6 +2143,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1679,6 +2168,19 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1688,12 +2190,37 @@ "picomatch": "^2.2.1" } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -1762,6 +2289,21 @@ "yn": "3.1.1" } }, + "twilio": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.11.2.tgz", + "integrity": "sha512-Lxms8EuB+8nozem4xUHFkNcimHYiGVEJEdYTkZLRcirLJXRte/vMEQyBnbZAtas3A3Zy42PLpfMrmeG9aEk+wA==", + "requires": { + "axios": "^0.26.1", + "dayjs": "^1.8.29", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.0", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "url-parse": "^1.5.9", + "xmlbuilder": "^13.0.2" + } + }, "typescript": { "version": "4.9.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", @@ -1780,6 +2322,15 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -1795,6 +2346,16 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/Notification/package.json b/Notification/package.json index db7cc6055c2..1406f173726 100644 --- a/Notification/package.json +++ b/Notification/package.json @@ -23,7 +23,8 @@ "Model": "file:../Model", "nodemailer": "^6.7.3", "nodemailer-express-handlebars": "^5.0.0", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "twilio": "^4.11.2" }, "devDependencies": { "nodemon": "^2.0.20" diff --git a/config.tpl.env b/config.tpl.env index c1f516d5bfa..34247516bd5 100644 --- a/config.tpl.env +++ b/config.tpl.env @@ -156,3 +156,10 @@ SENDGRID_API_KEY= # METERED PLANS: This is in the format of ,,,unitName METERED_PLAN_ACTIVE_MONITORING=,,1,active-monitor,month + + +# Twilio Settings +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_PHONE_NUMBER= +SMS_DEFAULT_COST_IN_CENTS= From 3f53053e1fa2ff5c9b6f33366ba886bb418b46a5 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 12:05:43 +0100 Subject: [PATCH 09/27] add usd cents field --- CommonUI/src/Components/Detail/Detail.tsx | 12 +++++++++ CommonUI/src/Components/Types/FieldType.ts | 1 + Dashboard/src/Pages/Settings/SmsLog.tsx | 31 ++++++++-------------- Model/Models/SmsLog.ts | 6 ++--- Notification/Services/SmsService.ts | 7 ++--- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/CommonUI/src/Components/Detail/Detail.tsx b/CommonUI/src/Components/Detail/Detail.tsx index de3c1cbaad0..9678f6d3b39 100644 --- a/CommonUI/src/Components/Detail/Detail.tsx +++ b/CommonUI/src/Components/Detail/Detail.tsx @@ -35,6 +35,8 @@ const Detail: Function = (props: ComponentProps): ReactElement => { return ; }; + + const getDropdownViewer: Function = ( data: string, options: Array, @@ -73,6 +75,10 @@ const Detail: Function = (props: ComponentProps): ReactElement => { return ; }; + const getUSDCentsField: Function = (usdCents: number): ReactElement => { + return
{usdCents/100} USD
; + }; + const getField: Function = (field: Field, index: number): ReactElement => { const fieldKey: string = field.key; @@ -112,6 +118,10 @@ const Detail: Function = (props: ComponentProps): ReactElement => { data = getColorField(data); } + if (data && field.fieldType === FieldType.USDCents) { + data = getUSDCentsField(data); + } + if (data && field.fieldType === FieldType.DictionaryOfStrings) { data = getDictionaryOfStringsViewer(props.item[field.key]); } @@ -278,3 +288,5 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; export default Detail; + + diff --git a/CommonUI/src/Components/Types/FieldType.ts b/CommonUI/src/Components/Types/FieldType.ts index bb9f0d5ea5d..135b2e2b471 100644 --- a/CommonUI/src/Components/Types/FieldType.ts +++ b/CommonUI/src/Components/Types/FieldType.ts @@ -28,6 +28,7 @@ enum FieldType { JavaScript = 'JavaScript', DictionaryOfStrings = 'DictionaryOfStrings', JSON = 'JSON', + USDCents = 'USDCents', } export default FieldType; diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index fbd4ba2247b..c25dea9ed23 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -65,7 +65,7 @@ const SMSLogs: FunctionComponent = ( }} selectMoreFields={{ smsText: true, - errorMessage: true, + statusMessage: true, }} actionButtons={[ { @@ -90,22 +90,15 @@ const SMSLogs: FunctionComponent = ( title: 'View Error', buttonStyleType: ButtonStyleType.NORMAL, icon: IconProp.Error, - isVisible: (item: JSONObject) => { - if (item['status'] === SmsStatus.Error) { - return true; - } - - return false; - }, onClick: async ( item: JSONObject, onCompleteAction: Function ) => { - setSmsText(item['errorMessage'] as string); + setSmsText(item['statusMessage'] as string); setSmsModalDescription( - 'Here is more information about the error.' + 'Here is more information about this message.' ); - setSmsModalTitle('Error'); + setSmsModalTitle('Status Message'); setShowViewSmsTextModal(true); onCompleteAction(); @@ -133,15 +126,6 @@ const SMSLogs: FunctionComponent = ( type: FieldType.Text, isFilterable: true, }, - { - field: { - fromNumber: true, - }, - isFilterable: true, - - title: 'From Number', - type: FieldType.Phone, - }, { field: { toNumber: true, @@ -159,6 +143,13 @@ const SMSLogs: FunctionComponent = ( type: FieldType.DateTime, isFilterable: true, }, + { + field: { + smsCostInUSDCents: true, + }, + title: 'SMS Cost', + type: FieldType.USDCents, + }, { field: { status: true, diff --git a/Model/Models/SmsLog.ts b/Model/Models/SmsLog.ts index e57170b87fc..431edd88b0c 100644 --- a/Model/Models/SmsLog.ts +++ b/Model/Models/SmsLog.ts @@ -204,8 +204,8 @@ export default class SmsLog extends BaseModel { @TableColumn({ required: false, type: TableColumnType.LongText, - title: 'Error Message', - description: 'Error Message (if any)', + title: 'Status Message', + description: 'Status Message (if any)', canReadOnPopulate: false, }) @Column({ @@ -213,7 +213,7 @@ export default class SmsLog extends BaseModel { type: ColumnType.LongText, length: ColumnLength.LongText, }) - public errorMessage?: string = undefined; + public statusMessage?: string = undefined; @ColumnAccessControl({ create: [], diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index eae646743fd..d21e7066ca7 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -9,6 +9,7 @@ import { IsBillingEnabled } from "CommonServer/Config"; import SmsLogService from "CommonServer/Services/SmsLogService" import ProjectService from "CommonServer/Services/ProjectService"; import Project from "Model/Models/Project"; +import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; export default class SmsService { public static async sendSms(to: Phone, message: string, options: { @@ -72,7 +73,7 @@ export default class SmsService { } - await client.messages + const twillioMessage: MessageInstance = await client.messages .create({ body: message, to: to.toString(), @@ -81,7 +82,7 @@ export default class SmsService { smsLog.status = SmsStatus.Success; - smsLog.errorMessage = ""; + smsLog.statusMessage = "Message ID: " + twillioMessage.sid; if (IsBillingEnabled && project) { smsLog.smsCostInUSDCents = SMSDefaultCostInCents; @@ -101,7 +102,7 @@ export default class SmsService { } catch (e: any) { smsLog.status = SmsStatus.Error; - smsLog.errorMessage = e && e.message ? e.message.toString() : e.toString(); + smsLog.statusMessage = e && e.message ? e.message.toString() : e.toString(); } if (options.projectId) { From 3b0904a01d0ae5af5aacef65229497ae09cb4d6e Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 13:34:34 +0100 Subject: [PATCH 10/27] add sms service --- Common/Types/SmsStatus.ts | 1 + Notification/API/SMS.ts | 40 +++++--------------- Notification/Index.ts | 3 +- Notification/Services/SmsService.ts | 57 +++++++++++++++++++++++++---- 4 files changed, 61 insertions(+), 40 deletions(-) diff --git a/Common/Types/SmsStatus.ts b/Common/Types/SmsStatus.ts index 2ba384a4b6e..14af6822fee 100644 --- a/Common/Types/SmsStatus.ts +++ b/Common/Types/SmsStatus.ts @@ -1,6 +1,7 @@ enum SmsStatus { Success = 'Success', Error = 'Error', + LowBalance = 'Low Balance', } export default SmsStatus; diff --git a/Notification/API/SMS.ts b/Notification/API/SMS.ts index 20e79891ef3..65c29310ae8 100644 --- a/Notification/API/SMS.ts +++ b/Notification/API/SMS.ts @@ -6,47 +6,25 @@ import Express, { const router: ExpressRouter = Express.getRouter(); import Response from 'CommonServer/Utils/Response'; import ClusterKeyAuthorization from 'CommonServer/Middleware/ClusterKeyAuthorization'; -import MailService from '../Services/MailService'; -import EmailMessage from 'Common/Types/Email/EmailMessage'; -import EmailTemplateType from 'Common/Types/Email/EmailTemplateType'; import { JSONObject } from 'Common/Types/JSON'; -import Email from 'Common/Types/Email'; -import Dictionary from 'Common/Types/Dictionary'; -import EmailServer from 'Common/Types/Email/EmailServer'; +import JSONFunctions from 'Common/Types/JSONFunctions'; +import SmsService from '../Services/SmsService'; +import Phone from 'Common/Types/Phone'; +import ObjectID from 'Common/Types/ObjectID'; router.post( '/send', ClusterKeyAuthorization.isAuthorizedServiceMiddleware, async (req: ExpressRequest, res: ExpressResponse) => { - const body: JSONObject = req.body; + const body: JSONObject = JSONFunctions.deserialize(req.body); - const mail: EmailMessage = { - templateType: body['templateType'] as EmailTemplateType, - toEmail: new Email(body['toEmail'] as string), - subject: body['subject'] as string, - vars: body['vars'] as Dictionary, - body: (body['body'] as string) || '', - }; - - let mailServer: EmailServer | undefined = undefined; - - if (hasMailServerSettingsInBody(body)) { - mailServer = MailService.getEmailServer(req.body); - } - - await MailService.send(mail, mailServer); + await SmsService.sendSms(body['to'] as Phone, body['message'] as string, { + projectId: body['projectId'] as ObjectID, + from: body['from'] as Phone, + }); return Response.sendEmptyResponse(req, res); } ); -const hasMailServerSettingsInBody: Function = (body: JSONObject): boolean => { - return ( - body && - Object.keys(body).filter((key: string) => { - return key.startsWith('SMTP_'); - }).length > 0 - ); -}; - export default router; diff --git a/Notification/Index.ts b/Notification/Index.ts index f9fad85d7ad..5aa709f244d 100644 --- a/Notification/Index.ts +++ b/Notification/Index.ts @@ -5,6 +5,7 @@ import Redis from 'CommonServer/Infrastructure/Redis'; // API import MailAPI from './API/Mail'; +import SmsAPI from './API/SMS'; import SMTPConfigAPI from './API/SMTPConfig'; import logger from 'CommonServer/Utils/Logger'; import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase'; @@ -16,7 +17,7 @@ const APP_NAME: string = 'notification'; const app: ExpressApplication = Express.getExpressApp(); app.use([`/${APP_NAME}/email`, '/email'], MailAPI); - +app.use([`/${APP_NAME}/sms`, '/sms'], SmsAPI); app.use([`/${APP_NAME}/smtp-config`, '/smtp-config'], SMTPConfigAPI); const init: Function = async (): Promise => { diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index d21e7066ca7..927c8fa2f97 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -41,18 +41,19 @@ export default class SmsService { smsLog.projectId = options.projectId; } - let project: Project | null = null ; + let project: Project | null = null; try { // make sure project has enough balance. - + if (options.projectId && IsBillingEnabled) { project = await ProjectService.findOneById({ id: options.projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, - enableAutoRechargeSmsOrCallBalance: true + enableAutoRechargeSmsOrCallBalance: true, + enableSmsNotifications: true }, props: { isRoot: true @@ -60,20 +61,60 @@ export default class SmsService { }); if (!project) { - throw new BadDataException(`Project ${options.projectId.toString()} not found.`); + smsLog.status = SmsStatus.Error; + smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`; + await SmsLogService.create({ + data: smsLog, + props: { + isRoot: true + } + }); + return; + + } + + if(!project.enableSmsNotifications){ + smsLog.status = SmsStatus.Error; + smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in project settings.`; + await SmsLogService.create({ + data: smsLog, + props: { + isRoot: true + } + }); + return; } if (!project.smsOrCallCurrentBalanceInUSDCents) { - throw new BadDataException(`Project ${options.projectId.toString()} does not have enough SMS balance.`); + smsLog.status = SmsStatus.LowBalance; + smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`; + await SmsLogService.create({ + data: smsLog, + props: { + isRoot: true + } + }); + return; + + } if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) { - throw new BadDataException(`Project does not have enough balance to send SMS. Current balance is ${project.smsOrCallCurrentBalanceInUSDCents} cents. Required balance is ${SMSDefaultCostInCents} cents to send this SMS.`); + smsLog.status = SmsStatus.LowBalance; + smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${project.smsOrCallCurrentBalanceInUSDCents} cents. Required balance is ${SMSDefaultCostInCents} cents to send this SMS.`; + await SmsLogService.create({ + data: smsLog, + props: { + isRoot: true + } + }); + return; + } } - const twillioMessage: MessageInstance = await client.messages + const twillioMessage: MessageInstance = await client.messages .create({ body: message, to: to.toString(), @@ -107,7 +148,7 @@ export default class SmsService { if (options.projectId) { await SmsLogService.create({ - data: smsLog, + data: smsLog, props: { isRoot: true } From 8459e2873c11274da5db2af8d55cf5f077311aea Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 13:40:03 +0100 Subject: [PATCH 11/27] send sms service --- CommonServer/Services/SmsService.ts | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 CommonServer/Services/SmsService.ts diff --git a/CommonServer/Services/SmsService.ts b/CommonServer/Services/SmsService.ts new file mode 100644 index 00000000000..47440bbf0e8 --- /dev/null +++ b/CommonServer/Services/SmsService.ts @@ -0,0 +1,40 @@ +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> { + + const body: JSONObject = { + to: to.toString(), + message, + from: options.from.toString(), + projectId: options.projectId?.toString(), + }; + + return await API.post( + new URL( + Protocol.HTTP, + NotificationHostname, + new Route('/sms/send') + ), + body, + { + ...ClusterKeyAuthorization.getClusterKeyHeaders(), + } + ); + } +} From 673f97404be7cc22eb5951c6a6af4ab8002741a1 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 13:53:04 +0100 Subject: [PATCH 12/27] fix sms service --- CommonServer/Services/SmsService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommonServer/Services/SmsService.ts b/CommonServer/Services/SmsService.ts index 47440bbf0e8..f88b4545f7f 100644 --- a/CommonServer/Services/SmsService.ts +++ b/CommonServer/Services/SmsService.ts @@ -14,14 +14,14 @@ 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 + from?: Phone, // from phone number } ): Promise> { const body: JSONObject = { to: to.toString(), message, - from: options.from.toString(), + from: options.from?.toString(), projectId: options.projectId?.toString(), }; From aed03ffcb30fddc92e84b35dd3e6c7382867ac0c Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 14:38:55 +0100 Subject: [PATCH 13/27] add invoice service --- Common/Types/SmsStatus.ts | 2 +- CommonServer/Services/BillingService.ts | 29 +++++++++++++ CommonServer/Services/SmsService.ts | 9 ++-- CommonUI/src/Components/Detail/Detail.tsx | 6 +-- Notification/API/SMS.ts | 12 ++++-- Notification/Config.ts | 9 +++- Notification/Services/SmsService.ts | 51 ++++++++++++++++++++--- 7 files changed, 97 insertions(+), 21 deletions(-) diff --git a/Common/Types/SmsStatus.ts b/Common/Types/SmsStatus.ts index 14af6822fee..e4f63cb5fb2 100644 --- a/Common/Types/SmsStatus.ts +++ b/Common/Types/SmsStatus.ts @@ -1,7 +1,7 @@ enum SmsStatus { Success = 'Success', Error = 'Error', - LowBalance = 'Low Balance', + LowBalance = 'Low Balance', } export default SmsStatus; diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index 91b562f0cac..bcc3730ac20 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -497,6 +497,35 @@ export class BillingService { }); } + public static async genrateInvoiceAndChargeCustomer( + customerId: string, + itemText: string, + amountInUsd: number + ): Promise { + 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, + quantity: 1, + description: itemText, + currency: 'usd', + customer: customerId, + }); + + await this.stripe.invoices.finalizeInvoice(invoice.id!); + + await this.stripe.invoices.pay(invoice.id); + } + public static async payInvoice( customerId: string, invoiceId: string diff --git a/CommonServer/Services/SmsService.ts b/CommonServer/Services/SmsService.ts index f88b4545f7f..c822864c4a4 100644 --- a/CommonServer/Services/SmsService.ts +++ b/CommonServer/Services/SmsService.ts @@ -12,12 +12,13 @@ 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 + to: Phone, + message: string, + options: { + projectId?: ObjectID | undefined; // project id for sms log + from?: Phone; // from phone number } ): Promise> { - const body: JSONObject = { to: to.toString(), message, diff --git a/CommonUI/src/Components/Detail/Detail.tsx b/CommonUI/src/Components/Detail/Detail.tsx index 9678f6d3b39..f9fc44aa9d9 100644 --- a/CommonUI/src/Components/Detail/Detail.tsx +++ b/CommonUI/src/Components/Detail/Detail.tsx @@ -35,8 +35,6 @@ const Detail: Function = (props: ComponentProps): ReactElement => { return ; }; - - const getDropdownViewer: Function = ( data: string, options: Array, @@ -76,7 +74,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; const getUSDCentsField: Function = (usdCents: number): ReactElement => { - return
{usdCents/100} USD
; + return
{usdCents / 100} USD
; }; const getField: Function = (field: Field, index: number): ReactElement => { @@ -288,5 +286,3 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; export default Detail; - - diff --git a/Notification/API/SMS.ts b/Notification/API/SMS.ts index 65c29310ae8..aed3e53e789 100644 --- a/Notification/API/SMS.ts +++ b/Notification/API/SMS.ts @@ -18,10 +18,14 @@ router.post( async (req: ExpressRequest, res: ExpressResponse) => { const body: JSONObject = JSONFunctions.deserialize(req.body); - await SmsService.sendSms(body['to'] as Phone, body['message'] as string, { - projectId: body['projectId'] as ObjectID, - from: body['from'] as Phone, - }); + await SmsService.sendSms( + body['to'] as Phone, + body['message'] as string, + { + projectId: body['projectId'] as ObjectID, + from: body['from'] as Phone, + } + ); return Response.sendEmptyResponse(req, res); } diff --git a/Notification/Config.ts b/Notification/Config.ts index 5df7eb98ff7..cb5174d56ba 100644 --- a/Notification/Config.ts +++ b/Notification/Config.ts @@ -28,7 +28,12 @@ export const InternalSmtpFromName: string = export const TwilioAccountSid: string = process.env['TWILIO_ACCOUNT_SID'] || ''; export const TwilioAuthToken: string = process.env['TWILIO_AUTH_TOKEN'] || ''; -export const TwilioPhoneNumber: string = process.env['TWILIO_PHONE_NUMBER'] || ''; -export const SMSDefaultCostInCents: number = process.env['SMS_DEFAULT_COST_IN_CENTS'] ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) : 0; +export const TwilioPhoneNumber: string = + process.env['TWILIO_PHONE_NUMBER'] || ''; +export const SMSDefaultCostInCents: number = process.env[ + 'SMS_DEFAULT_COST_IN_CENTS' +] + ? parseInt(process.env['SMS_DEFAULT_COST_IN_CENTS']) + : 0; export const SendGridApiKey: string = process.env['SENDGRID_API_KEY'] || ''; diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 927c8fa2f97..19510aa174a 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -10,6 +10,8 @@ import SmsLogService from "CommonServer/Services/SmsLogService" import ProjectService from "CommonServer/Services/ProjectService"; import Project from "Model/Models/Project"; import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; +import BillingService from "CommonServer/Services/BillingService"; +import logger from "CommonServer/Utils/Logger"; export default class SmsService { public static async sendSms(to: Phone, message: string, options: { @@ -48,12 +50,16 @@ export default class SmsService { // make sure project has enough balance. if (options.projectId && IsBillingEnabled) { + project = await ProjectService.findOneById({ id: options.projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, enableAutoRechargeSmsOrCallBalance: true, - enableSmsNotifications: true + enableSmsNotifications: true, + autoRechargeSmsOrCallByBalanceInUSD: true, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, + paymentProviderCustomerId: true }, props: { isRoot: true @@ -63,7 +69,7 @@ export default class SmsService { if (!project) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `Project ${options.projectId.toString()} not found.`; - await SmsLogService.create({ + await SmsLogService.create({ data: smsLog, props: { isRoot: true @@ -73,7 +79,7 @@ export default class SmsService { } - if(!project.enableSmsNotifications){ + if (!project.enableSmsNotifications) { smsLog.status = SmsStatus.Error; smsLog.statusMessage = `SMS notifications are not enabled for this project. Please enable SMS notifications in project settings.`; await SmsLogService.create({ @@ -85,6 +91,39 @@ export default class SmsService { return; } + // check if auto recharge is enabled and current balance is low. + + if (IsBillingEnabled && project && project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { + + if (project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { + try { + // recharge balance + const updatedAmount: number = Math.floor(project.smsOrCallCurrentBalanceInUSDCents + (project.autoRechargeSmsOrCallByBalanceInUSD * 100)); + + // If the recharge is succcessful, then update the project balance. + await BillingService.genrateInvoiceAndChargeCustomer(project.paymentProviderCustomerId!, "SMS or Call Balance Recharge", project.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); + } + + } + } + if (!project.smsOrCallCurrentBalanceInUSDCents) { smsLog.status = SmsStatus.LowBalance; smsLog.statusMessage = `Project ${options.projectId.toString()} does not have enough SMS balance.`; @@ -96,7 +135,7 @@ export default class SmsService { }); return; - + } if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) { @@ -109,7 +148,6 @@ export default class SmsService { } }); return; - } } @@ -153,6 +191,9 @@ export default class SmsService { isRoot: true } }); + + + } } } \ No newline at end of file From 2a7f18fa6081e3e001f736801614b0e661a7eb6e Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 14:41:59 +0100 Subject: [PATCH 14/27] fix fmt --- Notification/Services/SmsService.ts | 159 ++++++++++++++++------------ 1 file changed, 92 insertions(+), 67 deletions(-) diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 19510aa174a..013686366b8 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -1,24 +1,32 @@ -import ObjectID from "Common/Types/ObjectID"; -import Phone from "Common/Types/Phone"; -import { SMSDefaultCostInCents, TwilioAccountSid, TwilioAuthToken, TwilioPhoneNumber } from "../Config"; +import ObjectID from 'Common/Types/ObjectID'; +import Phone from 'Common/Types/Phone'; +import { + SMSDefaultCostInCents, + TwilioAccountSid, + TwilioAuthToken, + TwilioPhoneNumber, +} from '../Config'; import Twilio from 'twilio'; -import BadDataException from "Common/Types/Exception/BadDataException"; -import SmsLog from "Model/Models/SmsLog"; -import SmsStatus from "Common/Types/SmsStatus"; -import { IsBillingEnabled } from "CommonServer/Config"; -import SmsLogService from "CommonServer/Services/SmsLogService" -import ProjectService from "CommonServer/Services/ProjectService"; -import Project from "Model/Models/Project"; -import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message"; -import BillingService from "CommonServer/Services/BillingService"; -import logger from "CommonServer/Utils/Logger"; +import BadDataException from 'Common/Types/Exception/BadDataException'; +import SmsLog from 'Model/Models/SmsLog'; +import SmsStatus from 'Common/Types/SmsStatus'; +import { IsBillingEnabled } from 'CommonServer/Config'; +import SmsLogService from 'CommonServer/Services/SmsLogService'; +import ProjectService from 'CommonServer/Services/ProjectService'; +import Project from 'Model/Models/Project'; +import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; +import BillingService from 'CommonServer/Services/BillingService'; +import logger from 'CommonServer/Utils/Logger'; 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 { - + public static async sendSms( + to: Phone, + message: string, + options: { + projectId?: ObjectID | undefined; // project id for sms log + from: Phone; // from phone number + } + ): Promise { if (!TwilioAccountSid) { throw new BadDataException('TwilioAccountSid is not configured'); } @@ -27,7 +35,6 @@ export default class SmsService { throw new BadDataException('TwilioAuthToken is not configured'); } - if (!TwilioPhoneNumber) { throw new BadDataException('TwilioPhoneNumber is not configured'); } @@ -46,11 +53,9 @@ export default class SmsService { let project: Project | null = null; try { - - // make sure project has enough balance. + // make sure project has enough balance. if (options.projectId && IsBillingEnabled) { - project = await ProjectService.findOneById({ id: options.projectId, select: { @@ -59,11 +64,11 @@ export default class SmsService { enableSmsNotifications: true, autoRechargeSmsOrCallByBalanceInUSD: true, autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, - paymentProviderCustomerId: true + paymentProviderCustomerId: true, }, props: { - isRoot: true - } + isRoot: true, + }, }); if (!project) { @@ -72,11 +77,10 @@ export default class SmsService { await SmsLogService.create({ data: smsLog, props: { - isRoot: true - } + isRoot: true, + }, }); return; - } if (!project.enableSmsNotifications) { @@ -85,42 +89,60 @@ export default class SmsService { await SmsLogService.create({ data: smsLog, props: { - isRoot: true - } + isRoot: true, + }, }); return; } // check if auto recharge is enabled and current balance is low. - if (IsBillingEnabled && project && project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { - - if (project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD) { + if ( + IsBillingEnabled && + project && + project.enableAutoRechargeSmsOrCallBalance && + project.autoRechargeSmsOrCallByBalanceInUSD && + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + ) { + if ( + project.smsOrCallCurrentBalanceInUSDCents && + project.smsOrCallCurrentBalanceInUSDCents < + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + ) { try { // recharge balance - const updatedAmount: number = Math.floor(project.smsOrCallCurrentBalanceInUSDCents + (project.autoRechargeSmsOrCallByBalanceInUSD * 100)); + const updatedAmount: number = Math.floor( + project.smsOrCallCurrentBalanceInUSDCents + + project.autoRechargeSmsOrCallByBalanceInUSD * + 100 + ); // If the recharge is succcessful, then update the project balance. - await BillingService.genrateInvoiceAndChargeCustomer(project.paymentProviderCustomerId!, "SMS or Call Balance Recharge", project.autoRechargeSmsOrCallByBalanceInUSD); + await BillingService.genrateInvoiceAndChargeCustomer( + project.paymentProviderCustomerId!, + 'SMS or Call Balance Recharge', + project.autoRechargeSmsOrCallByBalanceInUSD + ); await ProjectService.updateOneById({ data: { - smsOrCallCurrentBalanceInUSDCents: updatedAmount + smsOrCallCurrentBalanceInUSDCents: + updatedAmount, }, id: project.id!, props: { - isRoot: true - } + isRoot: true, + }, }); - project.smsOrCallCurrentBalanceInUSDCents = updatedAmount; + project.smsOrCallCurrentBalanceInUSDCents = + updatedAmount; - // TODO: Send an email on successful recharge. + // TODO: Send an email on successful recharge. } catch (err) { - // TODO: if the recharge fails, then send email to the user. + // TODO: if the recharge fails, then send email to the user. logger.error(err); } - } } @@ -130,70 +152,73 @@ export default class SmsService { await SmsLogService.create({ data: smsLog, props: { - isRoot: true - } + isRoot: true, + }, }); return; - - } - if (project.smsOrCallCurrentBalanceInUSDCents < SMSDefaultCostInCents) { + if ( + project.smsOrCallCurrentBalanceInUSDCents < + SMSDefaultCostInCents + ) { smsLog.status = SmsStatus.LowBalance; smsLog.statusMessage = `Project does not have enough balance to send SMS. Current balance is ${project.smsOrCallCurrentBalanceInUSDCents} cents. Required balance is ${SMSDefaultCostInCents} cents to send this SMS.`; await SmsLogService.create({ data: smsLog, props: { - isRoot: true - } + isRoot: true, + }, }); return; } - } - const twillioMessage: MessageInstance = await client.messages - .create({ + const twillioMessage: MessageInstance = + await client.messages.create({ body: message, to: to.toString(), - from: options && options.from ? options.from.toString() : TwilioPhoneNumber.toString(), // From a valid Twilio number + from: + options && options.from + ? options.from.toString() + : TwilioPhoneNumber.toString(), // From a valid Twilio number }); - smsLog.status = SmsStatus.Success; - smsLog.statusMessage = "Message ID: " + twillioMessage.sid; + smsLog.statusMessage = 'Message ID: ' + twillioMessage.sid; if (IsBillingEnabled && project) { smsLog.smsCostInUSDCents = SMSDefaultCostInCents; - project.smsOrCallCurrentBalanceInUSDCents = Math.floor(project.smsOrCallCurrentBalanceInUSDCents! - SMSDefaultCostInCents); + project.smsOrCallCurrentBalanceInUSDCents = Math.floor( + project.smsOrCallCurrentBalanceInUSDCents! - + SMSDefaultCostInCents + ); await ProjectService.updateOneById({ data: { - smsOrCallCurrentBalanceInUSDCents: project.smsOrCallCurrentBalanceInUSDCents + smsOrCallCurrentBalanceInUSDCents: + project.smsOrCallCurrentBalanceInUSDCents, }, id: project.id!, props: { - isRoot: true - } + isRoot: true, + }, }); } - } catch (e: any) { smsLog.status = SmsStatus.Error; - smsLog.statusMessage = e && e.message ? e.message.toString() : e.toString(); + smsLog.statusMessage = + e && e.message ? e.message.toString() : e.toString(); } if (options.projectId) { await SmsLogService.create({ data: smsLog, props: { - isRoot: true - } + isRoot: true, + }, }); - - - } } -} \ No newline at end of file +} From 83289cc80e6cc741e47367e37766ffd7572b9c19 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 14:42:53 +0100 Subject: [PATCH 15/27] fix if cond --- Notification/Services/SmsService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 013686366b8..1ce6ad5aa0a 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -98,8 +98,6 @@ export default class SmsService { // check if auto recharge is enabled and current balance is low. if ( - IsBillingEnabled && - project && project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD From b840f78d1d2c2eaeea61c16174afba0f13bb8907 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 15:55:32 +0100 Subject: [PATCH 16/27] refactor notification service --- CommonServer/Services/NotificationService.ts | 86 ++++++++++++++++++++ Notification/Services/SmsService.ts | 56 +------------ 2 files changed, 89 insertions(+), 53 deletions(-) create mode 100644 CommonServer/Services/NotificationService.ts diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts new file mode 100644 index 00000000000..d7c9cb09c1c --- /dev/null +++ b/CommonServer/Services/NotificationService.ts @@ -0,0 +1,86 @@ + +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'; + +export default class NotificationService { + public static async rechargeIfBalanceIsLow( + projectId: ObjectID, + ): Promise { + let project: Project | null = null; + if (projectId && IsBillingEnabled) { + project= await ProjectService.findOneById({ + id: projectId, + select: { + smsOrCallCurrentBalanceInUSDCents: true, + enableAutoRechargeSmsOrCallBalance: true, + enableSmsNotifications: true, + autoRechargeSmsOrCallByBalanceInUSD: true, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, + paymentProviderCustomerId: true, + }, + props: { + isRoot: true, + }, + }); + + + if (!project) { + return 0; + } + + if ( + project.enableAutoRechargeSmsOrCallBalance && + project.autoRechargeSmsOrCallByBalanceInUSD && + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + ) { + if ( + project.smsOrCallCurrentBalanceInUSDCents && + project.smsOrCallCurrentBalanceInUSDCents < + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + ) { + try { + // recharge balance + const updatedAmount: number = Math.floor( + project.smsOrCallCurrentBalanceInUSDCents + + project.autoRechargeSmsOrCallByBalanceInUSD * + 100 + ); + + // If the recharge is succcessful, then update the project balance. + await BillingService.genrateInvoiceAndChargeCustomer( + project.paymentProviderCustomerId!, + 'SMS or Call Balance Recharge', + project.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; + } +} diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 1ce6ad5aa0a..d488c9c42e6 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -15,8 +15,7 @@ import SmsLogService from 'CommonServer/Services/SmsLogService'; import ProjectService from 'CommonServer/Services/ProjectService'; import Project from 'Model/Models/Project'; import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import BillingService from 'CommonServer/Services/BillingService'; -import logger from 'CommonServer/Utils/Logger'; +import NotificationService from "CommonServer/Services/NotificationService" export default class SmsService { public static async sendSms( @@ -60,11 +59,7 @@ export default class SmsService { id: options.projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, - enableAutoRechargeSmsOrCallBalance: true, enableSmsNotifications: true, - autoRechargeSmsOrCallByBalanceInUSD: true, - autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: true, - paymentProviderCustomerId: true, }, props: { isRoot: true, @@ -96,53 +91,8 @@ export default class SmsService { } // check if auto recharge is enabled and current balance is low. - - if ( - project.enableAutoRechargeSmsOrCallBalance && - project.autoRechargeSmsOrCallByBalanceInUSD && - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD - ) { - if ( - project.smsOrCallCurrentBalanceInUSDCents && - project.smsOrCallCurrentBalanceInUSDCents < - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD - ) { - try { - // recharge balance - const updatedAmount: number = Math.floor( - project.smsOrCallCurrentBalanceInUSDCents + - project.autoRechargeSmsOrCallByBalanceInUSD * - 100 - ); - - // If the recharge is succcessful, then update the project balance. - await BillingService.genrateInvoiceAndChargeCustomer( - project.paymentProviderCustomerId!, - 'SMS or Call Balance Recharge', - project.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); - } - } - } + const updtedBalance = await NotificationService.rechargeIfBalanceIsLow(project.id!); + project.smsOrCallCurrentBalanceInUSDCents = updtedBalance; if (!project.smsOrCallCurrentBalanceInUSDCents) { smsLog.status = SmsStatus.LowBalance; From b0f408329733445923460fa2b08e5cb3a939f0df Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 16:19:04 +0100 Subject: [PATCH 17/27] fix project service --- CommonServer/Services/ProjectService.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index 9efc778f3f3..d9ac3d20d37 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -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 { public constructor(postgresDatabase?: PostgresDatabase) { @@ -115,6 +116,15 @@ export class Service extends DatabaseService { return Promise.resolve({ createBy: data, carryForward: null }); } + protected override async onUpdateSuccess(onUpdate: OnUpdate, _updatedItemIds: ObjectID[]): Promise> { + + if(onUpdate.updateBy.data.autoRechargeSmsOrCallByBalanceInUSD && IsBillingEnabled) { + await NotificationService.rechargeIfBalanceIsLow(new ObjectID(onUpdate.updateBy.query._id! as string)); + } + + return onUpdate; + } + protected override async onBeforeUpdate( updateBy: UpdateBy ): Promise> { From d7c8edd598541aa54ab0ca6ec275cf45d4384a55 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 17:14:05 +0100 Subject: [PATCH 18/27] hard delete tiems in the database --- CommonServer/Services/DatabaseService.ts | 25 ++++ CommonServer/Services/Index.ts | 5 + CommonServer/Services/SmsLogService.ts | 1 + CommonUI/src/Components/Detail/Detail.tsx | 2 +- Dashboard/src/Pages/Settings/CallSms.tsx | 13 +- Dashboard/src/Pages/Settings/SideMenu.tsx | 4 +- Dashboard/src/Pages/Settings/SmsLog.tsx | 128 ++++++++++-------- Notification/Services/SmsService.ts | 1 + .../HardDelete/HardDeleteItemsInDatabase.ts | 38 ++++++ 9 files changed, 148 insertions(+), 69 deletions(-) diff --git a/CommonServer/Services/DatabaseService.ts b/CommonServer/Services/DatabaseService.ts index a22b65eafc9..6a00d397606 100644 --- a/CommonServer/Services/DatabaseService.ts +++ b/CommonServer/Services/DatabaseService.ts @@ -79,6 +79,26 @@ class DatabaseService { 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 @@ -92,6 +112,11 @@ class DatabaseService { } } + public hardDeleteItemsOlderThanInDays(columnName: string, olderThan: number) { + this.hardDeleteItemByColumnName = columnName; + this.hardDeleteItemsOlderThanDays = olderThan; + } + public getModel(): TBaseModel { return this.model; } diff --git a/CommonServer/Services/Index.ts b/CommonServer/Services/Index.ts index aaa5b6be1a9..472f252345b 100644 --- a/CommonServer/Services/Index.ts +++ b/CommonServer/Services/Index.ts @@ -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, @@ -118,4 +121,6 @@ export default [ WorkflowService, WorkflowVariablesService, WorkflowLogService, + + SmsLogService ]; diff --git a/CommonServer/Services/SmsLogService.ts b/CommonServer/Services/SmsLogService.ts index 9ec43b332b0..3563f0e3008 100644 --- a/CommonServer/Services/SmsLogService.ts +++ b/CommonServer/Services/SmsLogService.ts @@ -5,6 +5,7 @@ import DatabaseService from './DatabaseService'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); + this.hardDeleteItemsOlderThanInDays("createdAt", 30); } } diff --git a/CommonUI/src/Components/Detail/Detail.tsx b/CommonUI/src/Components/Detail/Detail.tsx index f9fc44aa9d9..f4ad2f7b94a 100644 --- a/CommonUI/src/Components/Detail/Detail.tsx +++ b/CommonUI/src/Components/Detail/Detail.tsx @@ -74,7 +74,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; const getUSDCentsField: Function = (usdCents: number): ReactElement => { - return
{usdCents / 100} USD
; + return
{usdCents / 100} USD
; }; const getField: Function = (field: Field, index: number): ReactElement => { diff --git a/Dashboard/src/Pages/Settings/CallSms.tsx b/Dashboard/src/Pages/Settings/CallSms.tsx index 7c8ad15a28b..9c633ab4803 100644 --- a/Dashboard/src/Pages/Settings/CallSms.tsx +++ b/Dashboard/src/Pages/Settings/CallSms.tsx @@ -11,6 +11,7 @@ import PageComponentProps from '../PageComponentProps'; import DashboardSideMenu from './SideMenu'; import FieldType from 'CommonUI/src/Components/Types/FieldType'; import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; +import { BILLING_ENABLED } from 'CommonUI/src/Config'; const Settings: FunctionComponent = ( _props: PageComponentProps @@ -41,7 +42,7 @@ const Settings: FunctionComponent = ( sideMenu={} > {/* API Key View */} - = ( fields: [ { field: { - smsOrCallCurrentBalanceInUSD: true, + smsOrCallCurrentBalanceInUSDCents: true, }, - fieldType: FieldType.Number, + fieldType: FieldType.USDCents, title: 'SMS or Call Current Balance', description: 'This is your current balance for SMS or Call. It is in USD. ', @@ -67,7 +68,7 @@ const Settings: FunctionComponent = ( ], modelId: DashboardNavigation.getProjectId()?.toString(), }} - /> + /> : <>} = ( }} /> - = ( ], modelId: DashboardNavigation.getProjectId()?.toString(), }} - /> + /> : <>} ); }; diff --git a/Dashboard/src/Pages/Settings/SideMenu.tsx b/Dashboard/src/Pages/Settings/SideMenu.tsx index 9879d8dfaa6..64e6b17a20b 100644 --- a/Dashboard/src/Pages/Settings/SideMenu.tsx +++ b/Dashboard/src/Pages/Settings/SideMenu.tsx @@ -155,12 +155,12 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => { /> = ( _props: PageComponentProps @@ -26,6 +29,70 @@ const SMSLogs: FunctionComponent = ( const [smsModelTitle, setSmsModalTitle] = useState(''); const [smsModelDescription, setSmsModalDescription] = useState(''); + + const modelTableColumns: Columns = [{ + field: { + _id: true, + }, + title: 'Log ID', + type: FieldType.Text, + isFilterable: true, + }, + { + field: { + toNumber: true, + }, + isFilterable: true, + + title: 'To Number', + type: FieldType.Phone, + }, + { + field: { + createdAt: true, + }, + title: 'Sent at', + type: FieldType.DateTime, + isFilterable: true, + }, + + { + field: { + status: true, + }, + title: 'Status', + type: FieldType.Text, + getElement: (item: JSONObject): ReactElement => { + if (item['status']) { + return ( + + ); + } + + return <>; + }, + isFilterable: true, + }]; + + if(BILLING_ENABLED){ + modelTableColumns.push({ + field: { + smsCostInUSDCents: true, + }, + title: 'SMS Cost', + type: FieldType.USDCents, + } as Column); + } + return ( = ( } showRefreshButton={true} showFilterButton={true} - columns={[ - { - field: { - _id: true, - }, - title: 'Log ID', - type: FieldType.Text, - isFilterable: true, - }, - { - field: { - toNumber: true, - }, - isFilterable: true, - - title: 'To Number', - type: FieldType.Phone, - }, - { - field: { - createdAt: true, - }, - title: 'Sent at', - type: FieldType.DateTime, - isFilterable: true, - }, - { - field: { - smsCostInUSDCents: true, - }, - title: 'SMS Cost', - type: FieldType.USDCents, - }, - { - field: { - status: true, - }, - title: 'Status', - type: FieldType.Text, - getElement: (item: JSONObject): ReactElement => { - if (item['status']) { - return ( - - ); - } - - return <>; - }, - isFilterable: true, - }, - ]} + columns={modelTableColumns} /> {showViewSmsTextModal && ( diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index d488c9c42e6..c91cabb4b50 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -155,6 +155,7 @@ export default class SmsService { }); } } catch (e: any) { + smsLog.smsCostInUSDCents = 0; smsLog.status = SmsStatus.Error; smsLog.statusMessage = e && e.message ? e.message.toString() : e.toString(); diff --git a/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts b/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts index 0bc6c56f3c6..99d84c5ffb7 100644 --- a/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts +++ b/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts @@ -35,3 +35,41 @@ RunCron( } } ); + + +RunCron( + 'HardDelete:HardDeleteOlderItemsInDatabase', + { schedule: IsDevelopment ? EVERY_MINUTE : EVERY_DAY, runOnStartup: false }, + async () => { + for (const service of Services) { + if (service instanceof DatabaseService) { + + if(!service.hardDeleteItemByColumnName || !service.hardDeleteItemsOlderThanDays) { + continue; + } + + try { + // Retain data for 30 days for accidental deletion, and then hard delete. + await service.hardDeleteBy({ + query: { + [service.hardDeleteItemByColumnName]: QueryHelper.lessThan( + OneUptimeDate.getSomeDaysAgo(service.hardDeleteItemsOlderThanDays) + ), + }, + props: { + isRoot: true, + }, + limit: LIMIT_MAX, + skip: 0, + }); + } catch (err) { + logger.error(err); + } + } + } + } +); + + + + From 4b17d3a125e0ead2d9a23191b5329af73c02cd04 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 17:15:20 +0100 Subject: [PATCH 19/27] fix issue in project service --- CommonServer/Services/ProjectService.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index d9ac3d20d37..a6196ce847d 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -116,19 +116,16 @@ export class Service extends DatabaseService { return Promise.resolve({ createBy: data, carryForward: null }); } - protected override async onUpdateSuccess(onUpdate: OnUpdate, _updatedItemIds: ObjectID[]): Promise> { - - if(onUpdate.updateBy.data.autoRechargeSmsOrCallByBalanceInUSD && IsBillingEnabled) { - await NotificationService.rechargeIfBalanceIsLow(new ObjectID(onUpdate.updateBy.query._id! as string)); - } - - return onUpdate; - } - protected override async onBeforeUpdate( updateBy: UpdateBy ): Promise> { if (IsBillingEnabled) { + + if(updateBy.data.autoRechargeSmsOrCallByBalanceInUSD) { + await NotificationService.rechargeIfBalanceIsLow(new ObjectID(updateBy.query._id! as string)); + } + + if (updateBy.data.paymentProviderPlanId) { // payment provider id changed. const project: Model | null = await this.findOneById({ From 19be9f42b6a6401e43bbec3cd1c0a79777a9b541 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 17:21:14 +0100 Subject: [PATCH 20/27] add payment method --- CommonServer/Services/BillingService.ts | 10 ++++++++ CommonServer/Services/NotificationService.ts | 24 +++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index bcc3730ac20..c1754ecab27 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -346,6 +346,16 @@ export class BillingService { await this.stripe.paymentMethods.detach(paymentMethodId); } + public static async hasPaymentMethods( + customerId: string + ): Promise { + if((await this.getPaymentMethods(customerId)).length > 0) { + return true + } + + return false; + } + public static async getPaymentMethods( customerId: string ): Promise> { diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index d7c9cb09c1c..47c479cb3ef 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -5,14 +5,19 @@ 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, + projectId: ObjectID, ): Promise { - let project: Project | null = null; + let project: Project | null = null; if (projectId && IsBillingEnabled) { - project= await ProjectService.findOneById({ + + // check payment methods. + + + project = await ProjectService.findOneById({ id: projectId, select: { smsOrCallCurrentBalanceInUSDCents: true, @@ -27,11 +32,14 @@ export default class NotificationService { }, }); - 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 ( project.enableAutoRechargeSmsOrCallBalance && project.autoRechargeSmsOrCallByBalanceInUSD && @@ -40,14 +48,14 @@ export default class NotificationService { if ( project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { try { // recharge balance const updatedAmount: number = Math.floor( project.smsOrCallCurrentBalanceInUSDCents + - project.autoRechargeSmsOrCallByBalanceInUSD * - 100 + project.autoRechargeSmsOrCallByBalanceInUSD * + 100 ); // If the recharge is succcessful, then update the project balance. @@ -78,7 +86,7 @@ export default class NotificationService { } } } - + } return project?.smsOrCallCurrentBalanceInUSDCents || 0; From 125c6417cf155971312520459a0b01389c853365 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 17:32:30 +0100 Subject: [PATCH 21/27] fix billing service --- CommonServer/Services/BillingService.ts | 4 +- CommonServer/Services/DatabaseService.ts | 21 +- CommonServer/Services/Index.ts | 2 +- CommonServer/Services/NotificationService.ts | 24 +- CommonServer/Services/ProjectService.ts | 8 +- CommonServer/Services/SmsLogService.ts | 2 +- CommonUI/src/Components/Detail/Detail.tsx | 2 +- Dashboard/src/Pages/Settings/CallSms.tsx | 348 +++++++++--------- Dashboard/src/Pages/Settings/SmsLog.tsx | 96 ++--- Notification/Services/SmsService.ts | 7 +- .../HardDelete/HardDeleteItemsInDatabase.ts | 20 +- 11 files changed, 273 insertions(+), 261 deletions(-) diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index c1754ecab27..d4e0c2557bf 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -349,8 +349,8 @@ export class BillingService { public static async hasPaymentMethods( customerId: string ): Promise { - if((await this.getPaymentMethods(customerId)).length > 0) { - return true + if ((await this.getPaymentMethods(customerId)).length > 0) { + return true; } return false; diff --git a/CommonServer/Services/DatabaseService.ts b/CommonServer/Services/DatabaseService.ts index 6a00d397606..ae1d6141971 100644 --- a/CommonServer/Services/DatabaseService.ts +++ b/CommonServer/Services/DatabaseService.ts @@ -79,25 +79,21 @@ class DatabaseService { private model!: TBaseModel; private modelName!: string; - - private _hardDeleteItemByColumnName : string = ''; - public get hardDeleteItemByColumnName() : string { + private _hardDeleteItemByColumnName: string = ''; + public get hardDeleteItemByColumnName(): string { return this._hardDeleteItemByColumnName; } - public set hardDeleteItemByColumnName(v : string) { + public set hardDeleteItemByColumnName(v: string) { this._hardDeleteItemByColumnName = v; } - - - private _hardDeleteItemsOlderThanDays : number = 0; - public get hardDeleteItemsOlderThanDays() : number { + private _hardDeleteItemsOlderThanDays: number = 0; + public get hardDeleteItemsOlderThanDays(): number { return this._hardDeleteItemsOlderThanDays; } - public set hardDeleteItemsOlderThanDays(v : number) { + public set hardDeleteItemsOlderThanDays(v: number) { this._hardDeleteItemsOlderThanDays = v; } - public constructor( modelType: { new (): TBaseModel }, @@ -112,7 +108,10 @@ class DatabaseService { } } - public hardDeleteItemsOlderThanInDays(columnName: string, olderThan: number) { + public hardDeleteItemsOlderThanInDays( + columnName: string, + olderThan: number + ) { this.hardDeleteItemByColumnName = columnName; this.hardDeleteItemsOlderThanDays = olderThan; } diff --git a/CommonServer/Services/Index.ts b/CommonServer/Services/Index.ts index 472f252345b..945db8ebc88 100644 --- a/CommonServer/Services/Index.ts +++ b/CommonServer/Services/Index.ts @@ -122,5 +122,5 @@ export default [ WorkflowVariablesService, WorkflowLogService, - SmsLogService + SmsLogService, ]; diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index 47c479cb3ef..606d2da664a 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -1,4 +1,3 @@ - import { IsBillingEnabled } from '../Config'; import ObjectID from 'Common/Types/ObjectID'; import Project from 'Model/Models/Project'; @@ -9,13 +8,11 @@ import BadDataException from 'Common/Types/Exception/BadDataException'; export default class NotificationService { public static async rechargeIfBalanceIsLow( - projectId: ObjectID, + projectId: ObjectID ): Promise { let project: Project | null = null; if (projectId && IsBillingEnabled) { - - // check payment methods. - + // check payment methods. project = await ProjectService.findOneById({ id: projectId, @@ -36,8 +33,14 @@ export default class NotificationService { 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 ( + !(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 ( @@ -48,14 +51,14 @@ export default class NotificationService { if ( project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { try { // recharge balance const updatedAmount: number = Math.floor( project.smsOrCallCurrentBalanceInUSDCents + - project.autoRechargeSmsOrCallByBalanceInUSD * - 100 + project.autoRechargeSmsOrCallByBalanceInUSD * + 100 ); // If the recharge is succcessful, then update the project balance. @@ -86,7 +89,6 @@ export default class NotificationService { } } } - } return project?.smsOrCallCurrentBalanceInUSDCents || 0; diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index a6196ce847d..9a7b355cfa7 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -120,12 +120,12 @@ export class Service extends DatabaseService { updateBy: UpdateBy ): Promise> { if (IsBillingEnabled) { - - if(updateBy.data.autoRechargeSmsOrCallByBalanceInUSD) { - await NotificationService.rechargeIfBalanceIsLow(new ObjectID(updateBy.query._id! as string)); + if (updateBy.data.autoRechargeSmsOrCallByBalanceInUSD) { + await NotificationService.rechargeIfBalanceIsLow( + new ObjectID(updateBy.query._id! as string) + ); } - if (updateBy.data.paymentProviderPlanId) { // payment provider id changed. const project: Model | null = await this.findOneById({ diff --git a/CommonServer/Services/SmsLogService.ts b/CommonServer/Services/SmsLogService.ts index 3563f0e3008..f9c8b9bf78f 100644 --- a/CommonServer/Services/SmsLogService.ts +++ b/CommonServer/Services/SmsLogService.ts @@ -5,7 +5,7 @@ import DatabaseService from './DatabaseService'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); - this.hardDeleteItemsOlderThanInDays("createdAt", 30); + this.hardDeleteItemsOlderThanInDays('createdAt', 30); } } diff --git a/CommonUI/src/Components/Detail/Detail.tsx b/CommonUI/src/Components/Detail/Detail.tsx index f4ad2f7b94a..3fbe0d54d9a 100644 --- a/CommonUI/src/Components/Detail/Detail.tsx +++ b/CommonUI/src/Components/Detail/Detail.tsx @@ -74,7 +74,7 @@ const Detail: Function = (props: ComponentProps): ReactElement => { }; const getUSDCentsField: Function = (usdCents: number): ReactElement => { - return
{usdCents / 100} USD
; + return
{usdCents / 100} USD
; }; const getField: Function = (field: Field, index: number): ReactElement => { diff --git a/Dashboard/src/Pages/Settings/CallSms.tsx b/Dashboard/src/Pages/Settings/CallSms.tsx index 9c633ab4803..0d37e26f73c 100644 --- a/Dashboard/src/Pages/Settings/CallSms.tsx +++ b/Dashboard/src/Pages/Settings/CallSms.tsx @@ -42,33 +42,37 @@ const Settings: FunctionComponent = ( sideMenu={} > {/* API Key View */} - {BILLING_ENABLED ? : <>} + ], + modelId: DashboardNavigation.getProjectId()?.toString(), + }} + /> + ) : ( + <> + )} = ( }} /> - {BILLING_ENABLED ? : <>} + ]} + modelDetailProps={{ + modelType: Project, + id: 'notifications', + fields: [ + { + field: { + enableAutoRechargeSmsOrCallBalance: true, + }, + fieldType: FieldType.Boolean, + title: 'Auto Recharge Balance by (in USD)', + description: + 'Amount of balance to be recharged when the balance is low. It is in USD. ', + placeholder: 'Not Enabled', + }, + { + field: { + autoRechargeSmsOrCallByBalanceInUSD: true, + }, + fieldType: FieldType.Text, + title: 'Auto Recharge by (in USD)', + placeholder: '0 USD', + }, + { + field: { + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: + true, + }, + fieldType: FieldType.Text, + title: 'Trigger auto recharge if balance falls below (in USD)', + placeholder: '0 USD', + }, + ], + modelId: DashboardNavigation.getProjectId()?.toString(), + }} + /> + ) : ( + <> + )}
); }; diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index c16f11db624..a10a3ab1b6c 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -29,61 +29,61 @@ const SMSLogs: FunctionComponent = ( const [smsModelTitle, setSmsModalTitle] = useState(''); const [smsModelDescription, setSmsModalDescription] = useState(''); - - const modelTableColumns: Columns = [{ - field: { - _id: true, - }, - title: 'Log ID', - type: FieldType.Text, - isFilterable: true, - }, - { - field: { - toNumber: true, + const modelTableColumns: Columns = [ + { + field: { + _id: true, + }, + title: 'Log ID', + type: FieldType.Text, + isFilterable: true, }, - isFilterable: true, + { + field: { + toNumber: true, + }, + isFilterable: true, - title: 'To Number', - type: FieldType.Phone, - }, - { - field: { - createdAt: true, + title: 'To Number', + type: FieldType.Phone, }, - title: 'Sent at', - type: FieldType.DateTime, - isFilterable: true, - }, - - { - field: { - status: true, + { + field: { + createdAt: true, + }, + title: 'Sent at', + type: FieldType.DateTime, + isFilterable: true, }, - title: 'Status', - type: FieldType.Text, - getElement: (item: JSONObject): ReactElement => { - if (item['status']) { - return ( - - ); - } - return <>; + { + field: { + status: true, + }, + title: 'Status', + type: FieldType.Text, + getElement: (item: JSONObject): ReactElement => { + if (item['status']) { + return ( + + ); + } + + return <>; + }, + isFilterable: true, }, - isFilterable: true, - }]; + ]; - if(BILLING_ENABLED){ + if (BILLING_ENABLED) { modelTableColumns.push({ field: { smsCostInUSDCents: true, diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index c91cabb4b50..5dd85ed5655 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -15,7 +15,7 @@ import SmsLogService from 'CommonServer/Services/SmsLogService'; import ProjectService from 'CommonServer/Services/ProjectService'; import Project from 'Model/Models/Project'; import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; -import NotificationService from "CommonServer/Services/NotificationService" +import NotificationService from 'CommonServer/Services/NotificationService'; export default class SmsService { public static async sendSms( @@ -91,7 +91,10 @@ export default class SmsService { } // check if auto recharge is enabled and current balance is low. - const updtedBalance = await NotificationService.rechargeIfBalanceIsLow(project.id!); + const updtedBalance = + await NotificationService.rechargeIfBalanceIsLow( + project.id! + ); project.smsOrCallCurrentBalanceInUSDCents = updtedBalance; if (!project.smsOrCallCurrentBalanceInUSDCents) { diff --git a/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts b/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts index 99d84c5ffb7..96c2b09abe6 100644 --- a/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts +++ b/Workers/Jobs/HardDelete/HardDeleteItemsInDatabase.ts @@ -36,15 +36,16 @@ RunCron( } ); - RunCron( 'HardDelete:HardDeleteOlderItemsInDatabase', { schedule: IsDevelopment ? EVERY_MINUTE : EVERY_DAY, runOnStartup: false }, async () => { for (const service of Services) { if (service instanceof DatabaseService) { - - if(!service.hardDeleteItemByColumnName || !service.hardDeleteItemsOlderThanDays) { + if ( + !service.hardDeleteItemByColumnName || + !service.hardDeleteItemsOlderThanDays + ) { continue; } @@ -52,9 +53,12 @@ RunCron( // Retain data for 30 days for accidental deletion, and then hard delete. await service.hardDeleteBy({ query: { - [service.hardDeleteItemByColumnName]: QueryHelper.lessThan( - OneUptimeDate.getSomeDaysAgo(service.hardDeleteItemsOlderThanDays) - ), + [service.hardDeleteItemByColumnName]: + QueryHelper.lessThan( + OneUptimeDate.getSomeDaysAgo( + service.hardDeleteItemsOlderThanDays + ) + ), }, props: { isRoot: true, @@ -69,7 +73,3 @@ RunCron( } } ); - - - - From c018f95e70e58754d4a16281ac806d195bef7e5b Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 17:38:43 +0100 Subject: [PATCH 22/27] fix issue with dropdown --- CommonUI/src/Components/Dropdown/Dropdown.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/CommonUI/src/Components/Dropdown/Dropdown.tsx b/CommonUI/src/Components/Dropdown/Dropdown.tsx index ff5e0bd2dbc..4b831d3ea1c 100644 --- a/CommonUI/src/Components/Dropdown/Dropdown.tsx +++ b/CommonUI/src/Components/Dropdown/Dropdown.tsx @@ -33,13 +33,31 @@ export interface ComponentProps { const Dropdown: FunctionComponent = ( props: ComponentProps ): ReactElement => { + + const getDropdownOptionFromValue: Function = (value: undefined | DropdownValue | DropdownOption | Array): DropdownOption | Array => { + + if(Array.isArray(value)) { + return value; + } + + if (!Array.isArray(value) && typeof value === 'string' || typeof value === 'number') { + return props.options.find((option) => { + return option.value === value; + }) as DropdownOption | Array; + } else { + return value as DropdownOption | Array; + } + } + const [value, setValue] = useState< DropdownOption | Array | undefined - >(props.initialValue); + >(getDropdownOptionFromValue(props.initialValue)); + + useEffect(() => { if (props.value) { - setValue(props.value ? props.value : undefined); + setValue(getDropdownOptionFromValue(props.value ? props.value : undefined)); } }, [props.value]); From eeb5cf1cce77b69c4f8ac6432f8d36ece0dc8d49 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 18:00:44 +0100 Subject: [PATCH 23/27] fix fmt --- CommonServer/Services/DatabaseService.ts | 2 +- CommonServer/Services/NotificationService.ts | 26 +++++++++++---- CommonServer/Services/ProjectService.ts | 9 +++++- CommonUI/src/Components/Dropdown/Dropdown.tsx | 32 ++++++++++++------- Notification/Services/SmsService.ts | 4 +-- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/CommonServer/Services/DatabaseService.ts b/CommonServer/Services/DatabaseService.ts index ae1d6141971..565e31bfae9 100644 --- a/CommonServer/Services/DatabaseService.ts +++ b/CommonServer/Services/DatabaseService.ts @@ -111,7 +111,7 @@ class DatabaseService { public hardDeleteItemsOlderThanInDays( columnName: string, olderThan: number - ) { + ): void { this.hardDeleteItemByColumnName = columnName; this.hardDeleteItemsOlderThanDays = olderThan; } diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index 606d2da664a..3ff7bb59f6a 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -8,7 +8,11 @@ import BadDataException from 'Common/Types/Exception/BadDataException'; export default class NotificationService { public static async rechargeIfBalanceIsLow( - projectId: ObjectID + projectId: ObjectID, + options?: { + autoRechargeSmsOrCallByBalanceInUSD: number; + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: number; + } ): Promise { let project: Project | null = null; if (projectId && IsBillingEnabled) { @@ -29,6 +33,15 @@ export default class NotificationService { }, }); + const autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: number = + options?.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD || + project?.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD || + 0; + const autoRechargeSmsOrCallByBalanceInUSD: number = + options?.autoRechargeSmsOrCallByBalanceInUSD || + project?.autoRechargeSmsOrCallByBalanceInUSD || + 0; + if (!project) { return 0; } @@ -45,27 +58,26 @@ export default class NotificationService { if ( project.enableAutoRechargeSmsOrCallBalance && - project.autoRechargeSmsOrCallByBalanceInUSD && - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + autoRechargeSmsOrCallByBalanceInUSD && + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { if ( project.smsOrCallCurrentBalanceInUSDCents && project.smsOrCallCurrentBalanceInUSDCents < - project.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { try { // recharge balance const updatedAmount: number = Math.floor( project.smsOrCallCurrentBalanceInUSDCents + - project.autoRechargeSmsOrCallByBalanceInUSD * - 100 + autoRechargeSmsOrCallByBalanceInUSD * 100 ); // If the recharge is succcessful, then update the project balance. await BillingService.genrateInvoiceAndChargeCustomer( project.paymentProviderCustomerId!, 'SMS or Call Balance Recharge', - project.autoRechargeSmsOrCallByBalanceInUSD + autoRechargeSmsOrCallByBalanceInUSD ); await ProjectService.updateOneById({ diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index 9a7b355cfa7..227ea463084 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -122,7 +122,14 @@ export class Service extends DatabaseService { if (IsBillingEnabled) { if (updateBy.data.autoRechargeSmsOrCallByBalanceInUSD) { await NotificationService.rechargeIfBalanceIsLow( - new ObjectID(updateBy.query._id! as string) + new ObjectID(updateBy.query._id! as string), + { + autoRechargeSmsOrCallByBalanceInUSD: updateBy.data + .autoRechargeSmsOrCallByBalanceInUSD as number, + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: + updateBy.data + .autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD as number, + } ); } diff --git a/CommonUI/src/Components/Dropdown/Dropdown.tsx b/CommonUI/src/Components/Dropdown/Dropdown.tsx index 4b831d3ea1c..a876e5a9644 100644 --- a/CommonUI/src/Components/Dropdown/Dropdown.tsx +++ b/CommonUI/src/Components/Dropdown/Dropdown.tsx @@ -33,31 +33,39 @@ export interface ComponentProps { const Dropdown: FunctionComponent = ( props: ComponentProps ): ReactElement => { - - const getDropdownOptionFromValue: Function = (value: undefined | DropdownValue | DropdownOption | Array): DropdownOption | Array => { - - if(Array.isArray(value)) { + const getDropdownOptionFromValue: Function = ( + value: + | undefined + | DropdownValue + | DropdownOption + | Array + ): DropdownOption | Array => { + if (Array.isArray(value)) { return value; } - if (!Array.isArray(value) && typeof value === 'string' || typeof value === 'number') { - return props.options.find((option) => { + if ( + (!Array.isArray(value) && typeof value === 'string') || + typeof value === 'number' + ) { + return props.options.find((option: DropdownOption) => { return option.value === value; }) as DropdownOption | Array; - } else { - return value as DropdownOption | Array; } - } + return value as DropdownOption | Array; + }; const [value, setValue] = useState< DropdownOption | Array | undefined >(getDropdownOptionFromValue(props.initialValue)); - - useEffect(() => { if (props.value) { - setValue(getDropdownOptionFromValue(props.value ? props.value : undefined)); + setValue( + getDropdownOptionFromValue( + props.value ? props.value : undefined + ) + ); } }, [props.value]); diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 5dd85ed5655..15a9fcf2a26 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -91,11 +91,11 @@ export default class SmsService { } // check if auto recharge is enabled and current balance is low. - const updtedBalance = + const updatedBalance: number = await NotificationService.rechargeIfBalanceIsLow( project.id! ); - project.smsOrCallCurrentBalanceInUSDCents = updtedBalance; + project.smsOrCallCurrentBalanceInUSDCents = updatedBalance; if (!project.smsOrCallCurrentBalanceInUSDCents) { smsLog.status = SmsStatus.LowBalance; From 39c6e36e2fe5ca173316e9fbac1d1817f354defa Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 20:52:10 +0100 Subject: [PATCH 24/27] fix basic form --- CommonServer/Services/NotificationService.ts | 5 ++++- CommonServer/Services/ProjectService.ts | 11 ++++++----- CommonUI/src/Components/Forms/BasicForm.tsx | 7 +++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index 3ff7bb59f6a..23233d2f11b 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -12,6 +12,7 @@ export default class NotificationService { options?: { autoRechargeSmsOrCallByBalanceInUSD: number; autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: number; + enableAutoRechargeSmsOrCallBalance: boolean; } ): Promise { let project: Project | null = null; @@ -42,6 +43,8 @@ export default class NotificationService { project?.autoRechargeSmsOrCallByBalanceInUSD || 0; + const enableAutoRechargeSmsOrCallBalance: boolean = options ? options.enableAutoRechargeSmsOrCallBalance : project?.enableAutoRechargeSmsOrCallBalance || false; + if (!project) { return 0; } @@ -57,7 +60,7 @@ export default class NotificationService { } if ( - project.enableAutoRechargeSmsOrCallBalance && + enableAutoRechargeSmsOrCallBalance && autoRechargeSmsOrCallByBalanceInUSD && autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index 227ea463084..607bbbc7d4e 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -120,7 +120,7 @@ export class Service extends DatabaseService { updateBy: UpdateBy ): Promise> { if (IsBillingEnabled) { - if (updateBy.data.autoRechargeSmsOrCallByBalanceInUSD) { + if (updateBy.data.enableAutoRechargeSmsOrCallBalance) { await NotificationService.rechargeIfBalanceIsLow( new ObjectID(updateBy.query._id! as string), { @@ -129,6 +129,7 @@ export class Service extends DatabaseService { autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: updateBy.data .autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD as number, + enableAutoRechargeSmsOrCallBalance: updateBy.data.enableAutoRechargeSmsOrCallBalance as boolean, } ); } @@ -183,7 +184,7 @@ export class Service extends DatabaseService { plan, project.paymentProviderSubscriptionSeats as number, plan.getYearlyPlanId() === - updateBy.data.paymentProviderPlanId, + updateBy.data.paymentProviderPlanId, project.trialEndsAt ); @@ -722,9 +723,9 @@ export class Service extends DatabaseService { plan === PlanSelect.Free ? false : SubscriptionPlan.isUnpaid( - project.paymentProviderSubscriptionStatus || - SubscriptionStatus.Active - ), + project.paymentProviderSubscriptionStatus || + SubscriptionStatus.Active + ), }; } } diff --git a/CommonUI/src/Components/Forms/BasicForm.tsx b/CommonUI/src/Components/Forms/BasicForm.tsx index 6f52fd115f8..7bab030b5ab 100644 --- a/CommonUI/src/Components/Forms/BasicForm.tsx +++ b/CommonUI/src/Components/Forms/BasicForm.tsx @@ -270,6 +270,13 @@ const BasicForm: ForwardRefExoticComponent = forwardRef( } } + if (field.fieldType === FormFieldSchemaType.Dropdown) { + const fieldName: string = field.name!; + if ((values as any)[fieldName] && (values as any)[fieldName]['value']) { + (values as any)[fieldName] = (values as any)[fieldName]['value']; + } + } + if (field.fieldType === FormFieldSchemaType.Password) { const fieldName: string = field.name!; if ( From d3d0fa15933d5556296ad19a0b915858ed06c93d Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 20:55:57 +0100 Subject: [PATCH 25/27] dleet workflow logs older than 30 days --- CommonServer/Services/WorkflowLogService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/CommonServer/Services/WorkflowLogService.ts b/CommonServer/Services/WorkflowLogService.ts index 05b736132a4..c5761479c6b 100644 --- a/CommonServer/Services/WorkflowLogService.ts +++ b/CommonServer/Services/WorkflowLogService.ts @@ -5,6 +5,7 @@ import DatabaseService from './DatabaseService'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); + this.hardDeleteItemsOlderThanInDays("createdAt", 30); } } export default new Service(); From 457a457eeed05c56e7f37ed84d2bf3aa46261aa5 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 8 Jun 2023 21:55:48 +0100 Subject: [PATCH 26/27] fix bugs with auto recharge --- CommonServer/Services/BillingService.ts | 4 +--- CommonServer/Services/NotificationService.ts | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CommonServer/Services/BillingService.ts b/CommonServer/Services/BillingService.ts index d4e0c2557bf..67127a0b428 100644 --- a/CommonServer/Services/BillingService.ts +++ b/CommonServer/Services/BillingService.ts @@ -525,15 +525,13 @@ export class BillingService { await this.stripe.invoiceItems.create({ invoice: invoice.id, amount: amountInUsd * 100, - quantity: 1, description: itemText, - currency: 'usd', customer: customerId, }); await this.stripe.invoices.finalizeInvoice(invoice.id!); - await this.stripe.invoices.pay(invoice.id); + await this.payInvoice(customerId, invoice.id!); } public static async payInvoice( diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index 23233d2f11b..a5e8c512969 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -65,14 +65,13 @@ export default class NotificationService { autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { if ( - project.smsOrCallCurrentBalanceInUSDCents && - project.smsOrCallCurrentBalanceInUSDCents < + ((project.smsOrCallCurrentBalanceInUSDCents || 0)/100) < autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { try { // recharge balance const updatedAmount: number = Math.floor( - project.smsOrCallCurrentBalanceInUSDCents + + (project.smsOrCallCurrentBalanceInUSDCents || 0) + autoRechargeSmsOrCallByBalanceInUSD * 100 ); From d0f783150522ad1db1deec9903c051c430419f17 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Fri, 9 Jun 2023 12:25:17 +0100 Subject: [PATCH 27/27] fix notification service --- .vscode/launch.json | 4 +-- CommonServer/Services/NotificationService.ts | 8 ++++-- CommonServer/Services/ProjectService.ts | 11 ++++---- CommonServer/Services/WorkflowLogService.ts | 2 +- CommonUI/src/Components/Forms/BasicForm.tsx | 9 ++++-- CommonUI/src/Components/Table/TableRow.tsx | 11 ++++++++ Dashboard/src/Pages/Settings/SmsLog.tsx | 29 ++++++-------------- Model/Models/SmsLog.ts | 4 +++ Notification/Index.ts | 1 - Notification/Services/SmsService.ts | 3 +- Workers/Index.ts | 3 ++ Workers/Jobs/Test.ts | 22 +++++++++++++++ docker-compose.tpl.yml | 1 + 13 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 Workers/Jobs/Test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 9cf1164d95c..b950541a634 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/CommonServer/Services/NotificationService.ts b/CommonServer/Services/NotificationService.ts index a5e8c512969..498e6a27bc0 100644 --- a/CommonServer/Services/NotificationService.ts +++ b/CommonServer/Services/NotificationService.ts @@ -43,7 +43,9 @@ export default class NotificationService { project?.autoRechargeSmsOrCallByBalanceInUSD || 0; - const enableAutoRechargeSmsOrCallBalance: boolean = options ? options.enableAutoRechargeSmsOrCallBalance : project?.enableAutoRechargeSmsOrCallBalance || false; + const enableAutoRechargeSmsOrCallBalance: boolean = options + ? options.enableAutoRechargeSmsOrCallBalance + : project?.enableAutoRechargeSmsOrCallBalance || false; if (!project) { return 0; @@ -65,8 +67,8 @@ export default class NotificationService { autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { if ( - ((project.smsOrCallCurrentBalanceInUSDCents || 0)/100) < - autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD + (project.smsOrCallCurrentBalanceInUSDCents || 0) / 100 < + autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD ) { try { // recharge balance diff --git a/CommonServer/Services/ProjectService.ts b/CommonServer/Services/ProjectService.ts index 607bbbc7d4e..8816e0bfe41 100755 --- a/CommonServer/Services/ProjectService.ts +++ b/CommonServer/Services/ProjectService.ts @@ -129,7 +129,8 @@ export class Service extends DatabaseService { autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: updateBy.data .autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD as number, - enableAutoRechargeSmsOrCallBalance: updateBy.data.enableAutoRechargeSmsOrCallBalance as boolean, + enableAutoRechargeSmsOrCallBalance: updateBy.data + .enableAutoRechargeSmsOrCallBalance as boolean, } ); } @@ -184,7 +185,7 @@ export class Service extends DatabaseService { plan, project.paymentProviderSubscriptionSeats as number, plan.getYearlyPlanId() === - updateBy.data.paymentProviderPlanId, + updateBy.data.paymentProviderPlanId, project.trialEndsAt ); @@ -723,9 +724,9 @@ export class Service extends DatabaseService { plan === PlanSelect.Free ? false : SubscriptionPlan.isUnpaid( - project.paymentProviderSubscriptionStatus || - SubscriptionStatus.Active - ), + project.paymentProviderSubscriptionStatus || + SubscriptionStatus.Active + ), }; } } diff --git a/CommonServer/Services/WorkflowLogService.ts b/CommonServer/Services/WorkflowLogService.ts index c5761479c6b..cea80e90094 100644 --- a/CommonServer/Services/WorkflowLogService.ts +++ b/CommonServer/Services/WorkflowLogService.ts @@ -5,7 +5,7 @@ import DatabaseService from './DatabaseService'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); - this.hardDeleteItemsOlderThanInDays("createdAt", 30); + this.hardDeleteItemsOlderThanInDays('createdAt', 30); } } export default new Service(); diff --git a/CommonUI/src/Components/Forms/BasicForm.tsx b/CommonUI/src/Components/Forms/BasicForm.tsx index 7bab030b5ab..6b0878dec56 100644 --- a/CommonUI/src/Components/Forms/BasicForm.tsx +++ b/CommonUI/src/Components/Forms/BasicForm.tsx @@ -272,8 +272,13 @@ const BasicForm: ForwardRefExoticComponent = forwardRef( if (field.fieldType === FormFieldSchemaType.Dropdown) { const fieldName: string = field.name!; - if ((values as any)[fieldName] && (values as any)[fieldName]['value']) { - (values as any)[fieldName] = (values as any)[fieldName]['value']; + if ( + (values as any)[fieldName] && + (values as any)[fieldName]['value'] + ) { + (values as any)[fieldName] = (values as any)[ + fieldName + ]['value']; } } diff --git a/CommonUI/src/Components/Table/TableRow.tsx b/CommonUI/src/Components/Table/TableRow.tsx index c5f4a95176d..e48023290b6 100644 --- a/CommonUI/src/Components/Table/TableRow.tsx +++ b/CommonUI/src/Components/Table/TableRow.tsx @@ -101,6 +101,17 @@ const TableRow: FunctionComponent = ( ) : ( column.noValueMessage || '' ) + ) : column.type === + FieldType.USDCents ? ( + props.item[column.key] ? ( + ((props.item[ + column.key + ] as number) || 0) / + 100 + + ' USD' + ) : ( + column.noValueMessage || '0 USD' + ) ) : column.type === FieldType.Boolean ? ( props.item[column.key] ? ( diff --git a/Dashboard/src/Pages/Settings/SmsLog.tsx b/Dashboard/src/Pages/Settings/SmsLog.tsx index a10a3ab1b6c..b0a305b133f 100644 --- a/Dashboard/src/Pages/Settings/SmsLog.tsx +++ b/Dashboard/src/Pages/Settings/SmsLog.tsx @@ -10,7 +10,6 @@ import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import DashboardNavigation from '../../Utils/Navigation'; import { JSONObject } from 'Common/Types/JSON'; import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button'; -import Modal, { ModalWidth } from 'CommonUI/src/Components/Modal/Modal'; import DashboardSideMenu from './SideMenu'; import Page from 'CommonUI/src/Components/Page/Page'; import Pill from 'CommonUI/src/Components/Pill/Pill'; @@ -19,6 +18,7 @@ import { Green, Red } from 'Common/Types/BrandColors'; import { BILLING_ENABLED } from 'CommonUI/src/Config'; import Column from 'CommonUI/src/Components/ModelTable/Column'; import Columns from 'CommonUI/src/Components/ModelTable/Columns'; +import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal'; const SMSLogs: FunctionComponent = ( _props: PageComponentProps @@ -27,7 +27,6 @@ const SMSLogs: FunctionComponent = ( useState(false); const [smsText, setSmsText] = useState(''); const [smsModelTitle, setSmsModalTitle] = useState(''); - const [smsModelDescription, setSmsModalDescription] = useState(''); const modelTableColumns: Columns = [ { @@ -144,9 +143,7 @@ const SMSLogs: FunctionComponent = ( onCompleteAction: Function ) => { setSmsText(item['smsText'] as string); - setSmsModalDescription( - 'Contents of the SMS message' - ); + setSmsModalTitle('SMS Text'); setShowViewSmsTextModal(true); @@ -154,7 +151,7 @@ const SMSLogs: FunctionComponent = ( }, }, { - title: 'View Error', + title: 'View Status Message', buttonStyleType: ButtonStyleType.NORMAL, icon: IconProp.Error, onClick: async ( @@ -162,9 +159,7 @@ const SMSLogs: FunctionComponent = ( onCompleteAction: Function ) => { setSmsText(item['statusMessage'] as string); - setSmsModalDescription( - 'Here is more information about this message.' - ); + setSmsModalTitle('Status Message'); setShowViewSmsTextModal(true); @@ -188,21 +183,15 @@ const SMSLogs: FunctionComponent = ( /> {showViewSmsTextModal && ( - { setShowViewSmsTextModal(false); }} - submitButtonText={'Close'} - submitButtonStyleType={ButtonStyleType.NORMAL} - > -
-
{smsText}
; -
-
+ submitButtonText="Close" + submitButtonType={ButtonStyleType.NORMAL} + /> )} diff --git a/Model/Models/SmsLog.ts b/Model/Models/SmsLog.ts index 431edd88b0c..ba4b0f986aa 100644 --- a/Model/Models/SmsLog.ts +++ b/Model/Models/SmsLog.ts @@ -139,6 +139,7 @@ export default class SmsLog extends BaseModel { nullable: false, type: ColumnType.Phone, length: ColumnLength.Phone, + transformer: Phone.getDatabaseTransformer(), }) public toNumber?: Phone = undefined; @@ -164,6 +165,7 @@ export default class SmsLog extends BaseModel { nullable: false, type: ColumnType.Phone, length: ColumnLength.Phone, + transformer: Phone.getDatabaseTransformer(), }) public fromNumber?: Phone = undefined; @@ -255,9 +257,11 @@ export default class SmsLog extends BaseModel { title: 'SMS Cost', description: 'SMS Cost in USD Cents', canReadOnPopulate: false, + isDefaultValueColumn: true, }) @Column({ nullable: false, + default: 0, type: ColumnType.Number, }) public smsCostInUSDCents?: number = undefined; diff --git a/Notification/Index.ts b/Notification/Index.ts index 5aa709f244d..349106bd060 100644 --- a/Notification/Index.ts +++ b/Notification/Index.ts @@ -9,7 +9,6 @@ import SmsAPI from './API/SMS'; import SMTPConfigAPI from './API/SMTPConfig'; import logger from 'CommonServer/Utils/Logger'; import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase'; - // import handlebars loader. import './Utils/Handlebars'; diff --git a/Notification/Services/SmsService.ts b/Notification/Services/SmsService.ts index 15a9fcf2a26..8abce3f6421 100644 --- a/Notification/Services/SmsService.ts +++ b/Notification/Services/SmsService.ts @@ -42,8 +42,9 @@ export default class SmsService { const smsLog: SmsLog = new SmsLog(); smsLog.toNumber = to; - smsLog.fromNumber = options.from; + smsLog.fromNumber = options.from || new Phone(TwilioPhoneNumber); smsLog.smsText = message; + smsLog.smsCostInUSDCents = 0; if (options.projectId) { smsLog.projectId = options.projectId; diff --git a/Workers/Index.ts b/Workers/Index.ts index 748a1ab700f..c67443a72ef 100644 --- a/Workers/Index.ts +++ b/Workers/Index.ts @@ -59,6 +59,9 @@ import './Jobs/StatusPageOwners/SendCreatedResourceEmail'; import './Jobs/StatusPageOwners/SendOwnerAddedEmail'; import './Jobs/StatusPageOwners/SendAnnouncementCreatedEmail'; +// Test Worker +import './Jobs/Test'; + const APP_NAME: string = 'workers'; const app: ExpressApplication = Express.getExpressApp(); diff --git a/Workers/Jobs/Test.ts b/Workers/Jobs/Test.ts new file mode 100644 index 00000000000..f14145e98be --- /dev/null +++ b/Workers/Jobs/Test.ts @@ -0,0 +1,22 @@ +import { IsDevelopment } from 'CommonServer/Config'; +import RunCron from '../Utils/Cron'; +import { EVERY_DAY, EVERY_MINUTE } from 'Common/Utils/CronTime'; +import SMSService from 'CommonServer/Services/SMSService'; +import Phone from 'Common/Types/Phone'; +import ObjectID from 'Common/Types/ObjectID'; + +const number = 1; + +RunCron( + 'TestWorker', + { schedule: IsDevelopment ? EVERY_MINUTE : EVERY_DAY, runOnStartup: false }, + async () => { + SMSService.sendSms( + new Phone('+15853641376'), + 'Test message from worker' + number, + { + projectId: new ObjectID('e1308f0f-364d-4f2c-8565-0d01fc25f8fe'), + } + ); + } +); diff --git a/docker-compose.tpl.yml b/docker-compose.tpl.yml index b4ad543a223..a96ed9ba78f 100644 --- a/docker-compose.tpl.yml +++ b/docker-compose.tpl.yml @@ -663,6 +663,7 @@ services: - dashboard-api - dashboard - home + - notification restart: always {{ if or (eq .Env.ENVIRONMENT "development") (eq .Env.ENVIRONMENT "ci") }} build: