@oneuptime/common 11.3.26 → 11.3.28

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 (241) hide show
  1. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  2. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  3. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  4. package/Models/DatabaseModels/Index.ts +3 -0
  5. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  6. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  7. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  8. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  11. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  12. package/Server/API/AlertAPI.ts +1 -1
  13. package/Server/API/IncidentAPI.ts +2 -2
  14. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  15. package/Server/API/LlmProviderAPI.ts +1 -0
  16. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  22. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  23. package/Server/Services/AIService.ts +74 -2
  24. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  25. package/Server/Services/DatabaseService.ts +10 -1
  26. package/Server/Services/IncidentService.ts +1 -1
  27. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  28. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  29. package/Server/Services/Index.ts +2 -0
  30. package/Server/Services/MigrationFailureService.ts +9 -0
  31. package/Server/Services/MonitorProbeService.ts +33 -7
  32. package/Server/Services/MonitorService.ts +19 -7
  33. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  34. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  35. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  36. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  37. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  38. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  39. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  40. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  41. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  42. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  43. package/Server/Services/TeamMemberService.ts +6 -0
  44. package/Server/Services/UserNotificationRuleService.ts +13 -42
  45. package/Server/Services/UserOnCallLogService.ts +90 -0
  46. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  47. package/Server/Types/Database/QueryUtil.ts +57 -0
  48. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  49. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
  50. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  51. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  52. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  53. package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
  54. package/Server/Utils/AI/Toolbox/Index.ts +4 -1
  55. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  56. package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
  57. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  58. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  59. package/Server/Utils/LLM/LLMService.ts +110 -11
  60. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  61. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  62. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  63. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  64. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  65. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  66. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  67. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  68. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
  69. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
  70. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  71. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  72. package/Tests/Types/Events/Recurring.test.ts +22 -9
  73. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  74. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  75. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  76. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  77. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  78. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  79. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  80. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  82. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  83. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  84. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  85. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  86. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  87. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  88. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  89. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  90. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  91. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  92. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  93. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  94. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  95. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  96. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  97. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  98. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  99. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  100. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  101. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  102. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  103. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  104. package/Types/Date.ts +200 -15
  105. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  106. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  107. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  108. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  109. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  110. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  111. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  112. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  114. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  116. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  118. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  120. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  121. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  122. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  124. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  125. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  126. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  127. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  128. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  129. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  130. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  131. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  132. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  133. package/build/dist/Server/API/AlertAPI.js +1 -1
  134. package/build/dist/Server/API/IncidentAPI.js +2 -2
  135. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  136. package/build/dist/Server/API/LlmProviderAPI.js +1 -0
  137. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  138. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  149. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  150. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  151. package/build/dist/Server/Services/AIService.js +53 -2
  152. package/build/dist/Server/Services/AIService.js.map +1 -1
  153. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  154. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +10 -1
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/IncidentService.js +1 -1
  158. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  159. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  160. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  161. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  162. package/build/dist/Server/Services/Index.js +2 -0
  163. package/build/dist/Server/Services/Index.js.map +1 -1
  164. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  165. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  166. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  167. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  168. package/build/dist/Server/Services/MonitorService.js +12 -5
  169. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  170. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  171. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  172. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  173. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  174. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  175. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  176. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  177. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  178. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  179. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  180. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  181. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  182. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  183. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  185. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  187. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  189. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  190. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  191. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  192. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  193. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  194. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  195. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  196. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  197. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  198. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  199. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  201. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  202. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
  203. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  204. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  205. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  206. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  207. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  208. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  209. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  210. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
  211. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  212. package/build/dist/Server/Utils/AI/Toolbox/Index.js +4 -1
  213. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  214. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  215. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  216. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
  217. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  218. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  219. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  220. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  221. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  222. package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
  223. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  224. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  225. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  226. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  227. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  228. package/build/dist/Types/Date.js +153 -15
  229. package/build/dist/Types/Date.js.map +1 -1
  230. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  231. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  232. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  233. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  234. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  235. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  236. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  237. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  238. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  239. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  240. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  241. package/package.json +1 -1
