@oneuptime/common 12.0.29 → 12.0.30

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 (189) hide show
  1. package/Models/DatabaseModels/Index.ts +11 -0
  2. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  3. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  4. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  5. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  6. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  7. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  8. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  9. package/Server/EnvironmentConfig.ts +138 -0
  10. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  13. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  14. package/Server/Services/Index.ts +12 -0
  15. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  16. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  17. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  18. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  19. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  20. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  21. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  22. package/Server/Services/TeamMemberService.ts +489 -1
  23. package/Server/Services/TeamService.ts +50 -0
  24. package/Server/Services/UserNotificationSettingService.ts +44 -1
  25. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  26. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  27. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  28. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  29. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  30. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  31. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  32. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  33. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  34. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  35. package/Server/Utils/Response.ts +134 -0
  36. package/Server/Utils/StartServer.ts +10 -0
  37. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  38. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  39. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  40. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  41. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  42. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  43. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  44. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  45. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  46. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  47. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  48. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  49. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  50. package/Tests/Server/API/Helpers.ts +8 -0
  51. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  52. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  53. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  54. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  55. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  56. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  57. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  58. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  59. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  60. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  61. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  62. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  63. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  64. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  65. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  66. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  67. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  68. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  69. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  70. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  71. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  72. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  73. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  74. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  75. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  76. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  77. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  78. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  79. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  80. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  82. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  83. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  84. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  85. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  86. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  87. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  88. package/Types/Calendar/ICalendar.ts +442 -0
  89. package/Types/Email/EmailTemplateType.ts +2 -0
  90. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  91. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  92. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  93. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  94. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  95. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  96. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  97. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  100. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  102. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  103. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  104. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  105. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  106. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  107. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  108. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  109. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  110. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  111. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  112. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  113. package/build/dist/Server/EnvironmentConfig.js +102 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  116. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  122. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  123. package/build/dist/Server/Services/Index.js +12 -0
  124. package/build/dist/Server/Services/Index.js.map +1 -1
  125. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  126. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  127. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  128. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  129. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  130. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  131. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  132. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  133. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  134. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  135. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  136. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  137. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  138. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  139. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  140. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  141. package/build/dist/Server/Services/TeamService.js +43 -0
  142. package/build/dist/Server/Services/TeamService.js.map +1 -1
  143. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  144. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  145. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  146. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  147. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  148. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  149. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  150. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  151. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  152. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  153. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  154. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  155. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  156. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  157. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  158. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  159. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  160. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  161. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  162. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  163. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  164. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  165. package/build/dist/Server/Utils/Response.js +97 -0
  166. package/build/dist/Server/Utils/Response.js.map +1 -1
  167. package/build/dist/Server/Utils/StartServer.js +9 -1
  168. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  169. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  170. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  171. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  172. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  173. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  174. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  175. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  176. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  177. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  178. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  179. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  180. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  181. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  182. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  183. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  184. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  185. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  186. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  187. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  188. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,310 @@
