@oneuptime/common 12.0.17 → 12.0.19

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 (220) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  11. package/Server/API/UserSlackAPI.ts +125 -0
  12. package/Server/EnvironmentConfig.ts +12 -107
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Infrastructure/Queue.ts +7 -0
  19. package/Server/Services/BillingService.ts +8 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +67 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  40. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  41. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  42. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  43. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  44. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
  45. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  46. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  47. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  48. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  49. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  50. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  51. package/Tests/Server/Services/BillingService.test.ts +20 -0
  52. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  53. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  54. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  55. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  56. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  57. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  58. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  59. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  60. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  61. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  62. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  63. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  64. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  65. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  66. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  67. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  68. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  69. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  70. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  71. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  72. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  73. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  74. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  75. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  76. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  77. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  78. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  79. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  80. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  81. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  82. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  83. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  84. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  85. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  86. package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
  87. package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
  88. package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
  90. package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
  91. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  92. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  93. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
  94. package/Types/Billing/PayAsYouGoPricing.ts +1 -1
  95. package/Types/Marketing/MarketingEvent.ts +97 -0
  96. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  97. package/UI/Utils/PermissionGate.ts +56 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  101. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  103. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  105. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  106. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  107. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  109. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  111. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  113. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  114. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  115. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  116. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  117. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  118. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  119. package/build/dist/Server/EnvironmentConfig.js +10 -70
  120. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  131. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  132. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  133. package/build/dist/Server/Services/BillingService.js +6 -0
  134. package/build/dist/Server/Services/BillingService.js.map +1 -1
  135. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  136. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  137. package/build/dist/Server/Services/Index.js +6 -2
  138. package/build/dist/Server/Services/Index.js.map +1 -1
  139. package/build/dist/Server/Services/LlmLogService.js +25 -9
  140. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  141. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  142. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  143. package/build/dist/Server/Services/ProjectService.js +56 -0
  144. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  145. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  146. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  147. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  148. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  149. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  150. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  151. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  152. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  153. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  154. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  155. package/build/dist/Server/Services/UserService.js +26 -1
  156. package/build/dist/Server/Services/UserService.js.map +1 -1
  157. package/build/dist/Server/Services/UserSlackService.js +195 -0
  158. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  159. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  160. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  161. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  162. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  163. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  164. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  165. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  166. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  167. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  168. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  169. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  170. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  171. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  172. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  173. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  174. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  175. package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
  176. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
  177. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  178. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  179. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  180. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  181. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  182. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  183. package/package.json +1 -1
  184. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  185. package/Server/Services/MarketingConversionService.ts +0 -10
  186. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  187. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  188. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  189. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  190. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  191. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  192. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  193. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  194. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  195. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  196. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  197. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  198. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  199. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  200. package/Types/Marketing/MarketingConversion.ts +0 -53
  201. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  202. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  203. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  204. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  205. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  206. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  207. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  208. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  209. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  210. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  211. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  212. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  213. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  214. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  215. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  216. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  217. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  218. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  219. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  220. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -540,6 +540,10 @@ import { AddLlmModelPrice1788300000000 } from "./1788300000000-AddLlmModelPrice"
540
540
  import { AddMarketingConversionAttribution1788400000000 } from "./1788400000000-AddMarketingConversionAttribution";
541
541
  import { RemoveLlmCostBudgetAlertColumns1788500000000 } from "./1788500000000-RemoveLlmCostBudgetAlertColumns";
542
542
  import { AddDetectionRuleIncidentColumns1788600000000 } from "./1788600000000-AddDetectionRuleIncidentColumns";
543
+ import { RemoveMarketingConversionUploadState1788700000000 } from "./1788700000000-RemoveMarketingConversionUploadState";
544
+ import { DropMarketingConversionAddEnterpriseLicenseEmail1788800000000 } from "./1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail";
545
+ import { RedactStoredMonitorIngestSecrets1788900000000 } from "./1788900000000-RedactStoredMonitorIngestSecrets";
546
+ import { AddUserSlackAndMicrosoftTeams1789000000000 } from "./1789000000000-AddUserSlackAndMicrosoftTeams";
543
547
  import { MigrationName1787142779538 } from "./1787142779538-MigrationName";
544
548
  import { MigrationName1787156982416 } from "./1787156982416-MigrationName";
545
549
 