@@ -10,6 +10,7 @@ import OnCallDutyPolicyEscalationRuleScheduleService from "./OnCallDutyPolicyEsc
10
10
  import OnCallDutyPolicyEscalationRuleTeamService from "./OnCallDutyPolicyEscalationRuleTeamService";
11
11
  import OnCallDutyPolicyEscalationRuleUserService from "./OnCallDutyPolicyEscalationRuleUserService";
12
12
  import OnCallDutyPolicyExecutionLogService from "./OnCallDutyPolicyExecutionLogService";
13
+ import OnCallDutyPolicyExecutionLog from "../../Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
13
14
  import OnCallDutyPolicyExecutionLogTimelineService from "./OnCallDutyPolicyExecutionLogTimelineService";
14
15
  import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
15
16
  import TeamMemberService from "./TeamMemberService";
@@ -315,31 +316,66 @@ export class Service extends DatabaseService<Model> {
315
316
  );
316
317
 
317
318
  type StartUserNotificationRuleExecutionFunction = (
318
- userId: ObjectID,
319
+ originalUserId: ObjectID,
320
+ alertSentToUserId: ObjectID,
319
321
  teamId: ObjectID | null,
320
322
  scheduleId: ObjectID | null,
321
323
  ) => Promise<void>;
322
324
 
