@oneuptime/common 7.0.3038 → 7.0.3050

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/Models/DatabaseModels/ScheduledMaintenance.ts +70 -0
  2. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +35 -0
  3. package/Server/EnvironmentConfig.ts +12 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1725975175669-MigrationName.ts +29 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1725976810107-MigrationName.ts +17 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/IncidentService.ts +45 -0
  8. package/Server/Services/IncidentStateService.ts +47 -6
  9. package/Server/Services/ProjectService.ts +14 -0
  10. package/Server/Services/ScheduledMaintenanceService.ts +307 -0
  11. package/Types/Database/DatabaseProperty.ts +6 -3
  12. package/Types/Events/Recurring.ts +86 -41
  13. package/Types/SerializableObjectDictionary.ts +2 -0
  14. package/UI/Components/Events/RecurringArrayFieldElement.tsx +104 -0
  15. package/UI/Components/Events/RecurringArrayViewElement.tsx +37 -0
  16. package/UI/Components/Events/RecurringViewElement.tsx +2 -0
  17. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +73 -0
  18. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  19. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +36 -0
  20. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  21. package/build/dist/Server/EnvironmentConfig.js +2 -0
  22. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  23. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1725975175669-MigrationName.js +16 -0
  24. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1725975175669-MigrationName.js.map +1 -0
  25. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1725976810107-MigrationName.js +12 -0
  26. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1725976810107-MigrationName.js.map +1 -0
  27. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  28. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  29. package/build/dist/Server/Services/IncidentService.js +34 -0
  30. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  31. package/build/dist/Server/Services/IncidentStateService.js +25 -2
  32. package/build/dist/Server/Services/IncidentStateService.js.map +1 -1
  33. package/build/dist/Server/Services/ProjectService.js +5 -0
  34. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  35. package/build/dist/Server/Services/ScheduledMaintenanceService.js +208 -0
  36. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  37. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  38. package/build/dist/Types/Events/Recurring.js +47 -20
  39. package/build/dist/Types/Events/Recurring.js.map +1 -1
  40. package/build/dist/Types/SerializableObjectDictionary.js +2 -0
  41. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  42. package/build/dist/UI/Components/Events/RecurringArrayFieldElement.js +49 -0
  43. package/build/dist/UI/Components/Events/RecurringArrayFieldElement.js.map +1 -0
  44. package/build/dist/UI/Components/Events/RecurringArrayViewElement.js +18 -0
  45. package/build/dist/UI/Components/Events/RecurringArrayViewElement.js.map +1 -0
  46. package/build/dist/UI/Components/Events/RecurringViewElement.js +2 -1
  47. package/build/dist/UI/Components/Events/RecurringViewElement.js.map +1 -1
  48. package/package.json +2 -2
@@ -34,6 +34,7 @@ import {
34
34
  ManyToMany,
35
35
  ManyToOne,
36
36
  } from "typeorm";
37
+ import Recurring from "../../Types/Events/Recurring";
37
38
 
38
39
  @EnableDocumentation()
39
40
  @AccessControlColumn("labels")
@@ -880,4 +881,73 @@ export default class ScheduledMaintenance extends BaseModel {
880
881
  default: false,
881
882
  })
882
883
  public isOwnerNotifiedOfResourceCreation?: boolean = undefined;