@@ -1088,4 +1092,8 @@ export default [
1088
1092
  AddMarketingConversionAttribution1788400000000,
1089
1093
  RemoveLlmCostBudgetAlertColumns1788500000000,
1090
1094
  AddDetectionRuleIncidentColumns1788600000000,
1095
+ RemoveMarketingConversionUploadState1788700000000,
1096
+ DropMarketingConversionAddEnterpriseLicenseEmail1788800000000,
1097
+ RedactStoredMonitorIngestSecrets1788900000000,
1098
+ AddUserSlackAndMicrosoftTeams1789000000000,
1091
1099
  ];
@@ -17,6 +17,13 @@ export enum QueueName {
17
17
  Worker = "Worker",
18
18
  Telemetry = "Telemetry",
19
19
  Runbook = "Runbook",
20
+ /*
21
+ * Outbound marketing conversion webhooks. Data-carrying (the job holds the
22
+ * whole event), unlike Worker, which looks its function up by name. Nothing
23
+ * stores these events, so this queue is the only thing that survives a
24
+ * receiver being briefly unreachable.
25
+ */
26
+ MarketingEvent = "MarketingEvent",
20
27
  }
21
28
 
22
29
  export type QueueJob = Job;
@@ -2074,6 +2074,14 @@ export class BillingService extends BaseService {
2074
2074
  return "price_1U0iWJANuQdJ93r7iVAXwhdP";
2075
2075
  }
2076
2076
 
2077
+ if (productType === ProductType.SecurityEvents) {
2078
+ if (this.isTestEnvironment()) {
2079
+ return "price_1U7efaANuQdJ93r74hFVOgdS";
2080
+ }
2081
+
2082
+ return "price_1U7edTANuQdJ93r7hR9jpBfv";
2083
+ }
2084
+
2077
2085
  throw new BadDataException(
2078
2086
  "Plan with productType " + productType + " not found",
2079
2087
  );
@@ -1,10 +1,70 @@
1
1
  import DatabaseService from "./DatabaseService";
2
2
  import EnterpriseLicense from "../../Models/DatabaseModels/EnterpriseLicense";
3
+ import MarketingEventUtil from "../Utils/Marketing/MarketingEventUtil";
4
+ import { MarketingEventType } from "../../Types/Marketing/MarketingEvent";
5
+ import { OnCreate } from "../Types/Database/Hooks";
6
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
3
7
 
4
8
  export class Service extends DatabaseService<EnterpriseLicense> {
5
9
  public constructor() {
6
10
  super(EnterpriseLicense);
7
11
  }
12
+
13
+ @CaptureSpan()
14
+ protected override async onCreateSuccess(
15
+ _onCreate: OnCreate<EnterpriseLicense>,
16
+ createdItem: EnterpriseLicense,
17
+ ): Promise<EnterpriseLicense> {
18
+ /*
19
+ * The outbound enterprise_license_issued conversion.
20
+ *
21
+ * This is where sales-led revenue enters the funnel. Everything else
22
+ * OneUptime emits is self-serve and knows its own value from the plan
23
+ * table; an enterprise contract is negotiated, so annualContractValue is
24
+ * the only place the number exists at all.
25
+ *
26
+ * Emitted even with no email and no ACV. A licence issued without either
27
+ * is still a licence issued, and a receiver that sees the event can chase
28
+ * the missing detail — one that never sees it cannot. The two nulls are
29
+ * reported honestly rather than defaulted to zero, which would quietly
30
+ * drag reported contract value down.
31
+ */
32
+ MarketingEventUtil.emitInBackground(
33
+ MarketingEventUtil.buildEvent({
34
+ eventType: MarketingEventType.EnterpriseLicenseIssued,
35
+ eventId: `${MarketingEventType.EnterpriseLicenseIssued}:${createdItem.id?.toString()}`,
36
+ occurredAt: createdItem.createdAt || new Date(),
37
+ email: createdItem.email?.toString(),
38
+ /*
39
+ * No attribution source. A licence row is typed in by a human and
40
+ * carries no session, so its campaign is whatever the meeting_booked
41
+ * sharing its email carried — the join the receiver makes on
42
+ * emailHash, not something OneUptime can restate here.
43
+ */
44
+ data: {
45
+ enterpriseLicenseId: createdItem.id?.toString() || "",
46
+ companyName: createdItem.companyName || "",
47
+ annualContractValueInUSD:
48
+ createdItem.annualContractValue === undefined ||
49
+ createdItem.annualContractValue === null
50
+ ? null
51
+ : createdItem.annualContractValue,
52
+ currency: "USD",
53
+ isEvaluationLicense: Boolean(createdItem.isEvaluationLicense),
54
+ userLimit:
55
+ createdItem.userLimit === undefined ||
56
+ createdItem.userLimit === null
57
+ ? null
58
+ : createdItem.userLimit,
59
+ expiresAt: createdItem.expiresAt
60
+ ? createdItem.expiresAt.toISOString()
61
+ : null,
62
+ },
63
+ }),
64
+ );
65
+
66
+ return createdItem;
67
+ }
8
68
  }