1
+ import moment from "moment-timezone";
2
+ import User from "../../../Models/DatabaseModels/User";
3
+ import OneUptimeDate from "../../../Types/Date";
4
+ import EventInterval from "../../../Types/Events/EventInterval";
5
+ import Recurring from "../../../Types/Events/Recurring";
6
+ import MaterializedShiftUtil, {
7
+ MaterializedShift,
8
+ MaterializedShiftPolicy,
9
+ } from "../../../Types/OnCallDutyPolicy/MaterializedShift";
10
+ import RestrictionTimes, {
11
+ RestrictionType,
12
+ WeeklyResctriction,
13
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
14
+ import { OnCallShift } from "../../../Types/OnCallDutyPolicy/ScheduleShiftUtil";
15
+
16
+ /*
17
+ * Shared fixtures for the on-call calendar feed suites. Everything is built
18
+ * from explicit IANA zones and fixed ISO instants (the tzInstant / hhmm idiom
19
+ * from LayerUtilTimezone.test.ts) so the results do not depend on the zone
20
+ * the test process happens to run in.
21
+ */
22
+
23
+ export function user(id: string): User {
24
+ return {
25
+ id: {
26
+ toString: (): string => {
27
+ return id;
28
+ },
29
+ } as any,
30
+ } as User;
31
+ }
32
+
33
+ export function rotation(
34
+ intervalType: EventInterval,
35
+ intervalCount: number,
36
+ ): Recurring {
37
+ return Recurring.fromJSON({
38
+ _type: "Recurring",
39
+ value: {
40
+ intervalType: intervalType,
41
+ intervalCount: { _type: "PositiveNumber", value: intervalCount },
42
+ },
43
+ } as any);
44
+ }
45
+
46
+ export function noRestriction(): RestrictionTimes {
47
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
48
+ restrictionTimes.restictionType = RestrictionType.None;
49
+ restrictionTimes.dayRestrictionTimes = null;
50
+ return restrictionTimes;
51
+ }
52
+
53
+ // An absolute instant that is `iso` ("YYYY-MM-DD HH:mm") wall-clock in `tz`.
54
+ export function tzInstant(iso: string, tz: string): Date {
55
+ return moment.tz(iso, tz).toDate();
56
+ }
57
+
58
+ // Wall-clock "HH:mm" of an instant AS SEEN IN `tz`.
59
+ export function hhmm(date: Date, tz: string): string {
60
+ return moment.tz(date, tz).format("HH:mm");
61
+ }
62
+
63
+ export function at(iso: string): Date {
64
+ return OneUptimeDate.fromString(iso);
65
+ }
66
+
67
+ /*
68
+ * Daily HH:mm-HH:mm restriction authored in `tz`. LayerUtil only reads the
69
+ * wall-clock time of the two instants (keepTimeButMoveDay), so the date part
70
+ * is irrelevant.
71
+ */
72
+ export function dailyRestriction(
73
+ startHHmm: string,
74
+ endHHmm: string,
75
+ tz: string,
76
+ ): RestrictionTimes {
77
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
78
+ restrictionTimes.restictionType = RestrictionType.Daily;
79
+ restrictionTimes.dayRestrictionTimes = {
80
+ startTime: tzInstant(`2026-01-05 ${startHHmm}`, tz),
81
+ endTime: tzInstant(`2026-01-05 ${endHHmm}`, tz),
82
+ };
83
+ return restrictionTimes;
84
+ }
85
+
86
+ /*
87
+ * Office hours, Mon-Fri 09:00-17:00 in `tz`, authored as five weekday windows
88
+ * on the week of Monday 2026-01-05. The engine derives the enforced weekday
89
+ * from each instant's weekday in `tz`.
90
+ */
91
+ export function businessHoursRestriction(tz: string): RestrictionTimes {
92
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
93
+ restrictionTimes.restictionType = RestrictionType.Weekly;
94
+ restrictionTimes.weeklyRestrictionTimes = [
95
+ "2026-01-05",
96
+ "2026-01-06",
97
+ "2026-01-07",
98
+ "2026-01-08",
99
+ "2026-01-09",
100
+ ].map((day: string): WeeklyResctriction => {
101
+ const startTime: Date = tzInstant(`${day} 09:00`, tz);
102
+ const endTime: Date = tzInstant(`${day} 17:00`, tz);
103
+ return {
104
+ startDay: OneUptimeDate.getDayOfWeek(startTime, tz),
105
+ endDay: OneUptimeDate.getDayOfWeek(endTime, tz),
106
+ startTime,
107
+ endTime,
108
+ };
109
+ });
110
+ return restrictionTimes;
111
+ }
112
+
113
+ export const DEFAULT_POLICY: MaterializedShiftPolicy = {
114
+ policyId: "pol-1",
115
+ policyName: "Payments Policy",
116
+ ruleId: "rule-1",
117
+ ruleName: "Primary",
118
+ ruleOrder: 1,
119
+ };
120
+
121
+ export const DEFAULT_LAST_MODIFIED: Date = at("2026-08-01T10:00:00Z");
122
+
123
+ export const DASHBOARD_URL: string = "https://oneuptime.example.com/dashboard";
124
+
125
+ export type ShiftOverrides = Omit<
126
+ Partial<MaterializedShift>,
127
+ "scheduleTimezone"
128
+ > & {
129
+ start: Date;
130
+ end: Date;
131
+ // Pass an explicit undefined to build a legacy shift with no timezone.
132
+ scheduleTimezone?: string | undefined;
133
+ };
134
+
135
+ /*
136
+ * A MaterializedShift with sensible defaults: Alice on the "Payments"
137
+ * schedule (Europe/Stockholm) attached to one policy. Optional fields are only
138
+ * added when the caller supplies them, so key presence matches what the
139
+ * materializer would produce.
140
+ */
141
+ export function shift(overrides: ShiftOverrides): MaterializedShift {
142
+ const scheduleId: string = overrides.scheduleId ?? "sched-1";
143
+ const policyVariantOf: MaterializedShift["policyVariantOf"] =
144
+ overrides.policyVariantOf;
145
+
146
+ const base: MaterializedShift = {
147
+ shiftKey:
148
+ overrides.shiftKey ??
149
+ MaterializedShiftUtil.buildShiftKey({
150
+ scheduleId,
151
+ start: overrides.start,
152
+ policyId: policyVariantOf?.policyId,
153
+ }),
154
+ contentHash: overrides.contentHash ?? "hash",
155
+ projectId: overrides.projectId ?? "proj-1",
156
+ scheduleId,
157
+ scheduleName: overrides.scheduleName ?? "Payments",
158
+ userId: overrides.userId ?? "user-a",
159
+ userName: overrides.userName ?? "Alice Andersson",
160
+ start: overrides.start,
161
+ end: overrides.end,
162
+ coverageSeconds:
163
+ overrides.coverageSeconds ??
164
+ Math.round((overrides.end.getTime() - overrides.start.getTime()) / 1000),
165
+ policies: overrides.policies ?? [{ ...DEFAULT_POLICY }],
166
+ isPast: overrides.isPast ?? false,
167
+ lastModifiedAt: overrides.lastModifiedAt ?? DEFAULT_LAST_MODIFIED,
168
+ shiftConfigVersion: overrides.shiftConfigVersion ?? 3,
169
+ };
170
+
171
+ if ("scheduleTimezone" in overrides) {
172
+ if (overrides.scheduleTimezone !== undefined) {
173
+ base.scheduleTimezone = overrides.scheduleTimezone;
174
+ }
175
+ } else {
176
+ base.scheduleTimezone = "Europe/Stockholm";
177
+ }
178
+
179
+ if (overrides.projectName !== undefined) {
180
+ base.projectName = overrides.projectName;
181
+ }
182
+
183
+ if (overrides.layerId !== undefined) {
184
+ base.layerId = overrides.layerId;
185
+ }
186
+
187
+ if (overrides.layerName !== undefined) {
188
+ base.layerName = overrides.layerName;
189
+ }
190
+
191
+ if (overrides.override !== undefined) {
192
+ base.override = overrides.override;
193
+ }
194
+
195
+ if (policyVariantOf !== undefined) {
196
+ base.policyVariantOf = policyVariantOf;
197
+ }
198
+
199
+ return base;
200
+ }
201
+
202
+ // Turn engine-derived OnCallShifts into MaterializedShifts on one schedule.
203
+ export function materialize(
204
+ shifts: Array<OnCallShift>,
205
+ overrides?: Partial<MaterializedShift> | undefined,
206
+ ): Array<MaterializedShift> {
207
+ return shifts.map((onCallShift: OnCallShift) => {
208
+ const extra: Partial<MaterializedShift> = { ...(overrides ?? {}) };
209
+
210
+ if (onCallShift.override) {
211
+ extra.override = {
212
+ originalUserId: onCallShift.override.originalUserId,
213
+ originalUserName: `Name of ${onCallShift.override.originalUserId}`,
214
+ overrideStartsAt: onCallShift.override.overrideStartsAt,
215
+ overrideEndsAt: onCallShift.override.overrideEndsAt,
216
+ };
217
+ }
218
+
219
+ if (onCallShift.layerId !== undefined) {
220
+ extra.layerId = onCallShift.layerId;
221
+ }
222
+
223
+ if (onCallShift.layerName !== undefined) {
224
+ extra.layerName = onCallShift.layerName;
225
+ }
226
+
227
+ return shift({
228
+ ...extra,
229
+ start: onCallShift.start,
230
+ end: onCallShift.end,
231
+ userId: onCallShift.userId,
232
+ userName: `Name of ${onCallShift.userId}`,
233
+ coverageSeconds: onCallShift.coverageSeconds,
234
+ });
235
+ });
236
+ }
237
+
238
+ // Physical lines of a serialized calendar (CRLF separated, no folding undone).
239
+ export function physicalLines(body: string): Array<string> {
240
+ const lines: Array<string> = body.split("\r\n");
241
+ // A well-formed body ends with CRLF, leaving one empty trailing element.
242
+ if (lines[lines.length - 1] === "") {
243
+ lines.pop();
244
+ }
245
+ return lines;
246
+ }
247
+
248
+ // Logical (unfolded) lines of a serialized calendar.
249
+ export function logicalLines(body: string): Array<string> {
250
+ return physicalLines(body.replace(/\r\n[ \t]/g, ""));
251
+ }
252
+
253
+ // The value of the first logical line starting with `NAME:` / `NAME;`.
254
+ export function property(body: string, name: string): string | undefined {
255
+ const line: string | undefined = logicalLines(body).find((entry: string) => {
256
+ return entry.startsWith(`${name}:`) || entry.startsWith(`${name};`);
257
+ });
258
+
259
+ if (line === undefined) {
260
+ return undefined;
261
+ }
262
+
263
+ return line.slice(line.indexOf(":") + 1);
264
+ }
265
+
266
+ // Every value of a repeated property (e.g. every UID in the body).
267
+ export function properties(body: string, name: string): Array<string> {
268
+ return logicalLines(body)
269
+ .filter((entry: string) => {
270
+ return entry.startsWith(`${name}:`) || entry.startsWith(`${name};`);
271
+ })
272
+ .map((entry: string) => {
273
+ return entry.slice(entry.indexOf(":") + 1);
274
+ });
275
+ }
276
+
277
+ // The logical lines of each VEVENT block, in document order.
278
+ export function eventBlocks(body: string): Array<Array<string>> {
279
+ const blocks: Array<Array<string>> = [];
280
+ let current: Array<string> | null = null;
281
+
282
+ for (const line of logicalLines(body)) {
283
+ if (line === "BEGIN:VEVENT") {
284
+ current = [];
285
+ continue;
286
+ }
287
+ if (line === "END:VEVENT") {
288
+ if (current) {
289
+ blocks.push(current);
290
+ }
291
+ current = null;
292
+ continue;
293
+ }
294
+ if (current) {
295
+ current.push(line);
296
+ }
297
+ }
298
+
299
+ return blocks;
300
+ }
301
+
302
+ export function blockProperty(
303
+ block: Array<string>,
304
+ name: string,
305
+ ): string | undefined {
306
+ const line: string | undefined = block.find((entry: string) => {
307
+ return entry.startsWith(`${name}:`) || entry.startsWith(`${name};`);
308
+ });
309
+ return line === undefined ? undefined : line.slice(line.indexOf(":") + 1);
310
+ }
@@ -0,0 +1,205 @@
1
+ import CalendarFeedWindow, {
2
+ BODY_CACHE_TTL_SECONDS,
3
+ DEFAULT_FUTURE_DAYS,
4
+ DEFAULT_PAST_DAYS,
5
+ FEED_SIMULATION_ITERATION_CAP,
6
+ FeedWindow,
7
+ MAX_EVENTS,
8
+ MAX_FUTURE_DAYS,
9
+ MAX_GAP_EVENTS,
10
+ MAX_PAST_DAYS,
11
+ MIN_FUTURE_DAYS,
12
+ PREVIOUS_TOKEN_GRACE_DAYS,
13
+ RENDER_CONCURRENCY,
14
+ SCHEDULE_CACHE_TTL_SECONDS,
15
+ } from "../../../Types/OnCallDutyPolicy/CalendarFeedWindow";
16
+ import { at } from "./CalendarFeedTestFixtures";
17
+
18
+ describe("CalendarFeedWindow constants", () => {
19
+ test("hold the values the design fixes", () => {
20
+ expect(MAX_FUTURE_DAYS).toBe(180);
21
+ expect(MIN_FUTURE_DAYS).toBe(7);
22
+ expect(DEFAULT_FUTURE_DAYS).toBe(90);
23
+ expect(MAX_PAST_DAYS).toBe(60);
24
+ expect(DEFAULT_PAST_DAYS).toBe(2);
25
+ expect(MAX_EVENTS).toBe(5000);
26
+ expect(MAX_GAP_EVENTS).toBe(100);
27
+ expect(PREVIOUS_TOKEN_GRACE_DAYS).toBe(30);
28
+ expect(BODY_CACHE_TTL_SECONDS).toBe(300);
29
+ expect(SCHEDULE_CACHE_TTL_SECONDS).toBe(3600);
30
+ expect(RENDER_CONCURRENCY).toBe(4);
31
+ expect(FEED_SIMULATION_ITERATION_CAP).toBe(200000);
32
+ });
33
+
34
+ test("are internally consistent", () => {
35
+ expect(MIN_FUTURE_DAYS).toBeLessThanOrEqual(DEFAULT_FUTURE_DAYS);
36
+ expect(DEFAULT_FUTURE_DAYS).toBeLessThanOrEqual(MAX_FUTURE_DAYS);
37
+ expect(DEFAULT_PAST_DAYS).toBeLessThanOrEqual(MAX_PAST_DAYS);
38
+ expect(MAX_GAP_EVENTS).toBeLessThan(MAX_EVENTS);
39
+ });
40
+ });
41
+
42
+ describe("CalendarFeedWindow.clampPastDays", () => {
43
+ test("keeps values inside 0..MAX_PAST_DAYS and clamps the rest", () => {
44
+ expect(CalendarFeedWindow.clampPastDays(0)).toBe(0);
45
+ expect(CalendarFeedWindow.clampPastDays(2)).toBe(2);
46
+ expect(CalendarFeedWindow.clampPastDays(60)).toBe(60);
47
+ expect(CalendarFeedWindow.clampPastDays(61)).toBe(60);
48
+ expect(CalendarFeedWindow.clampPastDays(999)).toBe(60);
49
+ expect(CalendarFeedWindow.clampPastDays(-5)).toBe(0);
50
+ });
51
+
52
+ test("floors fractions and parses numeric strings (as stored Number columns arrive)", () => {
53
+ expect(CalendarFeedWindow.clampPastDays(2.9)).toBe(2);
54
+ expect(CalendarFeedWindow.clampPastDays("7")).toBe(7);
55
+ expect(CalendarFeedWindow.clampPastDays(" 90 ")).toBe(60);
56
+ });
57
+
58
+ test("falls back to the default for anything that is not a number", () => {
59
+ expect(CalendarFeedWindow.clampPastDays(undefined)).toBe(DEFAULT_PAST_DAYS);
60
+ expect(CalendarFeedWindow.clampPastDays(null)).toBe(DEFAULT_PAST_DAYS);
61
+ expect(CalendarFeedWindow.clampPastDays(Number.NaN)).toBe(
62
+ DEFAULT_PAST_DAYS,
63
+ );
64
+ expect(CalendarFeedWindow.clampPastDays("abc")).toBe(DEFAULT_PAST_DAYS);
65
+ expect(CalendarFeedWindow.clampPastDays("")).toBe(DEFAULT_PAST_DAYS);
66
+ expect(CalendarFeedWindow.clampPastDays(Number.POSITIVE_INFINITY)).toBe(
67
+ DEFAULT_PAST_DAYS,
68
+ );
69
+ expect(CalendarFeedWindow.clampPastDays({})).toBe(DEFAULT_PAST_DAYS);
70
+ });
71
+ });
72
+
73
+ describe("CalendarFeedWindow.clampFutureDays", () => {
74
+ test("keeps values inside MIN..MAX and clamps the rest", () => {
75
+ expect(CalendarFeedWindow.clampFutureDays(7)).toBe(7);
76
+ expect(CalendarFeedWindow.clampFutureDays(90)).toBe(90);
77
+ expect(CalendarFeedWindow.clampFutureDays(180)).toBe(180);
78
+ expect(CalendarFeedWindow.clampFutureDays(181)).toBe(180);
79
+ expect(CalendarFeedWindow.clampFutureDays(6)).toBe(7);
80
+ expect(CalendarFeedWindow.clampFutureDays(0)).toBe(7);
81
+ expect(CalendarFeedWindow.clampFutureDays(-1)).toBe(7);
82
+ });
83
+
84
+ test("falls back to the default for anything that is not a number", () => {
85
+ expect(CalendarFeedWindow.clampFutureDays(undefined)).toBe(
86
+ DEFAULT_FUTURE_DAYS,
87
+ );
88
+ expect(CalendarFeedWindow.clampFutureDays(null)).toBe(DEFAULT_FUTURE_DAYS);
89
+ expect(CalendarFeedWindow.clampFutureDays("x")).toBe(DEFAULT_FUTURE_DAYS);
90
+ expect(CalendarFeedWindow.clampFutureDays("30")).toBe(30);
91
+ });
92
+ });
93
+
94
+ describe("CalendarFeedWindow.computeFeedWindow", () => {
95
+ test("puts both edges on UTC midnights around a mid-day instant", () => {
96
+ const window: FeedWindow = CalendarFeedWindow.computeFeedWindow({
97
+ now: at("2026-08-31T14:37:12Z"),
98
+ pastDays: 2,
99
+ futureDays: 90,
100
+ });
101
+
102
+ expect(window.feedStart.toISOString()).toBe("2026-08-29T00:00:00.000Z");
103
+ // 90 days ahead is 2026-11-29; the exclusive end is the following midnight.
104
+ expect(window.feedEnd.toISOString()).toBe("2026-11-30T00:00:00.000Z");
105
+ });
106
+
107
+ test("is identical for every instant inside the same UTC day", () => {
108
+ const first: FeedWindow = CalendarFeedWindow.computeFeedWindow({
109
+ now: at("2026-08-31T00:00:00Z"),
110
+ pastDays: 2,
111
+ futureDays: 30,
112
+ });
113
+ const last: FeedWindow = CalendarFeedWindow.computeFeedWindow({
114
+ now: at("2026-08-31T23:59:59.999Z"),
115
+ pastDays: 2,
116
+ futureDays: 30,
117
+ });
118
+
119
+ expect(first.feedStart.getTime()).toBe(last.feedStart.getTime());
120
+ expect(first.feedEnd.getTime()).toBe(last.feedEnd.getTime());
121
+ });
122
+
123
+ test("changes at the UTC day boundary, not the local one", () => {
124
+ const before: FeedWindow = CalendarFeedWindow.computeFeedWindow({
125
+ now: at("2026-08-31T23:59:59Z"),
126
+ pastDays: 0,
127
+ futureDays: 7,
128
+ });
129
+ const after: FeedWindow = CalendarFeedWindow.computeFeedWindow({
130
+ now: at("2026-09-01T00:00:00Z"),
131
+ pastDays: 0,
132
+ futureDays: 7,
133
+ });
134
+
135
+ expect(before.feedStart.toISOString()).toBe("2026-08-31T00:00:00.000Z");
136
+ expect(after.feedStart.toISOString()).toBe("2026-09-01T00:00:00.000Z");
137
+ expect(after.feedEnd.getTime() - before.feedEnd.getTime()).toBe(
138
+ 24 * 3600 * 1000,
139
+ );
140
+ });
141
+
142
+ test("clamps the day counts before applying them", () => {
143
+ const window: FeedWindow = CalendarFeedWindow.computeFeedWindow({
144
+ now: at("2026-08-31T12:00:00Z"),
145
+ pastDays: 500,
146
+ futureDays: 1,
147
+ });
148
+
149
+ expect(window.feedStart.toISOString()).toBe("2026-07-02T00:00:00.000Z"); // 60 days back
150
+ expect(window.feedEnd.toISOString()).toBe("2026-09-08T00:00:00.000Z"); // MIN 7 days + 1
151
+ });
152
+
153
+ test("pastDays 0 starts today; the window always spans at least MIN_FUTURE_DAYS + 1 days", () => {
154
+ const window: FeedWindow = CalendarFeedWindow.computeFeedWindow({
155
+ now: at("2026-02-28T09:00:00Z"),
156
+ pastDays: 0,
157
+ futureDays: 7,
158
+ });
159
+
160
+ expect(window.feedStart.toISOString()).toBe("2026-02-28T00:00:00.000Z");
161
+ expect(window.feedEnd.toISOString()).toBe("2026-03-08T00:00:00.000Z");
162
+ expect(
163
+ (window.feedEnd.getTime() - window.feedStart.getTime()) / 86400000,
164
+ ).toBe(8);
165
+ });
166
+
167
+ test("survives leap days and year boundaries", () => {
168
+ const window: FeedWindow = CalendarFeedWindow.computeFeedWindow({
169
+ now: at("2028-02-29T12:00:00Z"),
170
+ pastDays: 1,
171
+ futureDays: 7,
172
+ });
173
+
174
+ expect(window.feedStart.toISOString()).toBe("2028-02-28T00:00:00.000Z");
175
+ expect(window.feedEnd.toISOString()).toBe("2028-03-08T00:00:00.000Z");
176
+
177
+ const newYear: FeedWindow = CalendarFeedWindow.computeFeedWindow({
178
+ now: at("2026-12-31T23:00:00Z"),
179
+ pastDays: 2,
180
+ futureDays: 7,
181
+ });
182
+
183
+ expect(newYear.feedStart.toISOString()).toBe("2026-12-29T00:00:00.000Z");
184
+ expect(newYear.feedEnd.toISOString()).toBe("2027-01-08T00:00:00.000Z");
185
+ });
186
+ });
187
+
188
+ describe("CalendarFeedWindow.startOfUtcDay / getUtcDayBucket", () => {
189
+ test("startOfUtcDay truncates to 00:00:00.000 UTC", () => {
190
+ expect(
191
+ new Date(
192
+ CalendarFeedWindow.startOfUtcDay(at("2026-08-31T23:59:59.999Z")),
193
+ ).toISOString(),
194
+ ).toBe("2026-08-31T00:00:00.000Z");
195
+ });
196
+
197
+ test("getUtcDayBucket is the UTC calendar date", () => {
198
+ expect(CalendarFeedWindow.getUtcDayBucket(at("2026-08-31T23:59:59Z"))).toBe(
199
+ "2026-08-31",
200
+ );
201
+ expect(CalendarFeedWindow.getUtcDayBucket(at("2026-09-01T00:00:00Z"))).toBe(
202
+ "2026-09-01",
203
+ );
204
+ });
205
+ });