884
+
885
+ @ColumnAccessControl({
886
+ create: [
887
+ Permission.ProjectOwner,
888
+ Permission.ProjectAdmin,
889
+ Permission.ProjectMember,
890
+ Permission.CreateProjectScheduledMaintenance,
891
+ ],
892
+ read: [
893
+ Permission.ProjectOwner,
894
+ Permission.ProjectAdmin,
895
+ Permission.ProjectMember,
896
+ Permission.ReadProjectScheduledMaintenance,
897
+ ],
898
+ update: [
899
+ Permission.ProjectOwner,
900
+ Permission.ProjectAdmin,
901
+ Permission.ProjectMember,
902
+ Permission.EditProjectScheduledMaintenance,
903
+ ],
904
+ })
905
+ @TableColumn({
906
+ type: TableColumnType.JSON,
907
+ required: false,
908
+ isDefaultValueColumn: false,
909
+ title: "Subscriber notifications before the event",
910
+ description: "Should subscribers be notified before the event?",
911
+ })
912
+ @Column({
913
+ type: ColumnType.JSON,
914
+ nullable: true,
915
+ transformer: Recurring.getDatabaseTransformer(),
916
+ })
917
+ public sendSubscriberNotificationsOnBeforeTheEvent?: Array<Recurring> =
918
+ undefined;
919
+
920
+ @ColumnAccessControl({
921
+ create: [
922
+ Permission.ProjectOwner,
923
+ Permission.ProjectAdmin,
924
+ Permission.ProjectMember,
925
+ Permission.CreateProjectScheduledMaintenance,
926
+ ],
927
+ read: [
928
+ Permission.ProjectOwner,
929
+ Permission.ProjectAdmin,
930
+ Permission.ProjectMember,
931
+ Permission.ReadProjectScheduledMaintenance,
932
+ ],
933
+ update: [
934
+ Permission.ProjectOwner,
935
+ Permission.ProjectAdmin,
936
+ Permission.ProjectMember,
937
+ Permission.EditProjectScheduledMaintenance,
938
+ ],
939
+ })
940
+ @TableColumn({
941
+ type: TableColumnType.Date,
942
+ required: false,
943
+ isDefaultValueColumn: false,
944
+ title: "Next subscriber notification before the event at?",
945
+ description: "When will the next notification to subscribers be sent out?",
946
+ })
947
+ @Index()
948
+ @Column({
949
+ type: ColumnType.Date,
950
+ nullable: true,
951
+ })
952
+ public nextSubscriberNotificationBeforeTheEventAt?: Date = undefined;
883
953
  }
@@ -950,4 +950,39 @@ export default class ScheduledMaintenanceTemplate extends BaseModel {
950
950
  nullable: true,
951
951
  })
952
952
  public customFields?: JSONObject = undefined;
953
+
954
+ @ColumnAccessControl({
955
+ create: [
956
+ Permission.ProjectOwner,
957
+ Permission.ProjectAdmin,
958
+ Permission.ProjectMember,
959
+ Permission.CreateScheduledMaintenanceTemplate,
960
+ ],
961
+ read: [
962
+ Permission.ProjectOwner,
963
+ Permission.ProjectAdmin,
964
+ Permission.ProjectMember,
965
+ Permission.ReadScheduledMaintenanceTemplate,
966
+ ],
967
+ update: [
968
+ Permission.ProjectOwner,
969
+ Permission.ProjectAdmin,
970
+ Permission.ProjectMember,
971
+ Permission.EditScheduledMaintenanceTemplate,
972
+ ],
973
+ })
974
+ @TableColumn({
975
+ type: TableColumnType.JSON,
976
+ required: false,
977
+ isDefaultValueColumn: false,
978
+ title: "Subscriber notifications before the event",
979
+ description: "Should subscribers be notified before the event?",
980
+ })
981
+ @Column({
982
+ type: ColumnType.JSON,
983
+ nullable: true,
984
+ transformer: Recurring.getDatabaseTransformer(),
985
+ })
986
+ public sendSubscriberNotificationsOnBeforeTheEvent?: Array<Recurring> =
987
+ undefined;
953
988
  }