9
69
 
10
70
  export default new Service();
@@ -145,7 +145,6 @@ import GlobalOidcProjectService from "./GlobalOidcProjectService";
145
145
  import PromoCodeService from "./PromoCodeService";
146
146
  import EnterpriseLicenseService from "./EnterpriseLicenseService";
147
147
  import EnterpriseLicenseInstanceService from "./EnterpriseLicenseInstanceService";
148
- import MarketingConversionService from "./MarketingConversionService";
149
148
  import OpenSourceDeploymentService from "./OpenSourceDeploymentService";
150
149
  import RecommendationDismissalService from "./RecommendationDismissalService";
151
150
  import ResellerPlanService from "./ResellerPlanService";
@@ -213,6 +212,8 @@ import UserSmsService from "./UserSmsService";
213
212
  import UserIncomingCallNumberService from "./UserIncomingCallNumberService";
214
213
  import UserWhatsAppService from "./UserWhatsAppService";
215
214
  import UserTelegramService from "./UserTelegramService";
215
+ import UserSlackService from "./UserSlackService";
216
+ import UserMicrosoftTeamsService from "./UserMicrosoftTeamsService";
216
217
  import WorkflowLogService from "./WorkflowLogService";
217
218
  // Workflows.
218
219
  import WorkflowService from "./WorkflowService";
@@ -292,6 +293,7 @@ import WorkspaceSettingService from "./WorkspaceSettingService";
292
293
  import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
293
294
  import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
294
295
  import WorkspaceNotificationSummaryService from "./WorkspaceNotificationSummaryService";
296
+ import WorkspaceUserNotificationService from "./WorkspaceUserNotificationService";
295
297
  import OnCallDutyPolicyUserOverrideService from "./OnCallDutyPolicyUserOverrideService";
296
298
 
297
299
  import MonitorLogService from "./MonitorLogService";