323
- const startUserNotificationRuleExecution: StartUserNotificationRuleExecutionFunction =
324
- async (
325
- userId: ObjectID,
326
- teamId: ObjectID | null,
327
- scheduleId: ObjectID | null,
328
- ): Promise<void> => {
329
- // This is where user is notified.
330
-
331
- // get route alert to user id.
332
- let routeAlertToUserId: ObjectID | null = null;
325
+ /*
326
+ * Resolve the FINAL alert recipient for a target, applying a user override
327
+ * (getRouteAlertToUserId) exactly ONCE per paging decision.
328
+ *
329
+ * Schedule targets are already override-resolved: getCurrentUserIdInSchedule
330
+ * applies overrides via UserOverrideUtil. Applying getRouteAlertToUserId to
331
+ * them again re-substituted an already-substituted user (transitive), so the
332
+ * schedule paging path could page a different person than the roster,
333
+ * dashboard, handoff notification, and a rule that lists the same user
334
+ * directly all of which apply overrides only once (audit F5). So team and
335
+ * direct-user targets (the RAW roster user) get the single override hop here;
336
+ * schedule targets do not.
337
+ */
338
+ const resolveAlertRecipientUserId: (
339
+ userId: ObjectID,
340
+ isFromSchedule: boolean,
341
+ ) => Promise<ObjectID> = async (
342
+ userId: ObjectID,
343
+ isFromSchedule: boolean,
344
+ ): Promise<ObjectID> => {
345
+ if (isFromSchedule) {
346
+ return userId;
347
+ }
333
348
 
334
- if (options.onCallPolicyId) {
335
- routeAlertToUserId = await this.getRouteAlertToUserId({
349
+ if (options.onCallPolicyId) {
350
+ const routeAlertToUserId: ObjectID | null =
351
+ await this.getRouteAlertToUserId({
336
352
  userId,
337
353
  onCallDutyPolicyId: options.onCallPolicyId,
338
354
  projectId: options.projectId,
339
355
  });
356
+
357
+ if (routeAlertToUserId) {
358
+ return routeAlertToUserId;
340
359
  }
360
+ }
361
+
362
+ return userId;
363
+ };
364
+
365
+ const startUserNotificationRuleExecution: StartUserNotificationRuleExecutionFunction =
366
+ async (
367
+ originalUserId: ObjectID,
368
+ alertSentToUserId: ObjectID,
369
+ teamId: ObjectID | null,
370
+ scheduleId: ObjectID | null,
371
+ ): Promise<void> => {
372
+ /*
373
+ * This is where user is notified. alertSentToUserId is already resolved
374
+ * (override applied at most once by resolveAlertRecipientUserId).
375
+ */
341
376
 
342
- const alertSentToUserId: ObjectID = routeAlertToUserId || userId;
377
+ const wasOverridden: boolean =
378
+ alertSentToUserId.toString() !== originalUserId.toString();
343
379
 
344
380
  logger.debug(
345
381
  `Starting notification rule execution for userId: ${alertSentToUserId.toString()}`,
@@ -354,8 +390,8 @@ export class Service extends DatabaseService<Model> {
354
390
  log.status = OnCallDutyExecutionLogTimelineStatus.Executing;
355
391
  log.alertSentToUserId = alertSentToUserId;
356
392
 
357
- if (routeAlertToUserId) {
358
- log.overridedByUserId = userId;
393
+ if (wasOverridden) {
394
+ log.overridedByUserId = originalUserId;
359
395
  }
360
396
 
361
397
  if (teamId) {
@@ -390,12 +426,36 @@ export class Service extends DatabaseService<Model> {
390
426
  onCallDutyPolicyExecutionLogTimelineId: log.id!,
391
427
  projectId: options.projectId,
392
428
  onCallScheduleId: scheduleId || undefined,
393
- overridedByUserId: routeAlertToUserId ? userId : undefined,
429
+ overridedByUserId: wasOverridden ? originalUserId : undefined,
394
430
  },
395
431
  );
396
432
  };
397
433
 
434
+ /*
435
+ * Dedup on the RESOLVED recipient (post-override), not the pre-route id.
436
+ * Keying on the pre-route id let two different targets that both reroute to
437
+ * the same backup each fire a full notification chain for the same person in
438
+ * one rule (audit F6).
439
+ */
398
440
  const uniqueUserIds: Array<ObjectID> = [];
441
+ const alreadyNotified: (recipientId: ObjectID) => boolean = (
442
+ recipientId: ObjectID,
443
+ ): boolean => {
444
+ return Boolean(
445
+ uniqueUserIds.find((userId: ObjectID) => {
446
+ return recipientId.toString() === userId.toString();
447
+ }),
448
+ );
449
+ };
450
+
451
+ /*
452
+ * M-5: tracks whether any schedule target for this rule momentarily had no
453
+ * on-call user (a restriction gap / future start / empty layer). If a rule
454
+ * reaches NO ONE solely because of such a gap, we re-sample the same rule on
455
+ * the next cron ticks (bounded) instead of permanently skipping it, so a
456
+ * user coming on-call moments later still gets paged.
457
+ */
458
+ let hadScheduleGap: boolean = false;
399
459
 
400
460
  for (const teamInRule of teamsInRule) {
401
461
  const usersInTeam: Array<User> = await TeamMemberService.getUsersInTeam(
@@ -403,14 +463,16 @@ export class Service extends DatabaseService<Model> {
403
463
  );
404
464
 
405
465
  for (const user of usersInTeam) {
406
- if (
407
- !uniqueUserIds.find((userId: ObjectID) => {
408
- return user.id?.toString() === userId.toString();
409
- })
410
- ) {
411
- uniqueUserIds.push(user.id!);
466
+ const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
467
+ user.id!,
468
+ false,
469
+ );
470
+
471
+ if (!alreadyNotified(recipientUserId)) {
472
+ uniqueUserIds.push(recipientUserId);
412
473
  await startUserNotificationRuleExecution(
413
474
  user.id!,
475
+ recipientUserId,
414
476
  teamInRule.teamId!,
415
477
  null,
416
478
  );
@@ -419,7 +481,7 @@ export class Service extends DatabaseService<Model> {
419
481
  log.statusMessage =
420
482
  "Skipped because notification sent to this user already.";
421
483
  log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
422
- log.alertSentToUserId = user.id!;
484
+ log.alertSentToUserId = recipientUserId;
423
485
  log.userBelongsToTeamId = teamInRule.teamId!;
424
486
 
425
487
  await OnCallDutyPolicyExecutionLogTimelineService.create({
@@ -433,19 +495,25 @@ export class Service extends DatabaseService<Model> {
433
495
  }
434
496
 
435
497
  for (const userRule of usersInRule) {
436
- if (
437
- !uniqueUserIds.find((userId: ObjectID) => {
438
- return userRule.userId?.toString() === userId.toString();
439
- })
440
- ) {
441
- uniqueUserIds.push(userRule.userId!);
442
- await startUserNotificationRuleExecution(userRule.userId!, null, null);
498
+ const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
499
+ userRule.userId!,
500
+ false,
501
+ );
502
+
503
+ if (!alreadyNotified(recipientUserId)) {
504
+ uniqueUserIds.push(recipientUserId);
505
+ await startUserNotificationRuleExecution(
506
+ userRule.userId!,
507
+ recipientUserId,
508
+ null,
509
+ null,
510
+ );
443
511
  } else {
444
512
  const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
445
513
  log.statusMessage =
446
514
  "Skipped because notification sent to this user already.";
447
515
  log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
448
- log.alertSentToUserId = userRule.userId!;
516
+ log.alertSentToUserId = recipientUserId;
449
517
 
450
518
  await OnCallDutyPolicyExecutionLogTimelineService.create({
451
519
  data: log,
@@ -464,6 +532,8 @@ export class Service extends DatabaseService<Model> {
464
532
  );
465
533
 
466
534
  if (!userIdInSchedule) {
535
+ hadScheduleGap = true;
536
+
467
537
  const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
468
538
  log.statusMessage =
469
539
  "Skipped because no active users are found in this schedule.";
@@ -480,14 +550,17 @@ export class Service extends DatabaseService<Model> {
480
550
  continue;
481
551
  }
482
552
 
483
- if (
484
- !uniqueUserIds.find((userId: ObjectID) => {
485
- return userIdInSchedule?.toString() === userId.toString();
486
- })
487
- ) {
488
- uniqueUserIds.push(userIdInSchedule);
553
+ // Schedule users are already override-resolved; no second override hop (F5).
554
+ const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
555
+ userIdInSchedule,
556
+ true,
557
+ );
558
+
559
+ if (!alreadyNotified(recipientUserId)) {
560
+ uniqueUserIds.push(recipientUserId);
489
561
  await startUserNotificationRuleExecution(
490
562
  userIdInSchedule,
563
+ recipientUserId,
491
564
  null,
492
565
  scheduleRule.onCallDutyPolicyScheduleId!,
493
566
  );
@@ -496,7 +569,7 @@ export class Service extends DatabaseService<Model> {
496
569
  log.statusMessage =
497
570
  "Skipped because notification sent to this user already.";
498
571
  log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
499
- log.alertSentToUserId = userIdInSchedule;
572
+ log.alertSentToUserId = recipientUserId;
500
573
  log.onCallDutyScheduleId = scheduleRule.onCallDutyPolicyScheduleId!;
501
574
 
502
575
  await OnCallDutyPolicyExecutionLogTimelineService.create({
@@ -521,6 +594,77 @@ export class Service extends DatabaseService<Model> {
521
594
  });
522
595
  }
523
596
 
597
+ /*
598
+ * M-5: if this rule reached NO ONE and the only reason was a schedule gap
599
+ * (a schedule target with no on-call user right now), re-sample the SAME
600
+ * rule on the next cron ticks — bounded — instead of permanently skipping.
601
+ * ExecutePendingExecutions selects the rule with order =
602
+ * lastExecutedEscalationRuleOrder + 1, so rewinding the order by one makes
603
+ * it re-run this rule. No notifications were sent (uniqueUserIds is empty),
604
+ * so a re-sample cannot double-page anyone.
605
+ */
606
+ const maxScheduleGapRetries: number = 5;
607
+ const scheduleGapRetryIntervalMinutes: number = 1;
608
+ const reachedNoOne: boolean = uniqueUserIds.length === 0;
609
+
610
+ if (reachedNoOne && hadScheduleGap && rule.order) {
611
+ const executionLog: OnCallDutyPolicyExecutionLog | null =
612
+ await OnCallDutyPolicyExecutionLogService.findOneById({
613
+ id: options.onCallPolicyExecutionLogId,
614
+ select: {
615
+ scheduleGapRetryCount: true,
616
+ },
617
+ props: {
618
+ isRoot: true,
619
+ },
620
+ });
621
+
622
+ const retryCount: number = executionLog?.scheduleGapRetryCount || 0;
623
+
624
+ if (retryCount < maxScheduleGapRetries) {
625
+ await OnCallDutyPolicyExecutionLogService.updateOneById({
626
+ id: options.onCallPolicyExecutionLogId,
627
+ data: {
628
+ lastExecutedEscalationRuleOrder: rule.order - 1,
629
+ executeNextEscalationRuleInMinutes: scheduleGapRetryIntervalMinutes,
630
+ scheduleGapRetryCount: retryCount + 1,
631
+ },
632
+ props: {
633
+ isRoot: true,
634
+ },
635
+ });
636
+
637
+ logger.debug(
638
+ `Rule ${ruleId.toString()} reached no one due to a schedule gap; re-sampling ${retryCount + 1}/${maxScheduleGapRetries}.`,
639
+ {
640
+ onCallDutyPolicyEscalationRuleId: ruleId.toString(),
641
+ } as LogAttributes,
642
+ );
643
+ } else {
644
+ // Retries exhausted — advance normally next tick and reset the counter.
645
+ await OnCallDutyPolicyExecutionLogService.updateOneById({
646
+ id: options.onCallPolicyExecutionLogId,
647
+ data: {
648
+ scheduleGapRetryCount: 0,
649
+ },
650
+ props: {
651
+ isRoot: true,
652
+ },
653
+ });
654
+ }
655
+ } else {
656
+ // Reached someone (or no schedule gap): clear retry state for the next rule.
657
+ await OnCallDutyPolicyExecutionLogService.updateOneById({
658
+ id: options.onCallPolicyExecutionLogId,
659
+ data: {
660
+ scheduleGapRetryCount: 0,
661
+ },
662
+ props: {
663
+ isRoot: true,
664
+ },
665
+ });
666
+ }
667
+
524
668
  logger.debug(`Completed rule execution for ruleId: ${ruleId.toString()}`, {
525
669
  projectId: options.projectId.toString(),
526
670
  onCallDutyPolicyEscalationRuleId: ruleId.toString(),
@@ -912,8 +1056,8 @@ export class Service extends DatabaseService<Model> {
912
1056
  // moving down.
913
1057
 
914
1058
  for (const resource of resources) {
915
- if (resource.order! <= newOrder) {
916
- // increment order.
1059
+ if (resource.order! > currentOrder && resource.order! <= newOrder) {
1060
+ // decrement order to fill the gap left by the moved rule.
917
1061
  await this.updateOneBy({
918
1062
  query: {
919
1063
  _id: resource._id!,
@@ -261,22 +261,30 @@ export class Service extends DatabaseService<Model> {
261
261
  notifyUserId: userId || undefined,
262
262
  },
263
263
  });
264
-
265
- // also remove on-call duty time log.
266
- OnCallDutyPolicyTimeLogService.endTimeLogForUser({
267
- projectId: projectId,
268
- onCallDutyPolicyId: onCallDutyPolicyId,
269
- onCallDutyPolicyEscalationRuleId:
270
- item.onCallDutyPolicyEscalationRule!.id!,
271
- userId: userId,
272
- endsAt: OneUptimeDate.getCurrentDate(),
273
- }).catch((error: Error) => {
274
- logger.error(`Error ending time log for user ${userId}: ${error}`, {
275
- projectId: projectId?.toString(),
276
- userId: userId?.toString(),
277
- } as LogAttributes);
278
- });
279
264
  }
265
+
266
+ /*
267
+ * Close the removed user's open on-call time log. This MUST run
268
+ * independently of whether the user has a display name — invited users
269
+ * who have not finished signup commonly have a null/empty name, and the
270
+ * ADD path (onCreateSuccess) opens their time log regardless of name.
271
+ * Nesting this inside `if (user && user.name)` left those users' logs
272
+ * open forever, showing them as perpetually on-call in reporting
273
+ * (audit F19).
274
+ */
275
+ OnCallDutyPolicyTimeLogService.endTimeLogForUser({
276
+ projectId: projectId,
277
+ onCallDutyPolicyId: onCallDutyPolicyId,
278
+ onCallDutyPolicyEscalationRuleId:
279
+ item.onCallDutyPolicyEscalationRule!.id!,
280
+ userId: userId,
281
+ endsAt: OneUptimeDate.getCurrentDate(),
282
+ }).catch((error: Error) => {
283
+ logger.error(`Error ending time log for user ${userId}: ${error}`, {
284
+ projectId: projectId?.toString(),
285
+ userId: userId?.toString(),
286
+ } as LogAttributes);
287
+ });
280
288
  }
281
289
  }
282
290
 
@@ -26,6 +26,7 @@ import IncidentService from "./IncidentService";
26
26
  import AlertService from "./AlertService";
27
27
  import AlertEpisodeService from "./AlertEpisodeService";
28
28
  import IncidentEpisodeService from "./IncidentEpisodeService";
29
+ import { IsNull, UpdateResult } from "typeorm";
29
30
 
30
31
  export class Service extends DatabaseService<Model> {
31
32
  public constructor() {
@@ -35,6 +36,44 @@ export class Service extends DatabaseService<Model> {
35
36
  }
36
37
  }
37
38
 
39
+ /**
40
+ * Atomically claim the right to advance this execution to its next escalation
41
+ * rule on the current tick. Implemented as a single conditional
42
+ * `UPDATE ... WHERE lastEscalationRuleExecutedAt = <the value just read>`
43
+ * (via TypeORM's repository.update, which emits one atomic statement and
44
+ * returns the affected-row count), so that when two overlapping cron runs read
45
+ * the same lastEscalationRuleExecutedAt, exactly one wins — the loser sees
46
+ * `affected === 0` and bows out instead of double-paging responders (audit
47
+ * F13). `updateOneBy` could NOT provide this: it does a non-locking SELECT and
48
+ * then `save()` keyed only on `_id`, and returns the SELECT match count, so
49
+ * both overlapping runs would "win".
50
+ */
51
+ @CaptureSpan()
52
+ public async claimEscalationAdvance(data: {
53
+ executionLogId: ObjectID;
54
+ previousLastEscalationRuleExecutedAt: Date | null;
55
+ newLastEscalationRuleExecutedAt: Date;
56
+ }): Promise<boolean> {
57
+ const whereClause: Record<string, unknown> = {
58
+ _id: data.executionLogId.toString(),
59
+ status: OnCallDutyPolicyStatus.Executing,
60
+ lastEscalationRuleExecutedAt: data.previousLastEscalationRuleExecutedAt
61
+ ? data.previousLastEscalationRuleExecutedAt
62
+ : IsNull(),
63
+ };
64
+
65
+ const result: UpdateResult = await this.getRepository().update(
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ whereClause as any,
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ {
70
+ lastEscalationRuleExecutedAt: data.newLastEscalationRuleExecutedAt,
71
+ } as any,
72
+ );
73
+
74
+ return (result.affected || 0) > 0;
75
+ }
76
+
38
77
  @CaptureSpan()
39
78
  protected override async onBeforeCreate(
40
79
  createBy: CreateBy<Model>,
@@ -8,6 +8,12 @@ import LIMIT_MAX from "../../Types/Database/LimitMax";
8
8
  import BadDataException from "../../Types/Exception/BadDataException";
9
9
  import ObjectID from "../../Types/ObjectID";
10
10
  import PositiveNumber from "../../Types/PositiveNumber";
11
+ import Recurring from "../../Types/Events/Recurring";
12
+ import RestrictionTimes, {
13
+ RestrictionType,
14
+ } from "../../Types/OnCallDutyPolicy/RestrictionTimes";
15
+ import OneUptimeDate from "../../Types/Date";
16
+ import UpdateBy from "../Types/Database/UpdateBy";
11
17
  import Model from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayer";
12
18
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
13
19
  import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
@@ -17,6 +23,90 @@ export class Service extends DatabaseService<Model> {
17
23
  super(Model);
18
24
  }
19
25
 
26
+ /*
27
+ * A rotation interval count of 0 / NaN / negative breaks the handoff math
28
+ * (division by zero, Invalid Date), so reject it at the persistence boundary.
29
+ * The client also guards this, and the engine defensively clamps, but a raw
30
+ * API write must not be able to store an invalid rotation.
31
+ */
32
+ private validateRotationInterval(rotation: unknown): void {
33
+ if (!rotation || typeof rotation === "function") {
34
+ return;
35
+ }
36
+
37
+ let count: number;
38
+ try {
39
+ const recurring: Recurring =
40
+ rotation instanceof Recurring
41
+ ? rotation
42
+ : Recurring.fromJSON(rotation as any);
43
+ count = recurring.intervalCount.toNumber();
44
+ } catch {
45
+ throw new BadDataException("Invalid rotation configuration.");
46
+ }
47
+
48
+ if (
49
+ !Number.isFinite(count) ||
50
+ isNaN(count) ||
51
+ !Number.isInteger(count) ||
52
+ count < 1
53
+ ) {
54
+ throw new BadDataException(
55
+ "Rotation interval must be a whole number greater than or equal to 1.",
56
+ );
57
+ }
58
+ }
59
+
60
+ /*
61
+ * Reject a zero-length Daily restriction window (From == To time-of-day) at the
62
+ * persistence boundary. Such a window is active 0 seconds/day, so the layer
63
+ * silently pages nobody for its coverage. The dashboard already blocks this
64
+ * (audit F22), but a raw API write bypasses the form — so guard server-side
65
+ * too. Compares UTC time-of-day, which is stable regardless of server zone.
66
+ */
67
+ private validateRestrictionTimes(restrictionTimes: unknown): void {
68
+ if (!restrictionTimes || typeof restrictionTimes === "function") {
69
+ return;
70
+ }
71
+
72
+ let parsed: RestrictionTimes;
73
+ try {
74
+ parsed =
75
+ restrictionTimes instanceof RestrictionTimes
76
+ ? restrictionTimes
77
+ : RestrictionTimes.fromJSON(restrictionTimes as any);
78
+ } catch {
79
+ // Malformed input is handled elsewhere; nothing to validate here.
80
+ return;
81
+ }
82
+
83
+ if (
84
+ parsed.restictionType === RestrictionType.Daily &&
85
+ parsed.dayRestrictionTimes &&
86
+ parsed.dayRestrictionTimes.startTime &&
87
+ parsed.dayRestrictionTimes.endTime
88
+ ) {
89
+ const start: Date = OneUptimeDate.fromString(
90
+ parsed.dayRestrictionTimes.startTime as any,
91
+ );
92
+ const end: Date = OneUptimeDate.fromString(
93
+ parsed.dayRestrictionTimes.endTime as any,
94
+ );
95
+
96
+ const timeOfDay: (d: Date) => number = (d: Date): number => {
97
+ return (
98
+ d.getUTCHours() * 3600 + d.getUTCMinutes() * 60 + d.getUTCSeconds()
99
+ );
100
+ };
101
+
102
+ if (timeOfDay(start) === timeOfDay(end)) {
103
+ throw new BadDataException(
104
+ "Daily restriction 'From' and 'To' times cannot be the same. Choose a window with a positive duration, or set restrictions to None for 24/7 coverage.",
105
+ );
106
+ }
107
+ }
108
+ }
109
+
20
110
  @CaptureSpan()
21
111
  protected override async onBeforeCreate(
22
112
  createBy: CreateBy<Model>,
@@ -25,6 +115,9 @@ export class Service extends DatabaseService<Model> {
25
115
  throw new BadDataException("onCallDutyPolicyScheduleId is required");
26
116
  }
27
117
 
118
+ this.validateRotationInterval(createBy.data.rotation);
119
+ this.validateRestrictionTimes(createBy.data.restrictionTimes);
120
+
28
121
  if (!createBy.data.order) {
29
122
  // count number of users in this layer.
30
123
 
@@ -77,6 +170,19 @@ export class Service extends DatabaseService<Model> {
77
170
  return createdItem;
78
171
  }
79
172
 
173
+ @CaptureSpan()
174
+ protected override async onBeforeUpdate(
175
+ updateBy: UpdateBy<Model>,
176
+ ): Promise<OnUpdate<Model>> {
177
+ this.validateRotationInterval(updateBy.data.rotation);
178
+ this.validateRestrictionTimes(updateBy.data.restrictionTimes);
179
+
180
+ return {
181
+ updateBy,
182
+ carryForward: null,
183
+ };
184
+ }
185
+
80
186
  protected override async onUpdateSuccess(
81
187
  _onUpdate: OnUpdate<Model>,
82
188
  updatedItemIds: Array<ObjectID>,
@@ -107,6 +213,48 @@ export class Service extends DatabaseService<Model> {
107
213
  };
108
214
  }
109
215
 
216
+ @CaptureSpan()
217
+ protected override async onBeforeDelete(
218
+ deleteBy: DeleteBy<Model>,
219
+ ): Promise<OnDelete<Model>> {
220
+ /*
221
+ * Capture the layer's order + schedule id as carryForward so onDeleteSuccess
222
+ * can re-sequence the remaining layers AND refresh the schedule roster after
223
+ * a layer is removed. Without this hook, onDeleteSuccess (which gates all of
224
+ * its work on onDelete.carryForward) was dead code: deleting a layer never
225
+ * re-sequenced order and never called
226
+ * refreshCurrentUserIdAndHandoffTimeInSchedule, so the schedule kept showing
227
+ * the removed layer's user as on-call and the genuinely-now-on-call user got
228
+ * no handoff notification for up to a full rotation period (audit F3).
229
+ * Mirrors OnCallDutyPolicyScheduleLayerUserService.onBeforeDelete.
230
+ */
231
+ if (!deleteBy.query._id && !deleteBy.props.isRoot) {
232
+ throw new BadDataException(
233
+ "_id should be present when deleting a schedule layer. Please try the delete with objectId",
234
+ );
235
+ }
236
+
237
+ let resource: Model | null = null;
238
+
239
+ if (!deleteBy.props.isRoot) {
240
+ resource = await this.findOneBy({
241
+ query: deleteBy.query,
242
+ props: {
243
+ isRoot: true,
244
+ },
245
+ select: {
246
+ order: true,
247
+ onCallDutyPolicyScheduleId: true,
248
+ },
249
+ });
250
+ }
251
+
252
+ return {
253
+ deleteBy,
254
+ carryForward: resource,
255
+ };
256
+ }
257
+
110
258
  @CaptureSpan()
111
259
  protected override async onDeleteSuccess(
112
260
  onDelete: OnDelete<Model>,
@@ -247,8 +247,17 @@ export class Service extends DatabaseService<Model> {
247
247
  // moving down.
248
248
 
249
249
  for (const resource of resources) {
250
- if (resource.order! <= newOrder) {
251
- // increment order.
250
+ /*
251
+ * Only shift rows strictly BETWEEN the old and new position. The lower
252
+ * bound (order > currentOrder) was missing, so every row above the
253
+ * moved user was also decremented — driving the top row's order to 0
254
+ * (and negative after repeated down-drags) and opening a gap at 1,
255
+ * breaking the 1-based contiguous invariant that create-default
256
+ * (count+1) and delete re-sequencing rely on (audit L3). Mirrors the
257
+ * double-bounded moving-up branch above.
258
+ */
259
+ if (resource.order! <= newOrder && resource.order! > currentOrder) {
260
+ // decrement order.
252
261
  await this.updateOneBy({
253
262
  query: {
254
263
  _id: resource._id!,