@@ -100,6 +100,18 @@ export const IsolatedVMHostname: Hostname = Hostname.fromString(
100
100
  }`,
101
101
  );
102
102
 
103
+ export const WorkerHostname: Hostname = Hostname.fromString(
104
+ `${process.env["SERVER_WORKER_HOSTNAME"] || "localhost"}:${
105
+ process.env["WORKER_PORT"] || 80
106
+ }`,
107
+ );
108
+
109
+ export const HomeHostname: Hostname = Hostname.fromString(
110
+ `${process.env["SERVER_HOME_HOSTNAME"] || "localhost"}:${
111
+ process.env["HOME_PORT"] || 80
112
+ }`,
113
+ );
114
+
103
115
  export const AccountsHostname: Hostname = Hostname.fromString(
104
116
  `${process.env["SERVER_ACCOUNTS_HOSTNAME"] || "localhost"}:${
105
117
  process.env["ACCOUNTS_PORT"] || 80
@@ -0,0 +1,29 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1725975175669 implements MigrationInterface {
4
+ public name = "MigrationName1725975175669";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "ScheduledMaintenance" ADD "sendSubscriberNotificationsOnBeforeTheEvent" jsonb`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "ScheduledMaintenance" ADD "nextSubscriberNotificationBeforeTheEventAt" TIMESTAMP WITH TIME ZONE`,
12
+ );
13
+ await queryRunner.query(
14
+ `CREATE INDEX "IDX_37b2094ce25cc62b4766a7d3b1" ON "ScheduledMaintenance" ("nextSubscriberNotificationBeforeTheEventAt") `,
15
+ );
16
+ }
17
+
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ await queryRunner.query(
20
+ `DROP INDEX "public"."IDX_37b2094ce25cc62b4766a7d3b1"`,
21
+ );
22
+ await queryRunner.query(
23
+ `ALTER TABLE "ScheduledMaintenance" DROP COLUMN "nextSubscriberNotificationBeforeTheEventAt"`,
24
+ );
25
+ await queryRunner.query(
26
+ `ALTER TABLE "ScheduledMaintenance" DROP COLUMN "sendSubscriberNotificationsOnBeforeTheEvent"`,
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,17 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1725976810107 implements MigrationInterface {
4
+ public name = "MigrationName1725976810107";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "ScheduledMaintenanceTemplate" ADD "sendSubscriberNotificationsOnBeforeTheEvent" jsonb`,
9
+ );
10
+ }
11
+
12
+ public async down(queryRunner: QueryRunner): Promise<void> {
13
+ await queryRunner.query(
14
+ `ALTER TABLE "ScheduledMaintenanceTemplate" DROP COLUMN "sendSubscriberNotificationsOnBeforeTheEvent"`,
15
+ );
16
+ }
17
+ }
@@ -58,6 +58,8 @@ import { MigrationName1725884177663 } from "./1725884177663-MigrationName";
58
58
  import { MigrationName1725898621366 } from "./1725898621366-MigrationName";
59
59
  import { MigrationName1725900315712 } from "./1725900315712-MigrationName";
60
60
  import { MigrationName1725901024444 } from "./1725901024444-MigrationName";
61
+ import { MigrationName1725975175669 } from "./1725975175669-MigrationName";
62
+ import { MigrationName1725976810107 } from "./1725976810107-MigrationName";
61
63
 
62
64
  export default [
63
65
  InitialMigration,
@@ -120,4 +122,6 @@ export default [
120
122
  MigrationName1725898621366,
121
123
  MigrationName1725900315712,
122
124
  MigrationName1725901024444,
125
+ MigrationName1725975175669,
126
+ MigrationName1725976810107,
123
127
  ];
@@ -40,6 +40,51 @@ export class Service extends DatabaseService<Model> {
40
40
  this.hardDeleteItemsOlderThanInDays("createdAt", 120);
41
41
  }
42
42
 
43
+ public async isIncidentAcknowledged(data: {
44
+ incidentId: ObjectID;
45
+ }): Promise<boolean> {
46
+ const incident: Model | null = await this.findOneBy({
47
+ query: {
48
+ _id: data.incidentId,
49
+ },
50
+ select: {
51
+ projectId: true,
52
+ currentIncidentState: {
53
+ order: true,
54
+ },
55
+ },
56
+ props: {
57
+ isRoot: true,
58
+ },
59
+ });
60
+
61
+ if (!incident) {
62
+ throw new BadDataException("Incident not found");
63
+ }
64
+
65
+ if (!incident.projectId) {
66
+ throw new BadDataException("Incient Project ID not found");
67
+ }
68
+
69
+ const ackIncidentState: IncidentState =
70
+ await IncidentStateService.getAcknowledgedIncidentState({
71
+ projectId: incident.projectId,
72
+ props: {
73
+ isRoot: true,
74
+ },
75
+ });
76
+
77
+ const currentIncidentStateOrder: number =
78
+ incident.currentIncidentState!.order!;
79
+ const ackIncidentStateOrder: number = ackIncidentState.order!;
80
+
81
+ if (currentIncidentStateOrder >= ackIncidentStateOrder) {
82
+ return true;
83
+ }
84
+
85
+ return false;
86
+ }
87
+
43
88
  public async acknowledgeIncident(
44
89
  incidentId: ObjectID,
45
90
  acknowledgedByUserId: ObjectID,
@@ -152,13 +152,13 @@ export class Service extends DatabaseService<IncidentState> {
152
152
  }
153
153
  }
154
154
 
155
- public async getUnresolvedIncidentStates(
156
- projectId: ObjectID,
157
- props: DatabaseCommonInteractionProps,
158
- ): Promise<IncidentState[]> {
155
+ public async getAllIncidentStates(data: {
156
+ projectId: ObjectID;
157
+ props: DatabaseCommonInteractionProps;
158
+ }): Promise<Array<IncidentState>> {
159
159
  const incidentStates: Array<IncidentState> = await this.findBy({
160
160
  query: {
161
- projectId: projectId,
161
+ projectId: data.projectId,
162
162
  },
163
163
  skip: 0,
164
164
  limit: LIMIT_MAX,
@@ -168,10 +168,26 @@ export class Service extends DatabaseService<IncidentState> {
168
168
  select: {
169
169
  _id: true,
170
170
  isResolvedState: true,
171
+ isAcknowledgedState: true,
172
+ isCreatedState: true,
173
+ order: true,
171
174
  },
172
- props: props,
175
+ props: data.props,
173
176
  });
174
177
 
178
+ return incidentStates;
179
+ }
180
+
181
+ public async getUnresolvedIncidentStates(
182
+ projectId: ObjectID,
183
+ props: DatabaseCommonInteractionProps,
184
+ ): Promise<IncidentState[]> {
185
+ const incidentStates: Array<IncidentState> =
186
+ await this.getAllIncidentStates({
187
+ projectId: projectId,
188
+ props: props,
189
+ });
190
+
175
191
  const unresolvedIncidentStates: Array<IncidentState> = [];
176
192
 
177
193
  for (const state of incidentStates) {
@@ -184,5 +200,30 @@ export class Service extends DatabaseService<IncidentState> {
184
200
 
185
201
  return unresolvedIncidentStates;
186
202
  }
203
+
204
+ public async getAcknowledgedIncidentState(data: {
205
+ projectId: ObjectID;
206
+ props: DatabaseCommonInteractionProps;
207
+ }): Promise<IncidentState> {
208
+ const incidentStates: Array<IncidentState> =
209
+ await this.getAllIncidentStates({
210
+ projectId: data.projectId,
211
+ props: data.props,
212
+ });
213
+
214
+ const ackIncidentState: IncidentState | undefined = incidentStates.find(
215
+ (incidentState: IncidentState) => {
216
+ return incidentState?.isAcknowledgedState;
217
+ },
218
+ );
219
+
220
+ if (!ackIncidentState) {
221
+ throw new BadDataException(
222
+ "Acknowledged Incident State not found for this project",
223
+ );
224
+ }
225
+
226
+ return ackIncidentState;
227
+ }
187
228
  }
188
229
  export default new Service();
@@ -347,6 +347,17 @@ export class ProjectService extends DatabaseService<Model> {
347
347
  " completed.",
348
348
  );
349
349
 
350
+ // refresh subscription status.
351
+ const subscriptionState: SubscriptionStatus =
352
+ await BillingService.getSubscriptionStatus(
353
+ subscription.subscriptionId as string,
354
+ );
355
+
356
+ const meteredSubscriptionState: SubscriptionStatus =
357
+ await BillingService.getSubscriptionStatus(
358
+ subscription.meteredSubscriptionId as string,
359
+ );
360
+
350
361
  await this.updateOneById({
351
362
  id: new ObjectID(updateBy.query._id! as string),
352
363
  data: {
@@ -357,6 +368,9 @@ export class ProjectService extends DatabaseService<Model> {
357
368
  planName: SubscriptionPlan.getPlanType(
358
369
  updateBy.data.paymentProviderPlanId! as string,
359
370
  ),
371
+ paymentProviderMeteredSubscriptionStatus:
372
+ meteredSubscriptionState,
373
+ paymentProviderSubscriptionStatus: subscriptionState,
360
374
  },
361
375
  props: {
362
376
  isRoot: true,
@@ -23,6 +23,28 @@ import ScheduledMaintenanceOwnerUser from "Common/Models/DatabaseModels/Schedule
23
23
  import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMaintenanceState";
24
24
  import ScheduledMaintenanceStateTimeline from "Common/Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
25
25
  import User from "Common/Models/DatabaseModels/User";
26
+ import Recurring from "../../Types/Events/Recurring";
27
+ import OneUptimeDate from "../../Types/Date";
28
+ import UpdateBy from "../Types/Database/UpdateBy";
29
+ import { FileRoute } from "Common/ServiceRoute";
30
+ import Dictionary from "Common/Types/Dictionary";
31
+ import EmailTemplateType from "Common/Types/Email/EmailTemplateType";
32
+ import SMS from "Common/Types/SMS/SMS";
33
+ import MailService from "Common/Server/Services/MailService";
34
+ import ProjectCallSMSConfigService from "Common/Server/Services/ProjectCallSMSConfigService";
35
+ import ProjectSmtpConfigService from "Common/Server/Services/ProjectSmtpConfigService";
36
+ import SmsService from "Common/Server/Services/SmsService";
37
+ import StatusPageResourceService from "Common/Server/Services/StatusPageResourceService";
38
+ import StatusPageService from "Common/Server/Services/StatusPageService";
39
+ import StatusPageSubscriberService from "Common/Server/Services/StatusPageSubscriberService";
40
+ import QueryHelper from "Common/Server/Types/Database/QueryHelper";
41
+ import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown";
42
+ import logger from "Common/Server/Utils/Logger";
43
+ import StatusPage from "Common/Models/DatabaseModels/StatusPage";
44
+ import StatusPageResource from "Common/Models/DatabaseModels/StatusPageResource";
45
+ import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
46
+ import Hostname from "../../Types/API/Hostname";
47
+ import Protocol from "../../Types/API/Protocol";
26
48
 
27
49
  export class Service extends DatabaseService<Model> {
28
50
  public constructor() {
@@ -30,6 +52,242 @@ export class Service extends DatabaseService<Model> {
30
52
  this.hardDeleteItemsOlderThanInDays("createdAt", 120);
31
53
  }
32
54
 
55
+ public async notififySubscribersOnEventScheduled(
56
+ scheduledEvents: Array<Model>,
57
+ ): Promise<void> {
58
+ const host: Hostname = await DatabaseConfig.getHost();
59
+ const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
60
+
61
+ for (const event of scheduledEvents) {
62
+ // get status page resources from monitors.
63
+
64
+ let statusPageResources: Array<StatusPageResource> = [];
65
+
66
+ if (event.monitors && event.monitors.length > 0) {
67
+ statusPageResources = await StatusPageResourceService.findBy({
68
+ query: {
69
+ monitorId: QueryHelper.any(
70
+ event.monitors
71
+ .filter((m: Monitor) => {
72
+ return m._id;
73
+ })
74
+ .map((m: Monitor) => {
75
+ return new ObjectID(m._id!);
76
+ }),
77
+ ),
78
+ },
79
+ props: {
80
+ isRoot: true,
81
+ ignoreHooks: true,
82
+ },
83
+ skip: 0,
84
+ limit: LIMIT_PER_PROJECT,
85
+ select: {
86
+ _id: true,
87
+ displayName: true,
88
+ statusPageId: true,
89
+ },
90
+ });
91
+ }
92
+
93
+ const statusPageToResources: Dictionary<Array<StatusPageResource>> = {};
94
+
95
+ for (const resource of statusPageResources) {
96
+ if (!resource.statusPageId) {
97
+ continue;
98
+ }
99
+
100
+ if (!statusPageToResources[resource.statusPageId?.toString()]) {
101
+ statusPageToResources[resource.statusPageId?.toString()] = [];
102
+ }
103
+
104
+ statusPageToResources[resource.statusPageId?.toString()]?.push(
105
+ resource,
106
+ );
107
+ }
108
+
109
+ const statusPages: Array<StatusPage> =
110
+ await StatusPageSubscriberService.getStatusPagesToSendNotification(
111
+ event.statusPages?.map((i: StatusPage) => {
112
+ return i.id!;
113
+ }) || [],
114
+ );
115
+
116
+ for (const statuspage of statusPages) {
117
+ if (!statuspage.id) {
118
+ continue;
119
+ }
120
+
121
+ const subscribers: Array<StatusPageSubscriber> =
122
+ await StatusPageSubscriberService.getSubscribersByStatusPage(
123
+ statuspage.id!,
124
+ {
125
+ isRoot: true,
126
+ ignoreHooks: true,
127
+ },
128
+ );
129
+
130
+ const statusPageURL: string = await StatusPageService.getStatusPageURL(
131
+ statuspage.id,
132
+ );
133
+
134
+ const statusPageName: string =
135
+ statuspage.pageTitle || statuspage.name || "Status Page";
136
+
137
+ // Send email to Email subscribers.
138
+
139
+ const resourcesAffected: string =
140
+ statusPageToResources[statuspage._id!]
141
+ ?.map((r: StatusPageResource) => {
142
+ return r.displayName;
143
+ })
144
+ .join(", ") || "";
145
+
146
+ for (const subscriber of subscribers) {
147
+ if (!subscriber._id) {
148
+ continue;
149
+ }
150
+
151
+ const shouldNotifySubscriber: boolean =
152
+ StatusPageSubscriberService.shouldSendNotification({
153
+ subscriber: subscriber,
154
+ statusPageResources: statusPageToResources[statuspage._id!] || [],
155
+ statusPage: statuspage,
156
+ });
157
+
158
+ if (!shouldNotifySubscriber) {
159
+ continue;
160
+ }
161
+
162
+ const unsubscribeUrl: string =
163
+ StatusPageSubscriberService.getUnsubscribeLink(
164
+ URL.fromString(statusPageURL),
165
+ subscriber.id!,
166
+ ).toString();
167
+
168
+ if (subscriber.subscriberPhone) {
169
+ const sms: SMS = {
170
+ message: `
171
+ Scheduled Maintenance - ${statusPageName}
172
+
173
+ ${event.title || ""}
174
+
175
+ ${
176
+ resourcesAffected
177
+ ? "Resources Affected: " + resourcesAffected
178
+ : ""
179
+ }
180
+
181
+ To view this event, visit ${statusPageURL}
182
+
183
+ To update notification preferences or unsubscribe, visit ${unsubscribeUrl}
184
+ `,
185
+ to: subscriber.subscriberPhone,
186
+ };
187
+
188
+ // send sms here.
189
+ SmsService.sendSms(sms, {
190
+ projectId: statuspage.projectId,
191
+ customTwilioConfig: ProjectCallSMSConfigService.toTwilioConfig(
192
+ statuspage.callSmsConfig,
193
+ ),
194
+ }).catch((err: Error) => {
195
+ logger.error(err);
196
+ });
197
+ }
198
+
199
+ if (subscriber.subscriberEmail) {
200
+ // send email here.
201
+
202
+ MailService.sendMail(
203
+ {
204
+ toEmail: subscriber.subscriberEmail,
205
+ templateType:
206
+ EmailTemplateType.SubscriberScheduledMaintenanceEventCreated,
207
+ vars: {
208
+ statusPageName: statusPageName,
209
+ statusPageUrl: statusPageURL,
210
+ logoUrl: statuspage.logoFileId
211
+ ? new URL(httpProtocol, host)
212
+ .addRoute(FileRoute)
213
+ .addRoute("/image/" + statuspage.logoFileId)
214
+ .toString()
215
+ : "",
216
+ isPublicStatusPage: statuspage.isPublicStatusPage
217
+ ? "true"
218
+ : "false",
219
+ resourcesAffected: resourcesAffected,
220
+ scheduledAt:
221
+ OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
222
+ date: event.startsAt!,
223
+ timezones: statuspage.subscriberTimezones || [],
224
+ }),
225
+ eventTitle: event.title || "",
226
+ eventDescription: await Markdown.convertToHTML(
227
+ event.description || "",
228
+ MarkdownContentType.Email,
229
+ ),
230
+ unsubscribeUrl: unsubscribeUrl,
231
+ },
232
+ subject: "[Scheduled Maintenance] " + statusPageName,
233
+ },
234
+ {
235
+ mailServer: ProjectSmtpConfigService.toEmailServer(
236
+ statuspage.smtpConfig,
237
+ ),
238
+ projectId: statuspage.projectId!,
239
+ },
240
+ ).catch((err: Error) => {
241
+ logger.error(err);
242
+ });
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ protected override async onBeforeUpdate(
250
+ updateBy: UpdateBy<Model>,
251
+ ): Promise<OnUpdate<Model>> {
252
+ if (
253
+ updateBy.query.id &&
254
+ updateBy.data.sendSubscriberNotificationsOnBeforeTheEvent
255
+ ) {
256
+ const scheduledMaintenance: Model | null = await this.findOneById({
257
+ id: updateBy.query.id! as ObjectID,
258
+ select: {
259
+ startsAt: true,
260
+ },
261
+ props: {
262
+ isRoot: true,
263
+ },
264
+ });
265
+
266
+ if (!scheduledMaintenance) {
267
+ throw new BadDataException("Scheduled Maintennace Event not found");
268
+ }
269
+
270
+ const startsAt: Date =
271
+ (updateBy.data.startsAt as Date) ||
272
+ (scheduledMaintenance.startsAt! as Date);
273
+
274
+ const nextTimeToNotifyBeforeTheEvent: Date | null =
275
+ this.getNextTimeToNotify({
276
+ eventScheduledDate: startsAt,
277
+ sendSubscriberNotifiationsOn: updateBy.data
278
+ .sendSubscriberNotificationsOnBeforeTheEvent as Array<Recurring>,
279
+ });
280
+
281
+ updateBy.data.nextSubscriberNotificationBeforeTheEventAt =
282
+ nextTimeToNotifyBeforeTheEvent;
283
+ }
284
+
285
+ return {
286
+ updateBy,
287
+ carryForward: null,
288
+ };
289
+ }
290
+
33
291
  protected override async onBeforeDelete(
34
292
  deleteBy: DeleteBy<Model>,
35
293
  ): Promise<OnDelete<Model>> {
@@ -73,6 +331,36 @@ export class Service extends DatabaseService<Model> {
73
331
  return onDelete;
74
332
  }
75
333
 
334
+ public getNextTimeToNotify(data: {
335
+ eventScheduledDate: Date;
336
+ sendSubscriberNotifiationsOn: Array<Recurring>;
337
+ }): Date | null {
338
+ let recurringDate: Date | null = null;
339
+
340
+ for (const recurringItem of data.sendSubscriberNotifiationsOn) {
341
+ const notificationDate: Date = Recurring.getNextDateInterval(
342
+ data.eventScheduledDate,
343
+ recurringItem,
344
+ true,
345
+ );
346
+
347
+ // if this date is in the future. set it to recurring date.
348
+ if (OneUptimeDate.isInTheFuture(notificationDate)) {
349
+ recurringDate = notificationDate;
350
+ }
351
+
352
+ // if this new date is less than the recurring date then set it to recuring date. We need to get the least date.
353
+
354
+ if (recurringDate) {
355
+ if (OneUptimeDate.isBefore(notificationDate, recurringDate)) {
356
+ recurringDate = notificationDate;
357
+ }
358
+ }
359
+ }
360
+
361
+ return recurringDate;
362
+ }
363
+
76
364
  protected override async onBeforeCreate(
77
365
  createBy: CreateBy<Model>,
78
366
  ): Promise<OnCreate<Model>> {
@@ -105,6 +393,25 @@ export class Service extends DatabaseService<Model> {
105
393
  createBy.data.currentScheduledMaintenanceStateId =
106
394
  scheduledMaintenanceState.id;
107
395
 
396
+ // get next notification date.
397
+
398
+ if (
399
+ createBy.data.sendSubscriberNotificationsOnBeforeTheEvent &&
400
+ createBy.data.startsAt
401
+ ) {
402
+ const nextNotificationDate: Date | null = this.getNextTimeToNotify({
403
+ eventScheduledDate: createBy.data.startsAt,
404
+ sendSubscriberNotifiationsOn:
405
+ createBy.data.sendSubscriberNotificationsOnBeforeTheEvent,
406
+ });
407
+
408
+ if (nextNotificationDate) {
409
+ // set this.
410
+ createBy.data.nextSubscriberNotificationBeforeTheEventAt =
411
+ nextNotificationDate;
412
+ }
413
+ }
414
+
108
415
  return { createBy, carryForward: null };
109
416
  }
110
417