@bash-app/bash-common 30.246.0 → 30.253.0

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 (180) hide show
  1. package/README.md +4 -0
  2. package/dist/__tests__/bashSectionTitleForBashType.test.js +32 -1
  3. package/dist/__tests__/bashSectionTitleForBashType.test.js.map +1 -1
  4. package/dist/__tests__/definitionsHelpers.test.d.ts +2 -0
  5. package/dist/__tests__/definitionsHelpers.test.d.ts.map +1 -0
  6. package/dist/__tests__/definitionsHelpers.test.js +85 -0
  7. package/dist/__tests__/definitionsHelpers.test.js.map +1 -0
  8. package/dist/__tests__/extendedSchemas.test.d.ts +2 -0
  9. package/dist/__tests__/extendedSchemas.test.d.ts.map +1 -0
  10. package/dist/__tests__/extendedSchemas.test.js +40 -0
  11. package/dist/__tests__/extendedSchemas.test.js.map +1 -0
  12. package/dist/__tests__/membershipDefinitions.test.d.ts +2 -0
  13. package/dist/__tests__/membershipDefinitions.test.d.ts.map +1 -0
  14. package/dist/__tests__/membershipDefinitions.test.js +95 -0
  15. package/dist/__tests__/membershipDefinitions.test.js.map +1 -0
  16. package/dist/__tests__/ticketAdmission.test.d.ts +5 -0
  17. package/dist/__tests__/ticketAdmission.test.d.ts.map +1 -0
  18. package/dist/__tests__/ticketAdmission.test.js +42 -0
  19. package/dist/__tests__/ticketAdmission.test.js.map +1 -0
  20. package/dist/__tests__/utmAttribution.test.d.ts +2 -0
  21. package/dist/__tests__/utmAttribution.test.d.ts.map +1 -0
  22. package/dist/__tests__/utmAttribution.test.js +92 -0
  23. package/dist/__tests__/utmAttribution.test.js.map +1 -0
  24. package/dist/definitions.d.ts +18 -5
  25. package/dist/definitions.d.ts.map +1 -1
  26. package/dist/definitions.js +84 -22
  27. package/dist/definitions.js.map +1 -1
  28. package/dist/extendedSchemas.d.ts +41 -1
  29. package/dist/extendedSchemas.d.ts.map +1 -1
  30. package/dist/extendedSchemas.js +27 -0
  31. package/dist/extendedSchemas.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/sms/smsTemplates.d.ts +27 -0
  37. package/dist/sms/smsTemplates.d.ts.map +1 -0
  38. package/dist/sms/smsTemplates.js +11 -0
  39. package/dist/sms/smsTemplates.js.map +1 -0
  40. package/dist/utils/__tests__/addressUtils.test.d.ts +2 -0
  41. package/dist/utils/__tests__/addressUtils.test.d.ts.map +1 -0
  42. package/dist/utils/__tests__/addressUtils.test.js +138 -0
  43. package/dist/utils/__tests__/addressUtils.test.js.map +1 -0
  44. package/dist/utils/__tests__/apiUtils.test.d.ts +5 -0
  45. package/dist/utils/__tests__/apiUtils.test.d.ts.map +1 -0
  46. package/dist/utils/__tests__/apiUtils.test.js +161 -0
  47. package/dist/utils/__tests__/apiUtils.test.js.map +1 -0
  48. package/dist/utils/__tests__/arrayUtils.test.d.ts +2 -0
  49. package/dist/utils/__tests__/arrayUtils.test.d.ts.map +1 -0
  50. package/dist/utils/__tests__/arrayUtils.test.js +23 -0
  51. package/dist/utils/__tests__/arrayUtils.test.js.map +1 -0
  52. package/dist/utils/__tests__/blogUtils.test.d.ts +2 -0
  53. package/dist/utils/__tests__/blogUtils.test.d.ts.map +1 -0
  54. package/dist/utils/__tests__/blogUtils.test.js +102 -0
  55. package/dist/utils/__tests__/blogUtils.test.js.map +1 -0
  56. package/dist/utils/__tests__/dateTimeUtils.test.d.ts +2 -0
  57. package/dist/utils/__tests__/dateTimeUtils.test.d.ts.map +1 -0
  58. package/dist/utils/__tests__/dateTimeUtils.test.js +180 -0
  59. package/dist/utils/__tests__/dateTimeUtils.test.js.map +1 -0
  60. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts +2 -0
  61. package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts.map +1 -0
  62. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js +356 -0
  63. package/dist/utils/__tests__/discountEngine/discountCalculator.test.js.map +1 -0
  64. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts +2 -0
  65. package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts.map +1 -0
  66. package/dist/utils/__tests__/generalDateTimeUtils.test.js +26 -0
  67. package/dist/utils/__tests__/generalDateTimeUtils.test.js.map +1 -0
  68. package/dist/utils/__tests__/luxonUtils.test.d.ts +2 -0
  69. package/dist/utils/__tests__/luxonUtils.test.d.ts.map +1 -0
  70. package/dist/utils/__tests__/luxonUtils.test.js +139 -0
  71. package/dist/utils/__tests__/luxonUtils.test.js.map +1 -0
  72. package/dist/utils/__tests__/mathUtils.test.d.ts +5 -0
  73. package/dist/utils/__tests__/mathUtils.test.d.ts.map +1 -0
  74. package/dist/utils/__tests__/mathUtils.test.js +26 -0
  75. package/dist/utils/__tests__/mathUtils.test.js.map +1 -0
  76. package/dist/utils/__tests__/objUtils.test.d.ts +5 -0
  77. package/dist/utils/__tests__/objUtils.test.d.ts.map +1 -0
  78. package/dist/utils/__tests__/objUtils.test.js +41 -0
  79. package/dist/utils/__tests__/objUtils.test.js.map +1 -0
  80. package/dist/utils/__tests__/paymentUtils.test.js +48 -1
  81. package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
  82. package/dist/utils/__tests__/qrCodeUtils.test.js +16 -1
  83. package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -1
  84. package/dist/utils/__tests__/reviewUtils.test.d.ts +2 -0
  85. package/dist/utils/__tests__/reviewUtils.test.d.ts.map +1 -0
  86. package/dist/utils/__tests__/reviewUtils.test.js +49 -0
  87. package/dist/utils/__tests__/reviewUtils.test.js.map +1 -0
  88. package/dist/utils/__tests__/slugUtils.test.d.ts +2 -0
  89. package/dist/utils/__tests__/slugUtils.test.d.ts.map +1 -0
  90. package/dist/utils/__tests__/slugUtils.test.js +103 -0
  91. package/dist/utils/__tests__/slugUtils.test.js.map +1 -0
  92. package/dist/utils/__tests__/sortUtils.test.d.ts +5 -0
  93. package/dist/utils/__tests__/sortUtils.test.d.ts.map +1 -0
  94. package/dist/utils/__tests__/sortUtils.test.js +28 -0
  95. package/dist/utils/__tests__/sortUtils.test.js.map +1 -0
  96. package/dist/utils/__tests__/stringUtils.test.d.ts +2 -0
  97. package/dist/utils/__tests__/stringUtils.test.d.ts.map +1 -0
  98. package/dist/utils/__tests__/stringUtils.test.js +27 -0
  99. package/dist/utils/__tests__/stringUtils.test.js.map +1 -0
  100. package/dist/utils/__tests__/ticketListUtils.test.d.ts +2 -0
  101. package/dist/utils/__tests__/ticketListUtils.test.d.ts.map +1 -0
  102. package/dist/utils/__tests__/ticketListUtils.test.js +41 -0
  103. package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -0
  104. package/dist/utils/__tests__/typeUtils.test.d.ts +2 -0
  105. package/dist/utils/__tests__/typeUtils.test.d.ts.map +1 -0
  106. package/dist/utils/__tests__/typeUtils.test.js +11 -0
  107. package/dist/utils/__tests__/typeUtils.test.js.map +1 -0
  108. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts +5 -0
  109. package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts.map +1 -0
  110. package/dist/utils/__tests__/userSubscriptionUtils.test.js +66 -0
  111. package/dist/utils/__tests__/userSubscriptionUtils.test.js.map +1 -0
  112. package/dist/utils/__tests__/userUtils.test.d.ts +2 -0
  113. package/dist/utils/__tests__/userUtils.test.d.ts.map +1 -0
  114. package/dist/utils/__tests__/userUtils.test.js +34 -0
  115. package/dist/utils/__tests__/userUtils.test.js.map +1 -0
  116. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts +2 -0
  117. package/dist/utils/__tests__/venueTypeFormatter.test.d.ts.map +1 -0
  118. package/dist/utils/__tests__/venueTypeFormatter.test.js +25 -0
  119. package/dist/utils/__tests__/venueTypeFormatter.test.js.map +1 -0
  120. package/dist/utils/service/__tests__/regexUtils.test.d.ts +2 -0
  121. package/dist/utils/service/__tests__/regexUtils.test.d.ts.map +1 -0
  122. package/dist/utils/service/__tests__/regexUtils.test.js +51 -0
  123. package/dist/utils/service/__tests__/regexUtils.test.js.map +1 -0
  124. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts +2 -0
  125. package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts.map +1 -0
  126. package/dist/utils/service/__tests__/serviceDBUtils.test.js +17 -0
  127. package/dist/utils/service/__tests__/serviceDBUtils.test.js.map +1 -0
  128. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts +2 -0
  129. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts.map +1 -0
  130. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js +60 -0
  131. package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js.map +1 -0
  132. package/dist/utils/service/__tests__/serviceUtils.test.d.ts +2 -0
  133. package/dist/utils/service/__tests__/serviceUtils.test.d.ts.map +1 -0
  134. package/dist/utils/service/__tests__/serviceUtils.test.js +135 -0
  135. package/dist/utils/service/__tests__/serviceUtils.test.js.map +1 -0
  136. package/dist/utils/service/serviceFeaturedSortUtils.d.ts.map +1 -1
  137. package/dist/utils/service/serviceFeaturedSortUtils.js +5 -1
  138. package/dist/utils/service/serviceFeaturedSortUtils.js.map +1 -1
  139. package/dist/utils/ticketListUtils.d.ts.map +1 -1
  140. package/dist/utils/ticketListUtils.js +11 -7
  141. package/dist/utils/ticketListUtils.js.map +1 -1
  142. package/package.json +1 -1
  143. package/prisma/schema.prisma +47 -0
  144. package/src/__tests__/bashSectionTitleForBashType.test.ts +48 -1
  145. package/src/__tests__/definitionsHelpers.test.ts +136 -0
  146. package/src/__tests__/extendedSchemas.test.ts +50 -0
  147. package/src/__tests__/membershipDefinitions.test.ts +144 -0
  148. package/src/__tests__/ticketAdmission.test.ts +73 -0
  149. package/src/__tests__/utmAttribution.test.ts +118 -0
  150. package/src/definitions.ts +86 -23
  151. package/src/extendedSchemas.ts +35 -1
  152. package/src/index.ts +1 -0
  153. package/src/sms/smsTemplates.ts +20 -0
  154. package/src/utils/__tests__/addressUtils.test.ts +176 -0
  155. package/src/utils/__tests__/apiUtils.test.ts +219 -0
  156. package/src/utils/__tests__/arrayUtils.test.ts +26 -0
  157. package/src/utils/__tests__/blogUtils.test.ts +145 -0
  158. package/src/utils/__tests__/dateTimeUtils.test.ts +233 -0
  159. package/src/utils/__tests__/discountEngine/discountCalculator.test.ts +410 -0
  160. package/src/utils/__tests__/generalDateTimeUtils.test.ts +34 -0
  161. package/src/utils/__tests__/luxonUtils.test.ts +171 -0
  162. package/src/utils/__tests__/mathUtils.test.ts +30 -0
  163. package/src/utils/__tests__/objUtils.test.ts +48 -0
  164. package/src/utils/__tests__/paymentUtils.test.ts +59 -0
  165. package/src/utils/__tests__/qrCodeUtils.test.ts +28 -1
  166. package/src/utils/__tests__/reviewUtils.test.ts +65 -0
  167. package/src/utils/__tests__/slugUtils.test.ts +143 -0
  168. package/src/utils/__tests__/sortUtils.test.ts +33 -0
  169. package/src/utils/__tests__/stringUtils.test.ts +37 -0
  170. package/src/utils/__tests__/ticketListUtils.test.ts +47 -0
  171. package/src/utils/__tests__/typeUtils.test.ts +12 -0
  172. package/src/utils/__tests__/userSubscriptionUtils.test.ts +87 -0
  173. package/src/utils/__tests__/userUtils.test.ts +41 -0
  174. package/src/utils/__tests__/venueTypeFormatter.test.ts +31 -0
  175. package/src/utils/service/__tests__/regexUtils.test.ts +66 -0
  176. package/src/utils/service/__tests__/serviceDBUtils.test.ts +19 -0
  177. package/src/utils/service/__tests__/serviceFeaturedSortUtils.test.ts +67 -0
  178. package/src/utils/service/__tests__/serviceUtils.test.ts +190 -0
  179. package/src/utils/service/serviceFeaturedSortUtils.ts +5 -1
  180. package/src/utils/ticketListUtils.ts +13 -6
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { asyncFilter } from "../arrayUtils.js";
3
+
4
+ describe("asyncFilter", () => {
5
+ test("keeps items where async predicate resolves true", async () => {
6
+ const nums = [1, 2, 3, 4];
7
+ const result = await asyncFilter(nums, async (n) => n % 2 === 0);
8
+ expect(result).toEqual([2, 4]);
9
+ });
10
+
11
+ test("returns empty array when no items match", async () => {
12
+ const result = await asyncFilter([1, 3, 5], async () => false);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ test("returns empty array for empty input", async () => {
17
+ const result = await asyncFilter([], async () => true);
18
+ expect(result).toEqual([]);
19
+ });
20
+
21
+ test("preserves order of matching items", async () => {
22
+ const items = ["a", "b", "c", "d"];
23
+ const result = await asyncFilter(items, async (v) => v !== "b");
24
+ expect(result).toEqual(["a", "c", "d"]);
25
+ });
26
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { DEFAULT_BLOG_IMAGE } from "../../definitions.js";
3
+ import {
4
+ calculateReadingTime,
5
+ formatPublishDate,
6
+ generateBlogDetailUrl,
7
+ generateExcerpt,
8
+ generateMetaDescription,
9
+ generateMetaTitle,
10
+ getBlogDetailUrl,
11
+ getBlogImageUrl,
12
+ highlightSearchTerms,
13
+ parseBlogUrlParams,
14
+ processTags,
15
+ validateBlogContent,
16
+ } from "../blogUtils.js";
17
+
18
+ describe("blogUtils", () => {
19
+ describe("URL helpers", () => {
20
+ test("generateBlogDetailUrl with slug", () => {
21
+ expect(generateBlogDetailUrl("abc123", "my-post")).toBe("/blog/abc123-my-post");
22
+ });
23
+
24
+ test("generateBlogDetailUrl without slug", () => {
25
+ expect(generateBlogDetailUrl("abc123")).toBe("/blog/abc123");
26
+ });
27
+
28
+ test("getBlogDetailUrl prefers explicit slug", () => {
29
+ expect(getBlogDetailUrl("id1", "saved-slug", "Ignored Title")).toBe(
30
+ "/blog/id1-saved-slug"
31
+ );
32
+ });
33
+
34
+ test("getBlogDetailUrl generates slug from title when slug missing", () => {
35
+ expect(getBlogDetailUrl("id1", undefined, "Hello World Party")).toBe(
36
+ "/blog/id1-hello-world-party"
37
+ );
38
+ });
39
+
40
+ test("getBlogDetailUrl falls back to id-only URL", () => {
41
+ expect(getBlogDetailUrl("id1")).toBe("/blog/id1");
42
+ });
43
+
44
+ test("parseBlogUrlParams handles id-only param", () => {
45
+ expect(parseBlogUrlParams("abc123")).toEqual({ id: "abc123" });
46
+ });
47
+
48
+ test("parseBlogUrlParams splits id-slug at first hyphen", () => {
49
+ expect(parseBlogUrlParams("abc123-my-cool-post")).toEqual({
50
+ id: "abc123",
51
+ slug: "my-cool-post",
52
+ });
53
+ });
54
+
55
+ test("parseBlogUrlParams returns null for empty input", () => {
56
+ expect(parseBlogUrlParams("")).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe("content processing", () => {
61
+ test("generateExcerpt strips HTML tags", () => {
62
+ expect(generateExcerpt("<p>Hello <strong>world</strong></p>")).toBe("Hello world");
63
+ });
64
+
65
+ test("generateExcerpt truncates at word boundary with ellipsis", () => {
66
+ const long = "word ".repeat(50);
67
+ const excerpt = generateExcerpt(long, 20);
68
+ expect(excerpt.endsWith("...")).toBe(true);
69
+ expect(excerpt.length).toBeLessThanOrEqual(23);
70
+ });
71
+
72
+ test("calculateReadingTime rounds up by word count", () => {
73
+ const words = Array.from({ length: 401 }, (_, i) => `w${i}`).join(" ");
74
+ expect(calculateReadingTime(words)).toBe(3);
75
+ });
76
+
77
+ test("getBlogImageUrl uses default when missing", () => {
78
+ expect(getBlogImageUrl(null)).toBe(DEFAULT_BLOG_IMAGE);
79
+ expect(getBlogImageUrl("https://cdn.example/img.jpg")).toBe(
80
+ "https://cdn.example/img.jpg"
81
+ );
82
+ });
83
+ });
84
+
85
+ describe("SEO helpers", () => {
86
+ test("generateMetaTitle appends site name", () => {
87
+ expect(generateMetaTitle("My Post")).toBe("My Post | Bash");
88
+ expect(generateMetaTitle("My Post", "Custom")).toBe("My Post | Custom");
89
+ });
90
+
91
+ test("generateMetaDescription uses excerpt or fallback", () => {
92
+ const content = "x".repeat(200);
93
+ expect(generateMetaDescription(content, "Title")).toContain("...");
94
+ expect(generateMetaDescription("", "Title")).toContain("Title");
95
+ });
96
+ });
97
+
98
+ describe("validateBlogContent", () => {
99
+ test("accepts valid title and content", () => {
100
+ expect(
101
+ validateBlogContent("Good title", "x".repeat(100))
102
+ ).toEqual({ isValid: true, errors: [] });
103
+ });
104
+
105
+ test("collects validation errors", () => {
106
+ const result = validateBlogContent("", "short");
107
+ expect(result.isValid).toBe(false);
108
+ expect(result.errors).toContain("Title is required");
109
+ expect(result.errors).toContain("Content must be at least 100 characters");
110
+ });
111
+ });
112
+
113
+ describe("processTags", () => {
114
+ test("splits, trims, lowercases, and caps at 10 tags", () => {
115
+ const tags = processTags(" Jazz, ROCK , ,Blues, " + "tag,".repeat(15));
116
+ expect(tags[0]).toBe("jazz");
117
+ expect(tags[1]).toBe("rock");
118
+ expect(tags.length).toBeLessThanOrEqual(10);
119
+ });
120
+
121
+ test("returns empty array for blank input", () => {
122
+ expect(processTags("")).toEqual([]);
123
+ });
124
+ });
125
+
126
+ describe("highlightSearchTerms", () => {
127
+ test("wraps matches in mark tags case-insensitively", () => {
128
+ expect(highlightSearchTerms("Hello Bash world", "bash")).toBe(
129
+ "Hello <mark>Bash</mark> world"
130
+ );
131
+ });
132
+
133
+ test("returns text unchanged when search term empty", () => {
134
+ expect(highlightSearchTerms("Hello", "")).toBe("Hello");
135
+ });
136
+ });
137
+
138
+ describe("formatPublishDate", () => {
139
+ test("formats Date and ISO string consistently", () => {
140
+ const date = new Date("2024-06-15T12:00:00Z");
141
+ expect(formatPublishDate(date)).toMatch(/June/);
142
+ expect(formatPublishDate("2024-06-15T12:00:00Z")).toMatch(/2024/);
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,233 @@
1
+ /**
2
+ * dateTimeUtils — parsing, comparison, and normalization helpers.
3
+ * Pin UTC normalization and comparison semantics with fixed instants.
4
+ */
5
+ import { DateTime } from "luxon";
6
+ import { describe, expect, test, beforeEach, afterEach, jest } from "@jest/globals";
7
+ import {
8
+ getLocalTimezoneName,
9
+ ensureIsDateTime,
10
+ normalizeDate,
11
+ normalizeDates,
12
+ dateWithinDateRange,
13
+ findLatestDateTime,
14
+ isDateTimeLaterThanNow,
15
+ areDateTimesEqual,
16
+ compareDateTime,
17
+ parseTimeString,
18
+ timestampMinutesFromNow,
19
+ } from "../dateTimeUtils.js";
20
+
21
+ describe("getLocalTimezoneName", () => {
22
+ test("returns a non-empty timezone string", () => {
23
+ const tz = getLocalTimezoneName();
24
+ expect(typeof tz).toBe("string");
25
+ expect(tz.length).toBeGreaterThan(0);
26
+ });
27
+ });
28
+
29
+ describe("ensureIsDateTime", () => {
30
+ test("returns undefined for null/undefined", () => {
31
+ expect(ensureIsDateTime(null)).toBeUndefined();
32
+ expect(ensureIsDateTime(undefined)).toBeUndefined();
33
+ });
34
+
35
+ test("returns Date for Date input", () => {
36
+ const d = new Date("2026-03-07T12:00:00Z");
37
+ expect(ensureIsDateTime(d)).toBe(d);
38
+ });
39
+
40
+ test("parses ISO string to Date", () => {
41
+ const result = ensureIsDateTime("2026-03-07T12:00:00Z");
42
+ expect(result).toBeInstanceOf(Date);
43
+ expect(result?.toISOString()).toBe("2026-03-07T12:00:00.000Z");
44
+ });
45
+ });
46
+
47
+ describe("normalizeDate", () => {
48
+ test("returns undefined for null/undefined", () => {
49
+ expect(normalizeDate(null)).toBeUndefined();
50
+ expect(normalizeDate(undefined)).toBeUndefined();
51
+ });
52
+
53
+ test("returns undefined for invalid date", () => {
54
+ const spy = jest.spyOn(console, "error").mockImplementation(() => {});
55
+ expect(normalizeDate("not-a-date")).toBeUndefined();
56
+ expect(spy).toHaveBeenCalled();
57
+ spy.mockRestore();
58
+ });
59
+
60
+ test("normalizes to UTC start of minute", () => {
61
+ const input = new Date("2026-03-07T12:34:56.789Z");
62
+ const result = normalizeDate(input);
63
+ expect(result).toEqual(
64
+ DateTime.fromJSDate(input).toUTC().startOf("minute").toJSDate()
65
+ );
66
+ });
67
+
68
+ test("accepts ISO string input", () => {
69
+ const result = normalizeDate("2026-03-07T12:34:56.789Z");
70
+ expect(result?.getUTCSeconds()).toBe(0);
71
+ expect(result?.getUTCMilliseconds()).toBe(0);
72
+ });
73
+ });
74
+
75
+ describe("normalizeDates", () => {
76
+ test("filters out invalid dates", () => {
77
+ const spy = jest.spyOn(console, "error").mockImplementation(() => {});
78
+ const results = normalizeDates([
79
+ "2026-03-07T12:00:00Z",
80
+ null,
81
+ "bad",
82
+ "2026-03-08T12:00:00Z",
83
+ ]);
84
+ expect(results).toHaveLength(2);
85
+ spy.mockRestore();
86
+ });
87
+ });
88
+
89
+ describe("compareDateTime", () => {
90
+ test("returns 0 when both dates are null/undefined", () => {
91
+ expect(compareDateTime(null, null)).toBe(0);
92
+ expect(compareDateTime(undefined, undefined)).toBe(0);
93
+ });
94
+
95
+ test("null date1 is treated as earlier", () => {
96
+ const d = new Date("2026-03-07T12:00:00Z");
97
+ expect(compareDateTime(null, d)).toBe(-1);
98
+ });
99
+
100
+ test("null date2 is treated as earlier than date1", () => {
101
+ const d = new Date("2026-03-07T12:00:00Z");
102
+ expect(compareDateTime(d, null)).toBe(1);
103
+ });
104
+
105
+ test("compares Date objects", () => {
106
+ const earlier = new Date("2026-03-07T10:00:00Z");
107
+ const later = new Date("2026-03-07T12:00:00Z");
108
+ expect(compareDateTime(later, earlier)).toBe(1);
109
+ expect(compareDateTime(earlier, later)).toBe(-1);
110
+ expect(compareDateTime(earlier, earlier)).toBe(0);
111
+ });
112
+
113
+ test("compares ISO strings", () => {
114
+ expect(compareDateTime("2026-03-07T12:00:00Z", "2026-03-07T10:00:00Z")).toBe(1);
115
+ expect(compareDateTime("2026-03-07T10:00:00Z", "2026-03-07T12:00:00Z")).toBe(-1);
116
+ });
117
+ });
118
+
119
+ describe("areDateTimesEqual", () => {
120
+ test("returns true for equal instants", () => {
121
+ const d = new Date("2026-03-07T12:00:00Z");
122
+ expect(areDateTimesEqual(d, d)).toBe(true);
123
+ expect(areDateTimesEqual("2026-03-07T12:00:00Z", d)).toBe(true);
124
+ });
125
+
126
+ test("returns false for different instants", () => {
127
+ expect(
128
+ areDateTimesEqual("2026-03-07T12:00:00Z", "2026-03-07T10:00:00Z")
129
+ ).toBe(false);
130
+ });
131
+ });
132
+
133
+ describe("findLatestDateTime", () => {
134
+ test("returns the later date", () => {
135
+ const earlier = "2026-03-07T10:00:00Z";
136
+ const later = "2026-03-07T12:00:00Z";
137
+ expect(findLatestDateTime(earlier, later)?.toISOString()).toBe(
138
+ new Date(later).toISOString()
139
+ );
140
+ expect(findLatestDateTime(later, earlier)?.toISOString()).toBe(
141
+ new Date(later).toISOString()
142
+ );
143
+ });
144
+ });
145
+
146
+ describe("isDateTimeLaterThanNow", () => {
147
+ beforeEach(() => {
148
+ jest.useFakeTimers();
149
+ jest.setSystemTime(new Date("2026-03-07T12:00:00Z"));
150
+ });
151
+
152
+ afterEach(() => {
153
+ jest.useRealTimers();
154
+ });
155
+
156
+ test("returns true for future dates", () => {
157
+ expect(isDateTimeLaterThanNow("2026-03-07T13:00:00Z")).toBe(true);
158
+ });
159
+
160
+ test("returns false for past dates", () => {
161
+ expect(isDateTimeLaterThanNow("2026-03-07T11:00:00Z")).toBe(false);
162
+ });
163
+ });
164
+
165
+ describe("dateWithinDateRange", () => {
166
+ test("returns true when date is within inclusive start, exclusive end", () => {
167
+ const inRange = dateWithinDateRange("2026-03-07T15:00:00Z", {
168
+ startDate: new Date("2026-03-07T00:00:00Z"),
169
+ endDate: new Date("2026-03-08T00:00:00Z"),
170
+ });
171
+ expect(inRange).toBe(true);
172
+ });
173
+
174
+ test("returns false when date is on or after end day", () => {
175
+ const onEndDay = dateWithinDateRange("2026-03-08T12:00:00Z", {
176
+ startDate: new Date("2026-03-07T00:00:00Z"),
177
+ endDate: new Date("2026-03-08T00:00:00Z"),
178
+ });
179
+ expect(onEndDay).toBe(false);
180
+ });
181
+
182
+ test("defaults to today when range is undefined", () => {
183
+ jest.useFakeTimers();
184
+ jest.setSystemTime(new Date("2026-03-07T15:00:00Z"));
185
+ expect(dateWithinDateRange("2026-03-07T16:00:00Z", undefined)).toBe(true);
186
+ expect(dateWithinDateRange("2026-03-09T12:00:00Z", undefined)).toBe(false);
187
+ jest.useRealTimers();
188
+ });
189
+ });
190
+
191
+ describe("parseTimeString", () => {
192
+ test("parses 10:30 AM", () => {
193
+ expect(parseTimeString("10:30 AM")).toEqual({ hours: 10, minutes: 30, ampm: "AM" });
194
+ });
195
+
196
+ test("parses 10:30 PM as 24h hours", () => {
197
+ expect(parseTimeString("10:30 PM")).toEqual({ hours: 22, minutes: 30, ampm: "PM" });
198
+ });
199
+
200
+ test("parses 12:00 PM", () => {
201
+ expect(parseTimeString("12:00 PM")).toEqual({ hours: 12, minutes: 0, ampm: "PM" });
202
+ });
203
+
204
+ test("parses 1:05 AM", () => {
205
+ expect(parseTimeString("1:05 AM")).toEqual({ hours: 1, minutes: 5, ampm: "AM" });
206
+ });
207
+
208
+ test("returns null for invalid formats", () => {
209
+ expect(parseTimeString("")).toBeNull();
210
+ expect(parseTimeString("25:00 AM")).toBeNull();
211
+ expect(parseTimeString("10:60 AM")).toBeNull();
212
+ expect(parseTimeString("10:30")).toBeNull();
213
+ expect(parseTimeString("noon")).toBeNull();
214
+ });
215
+ });
216
+
217
+ describe("timestampMinutesFromNow", () => {
218
+ beforeEach(() => {
219
+ jest.useFakeTimers();
220
+ jest.setSystemTime(new Date("2026-03-07T12:00:00Z"));
221
+ });
222
+
223
+ afterEach(() => {
224
+ jest.useRealTimers();
225
+ });
226
+
227
+ test("returns unix seconds floored for N minutes ahead", () => {
228
+ const expected = Math.floor(
229
+ DateTime.fromJSDate(new Date("2026-03-07T12:00:00Z")).plus({ minutes: 30 }).toSeconds()
230
+ );
231
+ expect(timestampMinutesFromNow(30)).toBe(expected);
232
+ });
233
+ });