@@ -309,7 +311,6 @@ const services: Array<BaseService> = [
309
311
  PromoCodeService,
310
312
  EnterpriseLicenseService,
311
313
  EnterpriseLicenseInstanceService,
312
- MarketingConversionService,
313
314
  OpenSourceDeploymentService,
314
315
 
315
316
  ResellerService,
@@ -490,6 +491,8 @@ const services: Array<BaseService> = [
490
491
  UserIncomingCallNumberService,
491
492
  UserWhatsAppService,
492
493
  UserTelegramService,
494
+ UserSlackService,
495
+ UserMicrosoftTeamsService,
493
496
  UserTotpAuthService,
494
497
  UserWebAuthnService,
495
498
 
@@ -587,6 +590,7 @@ const services: Array<BaseService> = [
587
590
  WorkspaceNotificationRuleService,
588
591
  WorkspaceNotificationLogService,
589
592
  WorkspaceNotificationSummaryService,
593
+ WorkspaceUserNotificationService,
590
594
 
591
595
  ProjectSCIMLogService,
592
596
  StatusPageSCIMLogService,
@@ -57,24 +57,40 @@ export class Service extends DatabaseService<Model> {
57
57
  const incidentRunMembership: string = `EXISTS (SELECT 1 FROM "AIRun" AS "run" WHERE "run"."_id" = "log"."aiRunId" AND "run"."triggeredByIncidentId" IS NOT NULL AND "run"."triggeredByAlertId" IS NULL)`;
58
58
  const alertRunMembership: string = `EXISTS (SELECT 1 FROM "AIRun" AS "run" WHERE "run"."_id" = "log"."aiRunId" AND "run"."triggeredByIncidentId" IS NULL AND "run"."triggeredByAlertId" IS NOT NULL)`;
59
59
 
60
- let subjectClause: string = `AND "log"."incidentId" IS NULL AND "log"."alertId" IS NULL AND NOT ("log"."feature" = ANY($4)) AND NOT ("log"."feature" = ANY($5)) AND NOT (${incidentRunMembership}) AND NOT (${alertRunMembership})`;
60
+ /*
61
+ * Placeholders and bindings are built together, per branch, because
62
+ * PostgreSQL derives a prepared statement's parameter count from the
63
+ * HIGHEST $n present in the text — not from what the driver sends. A
64
+ * branch that binds an argument it never references is rejected outright
65
+ * at Bind (bind message supplies 5 parameters, but prepared statement
66
+ * requires 4), and a branch that skips a placeholder is rejected at Parse
67
+ * (could not determine data type of parameter $4). Both are runtime-only
68
+ * failures no compiler catches, so the parameter array is grown next to
69
+ * the clause that reads it and never assembled apart from it.
70
+ */
71
+ const params: Array<unknown> = [
72
+ data.projectId.toString(), // $1
73
+ data.since, // $2
74
+ data.features, // $3
75
+ ];
76
+
77
+ let subjectClause: string;
61
78
 
62
79
  if (data.incidentId) {
80
+ params.push(legacyIncidentFeatures); // $4
63
81
  subjectClause = `AND (("log"."incidentId" IS NOT NULL AND "log"."alertId" IS NULL) OR ("log"."incidentId" IS NULL AND "log"."alertId" IS NULL AND (("log"."feature" = ANY($4)) OR ${incidentRunMembership})))`;
64
82
  } else if (data.alertId) {
65
- subjectClause = `AND (("log"."incidentId" IS NULL AND "log"."alertId" IS NOT NULL) OR ("log"."incidentId" IS NULL AND "log"."alertId" IS NULL AND (("log"."feature" = ANY($5)) OR ${alertRunMembership})))`;
83
+ params.push(legacyAlertFeatures); // $4
84
+ subjectClause = `AND (("log"."incidentId" IS NULL AND "log"."alertId" IS NOT NULL) OR ("log"."incidentId" IS NULL AND "log"."alertId" IS NULL AND (("log"."feature" = ANY($4)) OR ${alertRunMembership})))`;
85
+ } else {
86
+ params.push(legacyIncidentFeatures, legacyAlertFeatures); // $4, $5
87
+ subjectClause = `AND "log"."incidentId" IS NULL AND "log"."alertId" IS NULL AND NOT ("log"."feature" = ANY($4)) AND NOT ("log"."feature" = ANY($5)) AND NOT (${incidentRunMembership}) AND NOT (${alertRunMembership})`;
66
88
  }
67
89
 
68
90
  const rows: Array<{ total: string | number | null }> =
69
91
  await this.getRepository().manager.query(
70
92
  `SELECT COALESCE(SUM("log"."totalTokens"), 0) AS "total" FROM "LlmLog" AS "log" WHERE "log"."projectId" = $1 AND "log"."createdAt" >= $2 AND "log"."feature" = ANY($3) ${subjectClause} AND "log"."deletedAt" IS NULL`,
71
- [
72
- data.projectId.toString(),
73
- data.since,
74
- data.features,
75
- legacyIncidentFeatures,
76
- legacyAlertFeatures,
77
- ],
93
+ params,
78
94
  );
79
95
 
80
96
  return Number(rows[0]?.total || 0);
@@ -16,6 +16,8 @@ import UserPushService from "./UserPushService";
16
16
  import UserService from "./UserService";
17
17
  import UserSmsService from "./UserSmsService";
18
18
  import UserTelegramService from "./UserTelegramService";
19
+ import UserSlackService from "./UserSlackService";
20
+ import UserMicrosoftTeamsService from "./UserMicrosoftTeamsService";
19
21
  import UserWebhookService from "./UserWebhookService";
20
22
  import UserWhatsAppService from "./UserWhatsAppService";
21
23
  import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
@@ -51,6 +53,8 @@ import UserNotificationRule from "../../Models/DatabaseModels/UserNotificationRu
51
53
  import UserPush from "../../Models/DatabaseModels/UserPush";
52
54
  import UserSMS from "../../Models/DatabaseModels/UserSMS";
53
55
  import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
56
+ import UserSlack from "../../Models/DatabaseModels/UserSlack";
57
+ import UserMicrosoftTeams from "../../Models/DatabaseModels/UserMicrosoftTeams";
54
58
  import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
55
59
  import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
56
60
 
@@ -116,7 +120,7 @@ export enum ResponderSource {
116
120
  }
117
121
 
118
122
  /**
119
- * The seven channels a page can be delivered on. These strings are the same literals the
123
+ * The nine channels a page can be delivered on. These strings are the same literals the
120
124
  * fallback uses for `channelsUsed` (UserNotificationRuleService.chooseFallbackChannels),
121
125
  * deliberately: an operator reading "notified via fallback (Push, Email)" in an execution
122
126
  * log and "Push, Email" in the readiness table must not have to translate between two
@@ -129,6 +133,8 @@ export enum ReadinessMethodType {
129
133
  Call = "Call",
130
134
  WhatsApp = "WhatsApp",
131
135
  Telegram = "Telegram",
136
+ Slack = "Slack",
137
+ MicrosoftTeams = "Microsoft Teams",
132
138
  Webhook = "Webhook",
133
139
  }
134
140
 
@@ -436,6 +442,8 @@ const RULE_TYPE_SCOPES: Array<RuleTypeScope> = [
436
442
  const METHOD_DISPLAY_ORDER: Array<ReadinessMethodType> = [
437
443
  ReadinessMethodType.Push,
438
444
  ReadinessMethodType.Email,
445
+ ReadinessMethodType.Slack,
446
+ ReadinessMethodType.MicrosoftTeams,
439
447
  ReadinessMethodType.SMS,
440
448
  ReadinessMethodType.Call,
441
449
  ReadinessMethodType.WhatsApp,
@@ -2194,6 +2202,74 @@ export default class OnCallReadinessService {
2194
2202
  },
2195
2203
  });
2196
2204
 
2205
+ await this.readEveryPage<UserSlack>({
2206
+ description: "Slack notification methods",
2207
+ projectId: projectId,
2208
+ completeness: completeness,
2209
+ service: UserSlackService,
2210
+ query: {
2211
+ projectId: projectId,
2212
+ userId: new Includes(userIds),
2213
+ },
2214
+ /*
2215
+ * The username only — never slackUserId. The member id is the
2216
+ * addressable target the bot sends to; the username is the human-facing
2217
+ * label, and it is the one a user can recognise as theirs.
2218
+ */
2219
+ select: {
2220
+ _id: true,
2221
+ userId: true,
2222
+ slackUserName: true,
2223
+ isVerified: true,
2224
+ },
2225
+ consumePage: (rows: Array<UserSlack>): void => {
2226
+ for (const row of rows) {
2227
+ addMethod(row, {
2228
+ methodType: ReadinessMethodType.Slack,
2229
+ maskedIdentifier: maskIdentifier(
2230
+ row.slackUserName,
2231
+ MaskedIdentifierKind.Handle,
2232
+ ),
2233
+ isVerified: Boolean(row.isVerified),
2234
+ });
2235
+ }
2236
+ },
2237
+ });
2238
+
2239
+ await this.readEveryPage<UserMicrosoftTeams>({
2240
+ description: "Microsoft Teams notification methods",
2241
+ projectId: projectId,
2242
+ completeness: completeness,
2243
+ service: UserMicrosoftTeamsService,
2244
+ query: {
2245
+ projectId: projectId,
2246
+ userId: new Includes(userIds),
2247
+ },
2248
+ /*
2249
+ * The display name only — never microsoftTeamsUserId. The Entra id is
2250
+ * the addressable target the bot sends to; the display name is the
2251
+ * human-facing label, and it is the one a user can recognise as theirs.
2252
+ */
2253
+ select: {
2254
+ _id: true,
2255
+ userId: true,
2256
+ microsoftTeamsUserName: true,
2257
+ isVerified: true,
2258
+ },
2259
+ consumePage: (rows: Array<UserMicrosoftTeams>): void => {
2260
+ for (const row of rows) {
2261
+ addMethod(row, {
2262
+ methodType: ReadinessMethodType.MicrosoftTeams,
2263
+ maskedIdentifier: maskIdentifier(
2264
+ row.microsoftTeamsUserName,
2265
+ MaskedIdentifierKind.Handle,
2266
+ ),
2267
+ isVerified: Boolean(row.isVerified),
2268
+ });
2269
+ }
2270
+ },
2271
+ });
2272
+
2197
2273
  await this.readEveryPage<UserWebhook>({
2198
2274
  description: "webhook notification methods",
2199
2275
  projectId: projectId,
@@ -2677,7 +2753,7 @@ export default class OnCallReadinessService {
2677
2753
  * the real thing rather than a hopeful "some verified method".
2678
2754
  *
2679
2755
  * This mirrors UserNotificationRuleService.chooseFallbackChannels exactly, including
2680
- * the project switches: zero-cost channels first and BOTH of them if present, then one
2756
+ * the project switches: zero-cost channels first and ALL of them if present, then one
2681
2757
  * paid channel in escalating-intrusiveness order, then webhook. If that function's
2682
2758
  * order ever changes, this one has to change with it — a readiness surface promising
2683
2759
  * "falls back to SMS" for a project that has SMS switched off is worse than saying
@@ -2709,6 +2785,14 @@ export default class OnCallReadinessService {
2709
2785
  zeroCost.push(ReadinessMethodType.Email);
2710
2786
  }
2711
2787
 
2788
+ if (has(ReadinessMethodType.Slack)) {
2789
+ zeroCost.push(ReadinessMethodType.Slack);
2790
+ }
2791
+
2792
+ if (has(ReadinessMethodType.MicrosoftTeams)) {
2793
+ zeroCost.push(ReadinessMethodType.MicrosoftTeams);
2794
+ }
2795
+
2712
2796
  if (zeroCost.length > 0) {
2713
2797
  return zeroCost;
2714
2798
  }
@@ -16,6 +16,8 @@ import UpdateBy from "../Types/Database/UpdateBy";
16
16
  import logger, { LogAttributes } from "../Utils/Logger";
17
17
  import Errors from "../Utils/Errors";
18
18
  import ProductAnalytics from "../Utils/ProductAnalytics";
19
+ import MarketingEventUtil from "../Utils/Marketing/MarketingEventUtil";
20
+ import { MarketingEventType } from "../../Types/Marketing/MarketingEvent";
19
21
  import SessionReplayGateCacheStore from "../Utils/SessionReplay/SessionReplayGateCacheStore";
20
22
  import AccessTokenService from "./AccessTokenService";
21
23
  import BillingService from "./BillingService";
@@ -558,10 +560,21 @@ export class ProjectService extends DatabaseService<Model> {
558
560
  trialEndsAt: true,
559
561
  paymentProviderCustomerId: true,
560
562
  createdOwnerEmail: true,
563
+ /*
564
+ * The full attribution set, not just the three UTMs the analytics
565
+ * properties carry: the outbound plan-change conversion is the only
566
+ * place the campaign that won a customer is ever reported, and a
567
+ * column left unselected reaches the receiver as empty rather than as
568
+ * an error.
569
+ */
561
570
  utmSource: true,
562
571
  utmMedium: true,
563
572
  utmCampaign: true,
573
+ utmTerm: true,
574
+ utmContent: true,
575
+ utmUrl: true,
564
576
  clickIds: true,
577
+ firstTouchAttribution: true,
565
578
  },
566
579
  props: {
567
580
  isRoot: true,
@@ -982,6 +995,60 @@ These are no longer recorded against the project and have to be cancelled by han
982
995
  distinctId: data.project.createdOwnerEmail.toString(),
983
996
  properties: properties,
984
997
  });
998
+
999
+ this.emitPlanChangeMarketingEvent({
1000
+ project: data.project,
1001
+ properties: properties,
1002
+ isUpgrade: properties["is_upgrade"] === true,
1003
+ isDowngrade: properties["is_downgrade"] === true,
1004
+ });
1005
+ }
1006
+
1007
+ /*
1008
+ * The outbound subscription_upgraded / subscription_downgraded conversions.
1009
+ *
1010
+ * Rides on the analytics pass above rather than on the plan-change path
1011
+ * directly, because that is where "is this an upgrade" is already decided —
1012
+ * by plan ORDER, not price, so a monthly-to-yearly switch at the same tier
1013
+ * is neither. Duplicating that comparison here is how the two would
1014
+ * eventually disagree about what a customer did.
1015
+ *
1016
+ * Only real tier movements are emitted. A first plan on a project with no
1017
+ * previous one has no direction (both flags read false, since oldPlanOrder
1018
+ * is null), and an interval change is not a conversion — sending either as
1019
+ * an upgrade would inflate exactly the number this exists to report.
1020
+ */
1021
+ private emitPlanChangeMarketingEvent(data: {
1022
+ project: Model;
1023
+ properties: JSONObject;
1024
+ isUpgrade: boolean;
1025
+ isDowngrade: boolean;
1026
+ }): void {
1027
+ if (!data.isUpgrade && !data.isDowngrade) {
1028
+ return;
1029
+ }
1030
+
1031
+ const eventType: MarketingEventType = data.isUpgrade
1032
+ ? MarketingEventType.SubscriptionUpgraded
1033
+ : MarketingEventType.SubscriptionDowngraded;
1034
+
1035
+ const occurredAt: Date = new Date();
1036
+
1037
+ MarketingEventUtil.emitInBackground(
1038
+ MarketingEventUtil.buildEvent({
1039
+ eventType: eventType,
1040
+ /*
1041
+ * A project can legitimately upgrade, downgrade and upgrade again, so
1042
+ * unlike a signup or a booking there is no naturally unique key —
1043
+ * the instant is what separates one change from the next.
1044
+ */
1045
+ eventId: `${eventType}:${data.project.id?.toString()}:${occurredAt.toISOString()}`,
1046
+ occurredAt: occurredAt,
1047
+ email: data.project.createdOwnerEmail?.toString(),
1048
+ attributionSource: data.project,
1049
+ data: data.properties,
1050
+ }),
1051
+ );
985
1052
  }
986
1053
 
987
1054
  private async sendSubscriptionChangeWebhookSlackNotification(
@@ -0,0 +1,208 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
4
+ import DatabaseService from "./DatabaseService";
5
+ import UserNotificationRuleService, {
6
+ NotificationDeletionImpact,
7
+ NotificationMethodChannel,
8
+ } from "./UserNotificationRuleService";
9
+ import WorkspaceProjectAuthTokenService from "./WorkspaceProjectAuthTokenService";
10
+ import WorkspaceUserAuthTokenService from "./WorkspaceUserAuthTokenService";
11
+ import logger from "../Utils/Logger";
12
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
13
+ import BadDataException from "../../Types/Exception/BadDataException";
14
+ import { JSONObject } from "../../Types/JSON";
15
+ import ObjectID from "../../Types/ObjectID";
16
+ import WorkspaceType from "../../Types/Workspace/WorkspaceType";
17
+ import WorkspaceProjectAuthToken from "../../Models/DatabaseModels/WorkspaceProjectAuthToken";
18
+ import WorkspaceUserAuthToken from "../../Models/DatabaseModels/WorkspaceUserAuthToken";
19
+ import Model from "../../Models/DatabaseModels/UserMicrosoftTeams";
20
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
21
+
22
+ export class Service extends DatabaseService<Model> {
23
+ public constructor() {
24
+ super(Model);
25
+ }
26
+
27
+ @CaptureSpan()
28
+ protected override async onBeforeDelete(
29
+ deleteBy: DeleteBy<Model>,
30
+ ): Promise<OnDelete<Model>> {
31
+ const itemsToDelete: Array<Model> = await this.findBy({
32
+ query: deleteBy.query,
33
+ select: {
34
+ _id: true,
35
+ projectId: true,
36
+ },
37
+ skip: 0,
38
+ limit: LIMIT_MAX,
39
+ props: {
40
+ isRoot: true,
41
+ },
42
+ });
43
+
44
+ for (const item of itemsToDelete) {
45
+ await UserNotificationRuleService.deleteBy({
46
+ query: {
47
+ userMicrosoftTeamsId: item.id!,
48
+ projectId: item.projectId!,
49
+ },
50
+ limit: LIMIT_MAX,
51
+ skip: 0,
52
+ props: {
53
+ isRoot: true,
54
+ },
55
+ });
56
+ }
57
+
58
+ return {
59
+ deleteBy,
60
+ carryForward: null,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * What this user would lose if this Microsoft Teams account were deleted.
66
+ * Ask BEFORE calling delete; nothing here refuses anything.
67
+ *
68
+ * The hook directly above deletes every UserNotificationRule that points at
69
+ * this account, and the foreign key is onDelete: "CASCADE" so the rows would
70
+ * go even if it did not.
71
+ */
72
+ @CaptureSpan()
73
+ public async getDeletionImpact(data: {
74
+ itemId: ObjectID;
75
+ projectId: ObjectID;
76
+ }): Promise<NotificationDeletionImpact> {
77
+ return UserNotificationRuleService.getNotificationMethodDeletionImpact({
78
+ projectId: data.projectId,
79
+ methodType: NotificationMethodChannel.MicrosoftTeams,
80
+ methodId: data.itemId,
81
+ });
82
+ }
83
+
84
+ /*
85
+ * A Microsoft Teams notification method is a POINTER at the user's existing
86
+ * workspace link, not a hand-typed address: the Microsoft Entra user id is
87
+ * resolved from the user's own WorkspaceUserAuthToken and never accepted
88
+ * from the request. That is why creation IS verification — the OAuth dance
89
+ * that wrote the auth-token row already proved the person controls that
90
+ * Microsoft Teams account.
91
+ */
92
+ @CaptureSpan()
93
+ protected override async onBeforeCreate(
94
+ createBy: CreateBy<Model>,
95
+ ): Promise<OnCreate<Model>> {
96
+ if (!createBy.props.isRoot && createBy.data.isVerified) {
97
+ throw new BadDataException("isVerified cannot be set to true");
98
+ }
99
+
100
+ if (!createBy.props.isRoot && createBy.data.microsoftTeamsUserId) {
101
+ throw new BadDataException("microsoftTeamsUserId cannot be set directly");
102
+ }
103
+
104
+ if (!createBy.data.projectId || !createBy.data.userId) {
105
+ throw new BadDataException("projectId and userId are required");
106
+ }
107
+
108
+ const projectId: ObjectID = new ObjectID(
109
+ createBy.data.projectId.toString(),
110
+ );
111
+ const userId: ObjectID = new ObjectID(createBy.data.userId.toString());
112
+
113
+ /*
114
+ * One workspace link per (user, project) means a second row would be an
115
+ * exact duplicate of the first.
116
+ */
117
+ const existingCount: number = (
118
+ await this.countBy({
119
+ query: {
120
+ projectId: projectId,
121
+ userId: userId,
122
+ },
123
+ props: {
124
+ isRoot: true,
125
+ },
126
+ })
127
+ ).toNumber();
128
+
129
+ if (existingCount > 0) {
130
+ throw new BadDataException(
131
+ "Microsoft Teams is already added as a notification method for this project.",
132
+ );
133
+ }
134
+
135
+ const projectAuth: WorkspaceProjectAuthToken | null =
136
+ await WorkspaceProjectAuthTokenService.getProjectAuth({
137
+ projectId: projectId,
138
+ workspaceType: WorkspaceType.MicrosoftTeams,
139
+ });
140
+
141
+ if (!projectAuth || !projectAuth.authToken) {
142
+ throw new BadDataException(
143
+ "This project is not connected to Microsoft Teams. Please ask a project admin to connect Microsoft Teams in Project Settings > Microsoft Teams Integration.",
144
+ );
145
+ }
146
+
147
+ const userAuth: WorkspaceUserAuthToken | null =
148
+ await WorkspaceUserAuthTokenService.getUserAuth({
149
+ projectId: projectId,
150
+ userId: userId,
151
+ workspaceType: WorkspaceType.MicrosoftTeams,
152
+ });
153
+
154
+ if (!userAuth || !userAuth.workspaceUserId) {
155
+ throw new BadDataException(
156
+ "Your Microsoft Teams account is not connected to OneUptime for this project. Please go to User Settings > Microsoft Teams Integration and connect your Microsoft Teams account first.",
157
+ );
158
+ }
159
+
160
+ createBy.data.microsoftTeamsUserId = userAuth.workspaceUserId;
161
+ createBy.data.isVerified = true;
162
+
163
+ /* Display label only — captured when the user linked their account. */
164
+ const displayName: string | undefined = (
165
+ userAuth.miscData as JSONObject | undefined
166
+ )?.["displayName"] as string | undefined;
167
+
168
+ if (displayName) {
169
+ createBy.data.microsoftTeamsUserName = displayName;
170
+ }
171
+
172
+ return {
173
+ createBy,
174
+ carryForward: null,
175
+ };
176
+ }
177
+
178
+ @CaptureSpan()
179
+ protected override async onCreateSuccess(
180
+ _onCreate: OnCreate<Model>,
181
+ createdItem: Model,
182
+ ): Promise<Model> {
183
+ /*
184
+ * The row is born verified (creation is gated on the live workspace
185
+ * link), so default on-call rules are seeded at create time — same as
186
+ * webhooks.
187
+ */
188
+ if (createdItem.projectId && createdItem.userId && createdItem.id) {
189
+ try {
190
+ await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
191
+ {
192
+ projectId: createdItem.projectId,
193
+ userId: createdItem.userId,
194
+ notificationMethod: {
195
+ userMicrosoftTeamsId: createdItem.id,
196
+ },
197
+ },
198
+ );
199
+ } catch (err) {
200
+ logger.error(err);
201
+ }
202
+ }
203
+
204
+ return createdItem;
205
+ }
206
+ }
207
+
208
+ export default new Service();