@perdieminc/time-slots 0.0.1

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.
@@ -0,0 +1,223 @@
1
+ import { compareAsc, isBefore } from "date-fns";
2
+ import { findTimeZone, getZonedTime } from "timezone-support";
3
+
4
+ import { getNextAvailableDates } from "../schedule/available-dates";
5
+ import type {
6
+ BusinessHour,
7
+ BusinessHoursOverrideOutput,
8
+ FulfillmentPreference,
9
+ GetOpeningClosingTimeOnDateParams,
10
+ LocationLike,
11
+ } from "../types";
12
+ import { getLocationBusinessHoursForFulfillment } from "./business-hours";
13
+ import { isMidnightTransition, isTodayInTimeZone, setHmOnDate } from "./date";
14
+
15
+ // ── Private helpers ─────────────────────────────────────────────────────────
16
+
17
+ interface GetAvailableBusinessHoursParams {
18
+ businessHours?: BusinessHour[];
19
+ businessHoursOverrides?: BusinessHoursOverrideOutput[];
20
+ timeZone: string;
21
+ nextAvailableDate: Date;
22
+ }
23
+
24
+ function getAvailableBusinessHours({
25
+ businessHours = [],
26
+ businessHoursOverrides = [],
27
+ timeZone,
28
+ nextAvailableDate,
29
+ }: GetAvailableBusinessHoursParams): {
30
+ dayBusinessTimes: Array<{ startDate: Date; endDate: Date }>;
31
+ businessHoursOverride: BusinessHoursOverrideOutput | undefined;
32
+ } {
33
+ const zonedDate = getZonedTime(nextAvailableDate, findTimeZone(timeZone));
34
+
35
+ const dayBusinessHours = businessHours.filter(
36
+ (bh) =>
37
+ bh.day ===
38
+ getZonedTime(nextAvailableDate, findTimeZone(timeZone)).dayOfWeek,
39
+ );
40
+
41
+ const businessHoursOverride = businessHoursOverrides.find(
42
+ (override) =>
43
+ override.day === zonedDate.day && override.month === zonedDate.month,
44
+ );
45
+
46
+ const dayBusinessTimes = dayBusinessHours
47
+ .map((businessHour) => {
48
+ const effectiveHour = businessHoursOverride
49
+ ? {
50
+ day: businessHour.day,
51
+ startTime: businessHoursOverride.startTime ?? "00:00",
52
+ endTime: businessHoursOverride.endTime ?? "23:59",
53
+ }
54
+ : businessHour;
55
+
56
+ const startDate = setHmOnDate(
57
+ nextAvailableDate,
58
+ effectiveHour.startTime,
59
+ timeZone,
60
+ );
61
+
62
+ const endDate = setHmOnDate(
63
+ nextAvailableDate,
64
+ effectiveHour.endTime,
65
+ timeZone,
66
+ );
67
+
68
+ if (!isBefore(startDate, endDate)) {
69
+ return null;
70
+ }
71
+
72
+ return { startDate, endDate };
73
+ })
74
+ .filter((time): time is { startDate: Date; endDate: Date } => time !== null)
75
+ .sort((a, b) => compareAsc(a.startDate, b.startDate));
76
+
77
+ return { dayBusinessTimes, businessHoursOverride };
78
+ }
79
+
80
+ // ── Public API ──────────────────────────────────────────────────────────────
81
+
82
+ export function getOpeningClosingTimeOnDate({
83
+ date = new Date(),
84
+ businessHours = [],
85
+ businessHoursOverrides = [],
86
+ timeZone,
87
+ }: GetOpeningClosingTimeOnDateParams): {
88
+ openingTime: Date;
89
+ closingTime: Date;
90
+ } | null {
91
+ try {
92
+ const nextAvailableDates = getNextAvailableDates({
93
+ startDate: date,
94
+ businessHours,
95
+ businessHoursOverrides,
96
+ timeZone,
97
+ datesCount: 7,
98
+ });
99
+
100
+ if (!Array.isArray(nextAvailableDates) || !nextAvailableDates.length) {
101
+ return null;
102
+ }
103
+
104
+ for (
105
+ let nextDateIndex = 0;
106
+ nextDateIndex < nextAvailableDates.length;
107
+ ++nextDateIndex
108
+ ) {
109
+ const nextAvailableDate = nextAvailableDates[nextDateIndex];
110
+
111
+ const { dayBusinessTimes, businessHoursOverride } =
112
+ getAvailableBusinessHours({
113
+ businessHours,
114
+ businessHoursOverrides,
115
+ timeZone,
116
+ nextAvailableDate,
117
+ });
118
+
119
+ if (!Array.isArray(dayBusinessTimes) || dayBusinessTimes.length === 0) {
120
+ if (
121
+ businessHoursOverride?.startTime &&
122
+ businessHoursOverride?.endTime
123
+ ) {
124
+ const openingTime = setHmOnDate(
125
+ nextAvailableDate,
126
+ businessHoursOverride.startTime,
127
+ timeZone,
128
+ );
129
+ const closingTime = setHmOnDate(
130
+ nextAvailableDate,
131
+ businessHoursOverride.endTime,
132
+ timeZone,
133
+ );
134
+
135
+ if (isBefore(closingTime, date)) {
136
+ continue;
137
+ }
138
+
139
+ return { openingTime, closingTime };
140
+ }
141
+
142
+ continue;
143
+ }
144
+
145
+ const currentTime = date;
146
+ let currentSlot: { startDate: Date; endDate: Date } | null = null;
147
+
148
+ for (const slot of dayBusinessTimes) {
149
+ if (isBefore(currentTime, slot.endDate)) {
150
+ currentSlot = slot;
151
+ break;
152
+ }
153
+ }
154
+
155
+ if (!currentSlot) {
156
+ currentSlot = dayBusinessTimes[0];
157
+ }
158
+
159
+ if (isBefore(currentSlot.endDate, date)) {
160
+ continue;
161
+ }
162
+
163
+ if (
164
+ isTodayInTimeZone(nextAvailableDate, timeZone) &&
165
+ nextDateIndex + 1 < nextAvailableDates.length
166
+ ) {
167
+ const { dayBusinessTimes: nextDayTimes } = getAvailableBusinessHours({
168
+ businessHours,
169
+ businessHoursOverrides,
170
+ timeZone,
171
+ nextAvailableDate: nextAvailableDates[nextDateIndex + 1],
172
+ });
173
+ if (nextDayTimes.length) {
174
+ const firstNextDaySlot = nextDayTimes?.[0];
175
+ if (
176
+ firstNextDaySlot &&
177
+ isMidnightTransition(
178
+ currentSlot.endDate,
179
+ firstNextDaySlot.startDate,
180
+ timeZone,
181
+ )
182
+ ) {
183
+ currentSlot = {
184
+ ...currentSlot,
185
+ endDate: firstNextDaySlot.endDate,
186
+ };
187
+ }
188
+ }
189
+ }
190
+
191
+ return {
192
+ openingTime: currentSlot.startDate,
193
+ closingTime: currentSlot.endDate,
194
+ };
195
+ }
196
+
197
+ return null;
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
203
+ export function getOpeningClosingTime({
204
+ location,
205
+ fulfillmentPreference,
206
+ businessHoursOverrides,
207
+ }: {
208
+ location: LocationLike;
209
+ fulfillmentPreference: FulfillmentPreference;
210
+ businessHoursOverrides?: Record<string, BusinessHoursOverrideOutput[]>;
211
+ }): { openingTime: Date; closingTime: Date } | null {
212
+ const businessHours = getLocationBusinessHoursForFulfillment(
213
+ location,
214
+ fulfillmentPreference,
215
+ );
216
+
217
+ return getOpeningClosingTimeOnDate({
218
+ businessHours,
219
+ businessHoursOverrides:
220
+ businessHoursOverrides?.[location.location_id] ?? [],
221
+ timeZone: location.timezone,
222
+ });
223
+ }
@@ -0,0 +1,38 @@
1
+ export function parseTimeString(timeString: string | null | undefined): {
2
+ hours: number;
3
+ minutes: number;
4
+ } {
5
+ if (!timeString) {
6
+ return { hours: 0, minutes: 0 };
7
+ }
8
+
9
+ const [hours = 0, minutes = 0] = String(timeString).split(":");
10
+
11
+ return { hours: Number(hours), minutes: Number(minutes) };
12
+ }
13
+
14
+ export function isTimeInRange(
15
+ schedule: { start_time?: string; end_time?: string },
16
+ time: { hours: number; minutes: number },
17
+ ): boolean {
18
+ const startTime = parseTimeString(schedule?.start_time);
19
+ const endTime = parseTimeString(schedule?.end_time);
20
+
21
+ if (time.hours < startTime.hours || time.hours > endTime.hours) {
22
+ return false;
23
+ }
24
+
25
+ if (time.hours === startTime.hours) {
26
+ if (time.minutes < startTime.minutes) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ if (time.hours === endTime.hours) {
32
+ if (time.minutes > endTime.minutes) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ return true;
38
+ }