@porulle/plugin-appointments 0.1.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 (66) hide show
  1. package/README.md +60 -0
  2. package/dist/analytics-models.d.ts +4 -0
  3. package/dist/analytics-models.d.ts.map +1 -0
  4. package/dist/analytics-models.js +27 -0
  5. package/dist/hooks.d.ts +13 -0
  6. package/dist/hooks.d.ts.map +1 -0
  7. package/dist/hooks.js +12 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +60 -0
  11. package/dist/routes/availability.d.ts +8 -0
  12. package/dist/routes/availability.d.ts.map +1 -0
  13. package/dist/routes/availability.js +118 -0
  14. package/dist/routes/bookings.d.ts +8 -0
  15. package/dist/routes/bookings.d.ts.map +1 -0
  16. package/dist/routes/bookings.js +122 -0
  17. package/dist/routes/my-bookings.d.ts +6 -0
  18. package/dist/routes/my-bookings.d.ts.map +1 -0
  19. package/dist/routes/my-bookings.js +22 -0
  20. package/dist/routes/providers.d.ts +6 -0
  21. package/dist/routes/providers.d.ts.map +1 -0
  22. package/dist/routes/providers.js +84 -0
  23. package/dist/routes/services.d.ts +6 -0
  24. package/dist/routes/services.d.ts.map +1 -0
  25. package/dist/routes/services.js +68 -0
  26. package/dist/routes/util.d.ts +4 -0
  27. package/dist/routes/util.d.ts.map +1 -0
  28. package/dist/routes/util.js +12 -0
  29. package/dist/schema.d.ts +1476 -0
  30. package/dist/schema.d.ts.map +1 -0
  31. package/dist/schema.js +128 -0
  32. package/dist/services/booking-service.d.ts +156 -0
  33. package/dist/services/booking-service.d.ts.map +1 -0
  34. package/dist/services/booking-service.js +249 -0
  35. package/dist/services/provider-service.d.ts +328 -0
  36. package/dist/services/provider-service.d.ts.map +1 -0
  37. package/dist/services/provider-service.js +152 -0
  38. package/dist/services/slot-generation.d.ts +15 -0
  39. package/dist/services/slot-generation.d.ts.map +1 -0
  40. package/dist/services/slot-generation.js +121 -0
  41. package/dist/services/slot-service.d.ts +11 -0
  42. package/dist/services/slot-service.d.ts.map +1 -0
  43. package/dist/services/slot-service.js +91 -0
  44. package/dist/tasks.d.ts +43 -0
  45. package/dist/tasks.d.ts.map +1 -0
  46. package/dist/tasks.js +107 -0
  47. package/dist/types.d.ts +42 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +7 -0
  50. package/package.json +60 -0
  51. package/src/analytics-models.ts +30 -0
  52. package/src/hooks.ts +15 -0
  53. package/src/index.ts +74 -0
  54. package/src/routes/availability.ts +153 -0
  55. package/src/routes/bookings.ts +141 -0
  56. package/src/routes/my-bookings.ts +28 -0
  57. package/src/routes/providers.ts +97 -0
  58. package/src/routes/services.ts +78 -0
  59. package/src/routes/util.ts +11 -0
  60. package/src/schema.ts +144 -0
  61. package/src/services/booking-service.ts +340 -0
  62. package/src/services/provider-service.ts +223 -0
  63. package/src/services/slot-generation.ts +156 -0
  64. package/src/services/slot-service.ts +128 -0
  65. package/src/tasks.ts +141 -0
  66. package/src/types.ts +61 -0
@@ -0,0 +1,328 @@
1
+ import type { Db } from "../types.js";
2
+ export declare class ProviderService {
3
+ private db;
4
+ constructor(db: Db);
5
+ createServiceType(orgId: string, data: {
6
+ name: string;
7
+ slug: string;
8
+ description?: string;
9
+ durationMinutes?: number;
10
+ priceCents?: number;
11
+ currency?: string;
12
+ bufferBeforeMinutes?: number;
13
+ bufferAfterMinutes?: number;
14
+ }): Promise<{
15
+ id: string;
16
+ createdAt: Date;
17
+ name: string;
18
+ organizationId: string;
19
+ slug: string;
20
+ description: string | null;
21
+ durationMinutes: number;
22
+ priceCents: number;
23
+ currency: string;
24
+ mode: string;
25
+ bufferBeforeMinutes: number;
26
+ bufferAfterMinutes: number;
27
+ maxAdvanceDays: number;
28
+ minNoticeMins: number;
29
+ isActive: boolean;
30
+ metadata: Record<string, unknown> | null;
31
+ updatedAt: Date;
32
+ } | undefined>;
33
+ getServiceType(id: string): Promise<{
34
+ id: string;
35
+ organizationId: string;
36
+ name: string;
37
+ slug: string;
38
+ description: string | null;
39
+ durationMinutes: number;
40
+ priceCents: number;
41
+ currency: string;
42
+ mode: string;
43
+ bufferBeforeMinutes: number;
44
+ bufferAfterMinutes: number;
45
+ maxAdvanceDays: number;
46
+ minNoticeMins: number;
47
+ isActive: boolean;
48
+ metadata: Record<string, unknown> | null;
49
+ createdAt: Date;
50
+ updatedAt: Date;
51
+ } | null>;
52
+ getServiceTypeBySlug(slug: string): Promise<{
53
+ id: string;
54
+ organizationId: string;
55
+ name: string;
56
+ slug: string;
57
+ description: string | null;
58
+ durationMinutes: number;
59
+ priceCents: number;
60
+ currency: string;
61
+ mode: string;
62
+ bufferBeforeMinutes: number;
63
+ bufferAfterMinutes: number;
64
+ maxAdvanceDays: number;
65
+ minNoticeMins: number;
66
+ isActive: boolean;
67
+ metadata: Record<string, unknown> | null;
68
+ createdAt: Date;
69
+ updatedAt: Date;
70
+ } | null>;
71
+ listServiceTypes(): Promise<{
72
+ id: string;
73
+ organizationId: string;
74
+ name: string;
75
+ slug: string;
76
+ description: string | null;
77
+ durationMinutes: number;
78
+ priceCents: number;
79
+ currency: string;
80
+ mode: string;
81
+ bufferBeforeMinutes: number;
82
+ bufferAfterMinutes: number;
83
+ maxAdvanceDays: number;
84
+ minNoticeMins: number;
85
+ isActive: boolean;
86
+ metadata: Record<string, unknown> | null;
87
+ createdAt: Date;
88
+ updatedAt: Date;
89
+ }[]>;
90
+ updateServiceType(id: string, data: Partial<{
91
+ name: string;
92
+ description: string;
93
+ durationMinutes: number;
94
+ priceCents: number;
95
+ currency: string;
96
+ bufferBeforeMinutes: number;
97
+ bufferAfterMinutes: number;
98
+ isActive: boolean;
99
+ }>): Promise<{
100
+ id: string;
101
+ organizationId: string;
102
+ name: string;
103
+ slug: string;
104
+ description: string | null;
105
+ durationMinutes: number;
106
+ priceCents: number;
107
+ currency: string;
108
+ mode: string;
109
+ bufferBeforeMinutes: number;
110
+ bufferAfterMinutes: number;
111
+ maxAdvanceDays: number;
112
+ minNoticeMins: number;
113
+ isActive: boolean;
114
+ metadata: Record<string, unknown> | null;
115
+ createdAt: Date;
116
+ updatedAt: Date;
117
+ } | null>;
118
+ deleteServiceType(id: string): Promise<{
119
+ id: string;
120
+ createdAt: Date;
121
+ name: string;
122
+ organizationId: string;
123
+ slug: string;
124
+ description: string | null;
125
+ durationMinutes: number;
126
+ priceCents: number;
127
+ currency: string;
128
+ mode: string;
129
+ bufferBeforeMinutes: number;
130
+ bufferAfterMinutes: number;
131
+ maxAdvanceDays: number;
132
+ minNoticeMins: number;
133
+ isActive: boolean;
134
+ metadata: Record<string, unknown> | null;
135
+ updatedAt: Date;
136
+ } | null>;
137
+ createProvider(orgId: string, data: {
138
+ name: string;
139
+ email?: string;
140
+ phone?: string;
141
+ timezone?: string;
142
+ metadata?: Record<string, unknown>;
143
+ }): Promise<{
144
+ id: string;
145
+ createdAt: Date;
146
+ name: string;
147
+ organizationId: string;
148
+ isActive: boolean;
149
+ metadata: Record<string, unknown> | null;
150
+ updatedAt: Date;
151
+ userId: string | null;
152
+ bio: string | null;
153
+ email: string | null;
154
+ phone: string | null;
155
+ timezone: string;
156
+ } | undefined>;
157
+ getProvider(id: string): Promise<{
158
+ id: string;
159
+ organizationId: string;
160
+ userId: string | null;
161
+ name: string;
162
+ bio: string | null;
163
+ email: string | null;
164
+ phone: string | null;
165
+ timezone: string;
166
+ isActive: boolean;
167
+ metadata: Record<string, unknown> | null;
168
+ createdAt: Date;
169
+ updatedAt: Date;
170
+ } | null>;
171
+ listProviders(): Promise<{
172
+ id: string;
173
+ organizationId: string;
174
+ userId: string | null;
175
+ name: string;
176
+ bio: string | null;
177
+ email: string | null;
178
+ phone: string | null;
179
+ timezone: string;
180
+ isActive: boolean;
181
+ metadata: Record<string, unknown> | null;
182
+ createdAt: Date;
183
+ updatedAt: Date;
184
+ }[]>;
185
+ updateProvider(id: string, data: Partial<{
186
+ name: string;
187
+ email: string;
188
+ phone: string;
189
+ timezone: string;
190
+ isActive: boolean;
191
+ metadata: Record<string, unknown>;
192
+ }>): Promise<{
193
+ id: string;
194
+ organizationId: string;
195
+ userId: string | null;
196
+ name: string;
197
+ bio: string | null;
198
+ email: string | null;
199
+ phone: string | null;
200
+ timezone: string;
201
+ isActive: boolean;
202
+ metadata: Record<string, unknown> | null;
203
+ createdAt: Date;
204
+ updatedAt: Date;
205
+ } | null>;
206
+ deleteProvider(id: string): Promise<{
207
+ id: string;
208
+ createdAt: Date;
209
+ name: string;
210
+ organizationId: string;
211
+ isActive: boolean;
212
+ metadata: Record<string, unknown> | null;
213
+ updatedAt: Date;
214
+ userId: string | null;
215
+ bio: string | null;
216
+ email: string | null;
217
+ phone: string | null;
218
+ timezone: string;
219
+ } | null>;
220
+ linkServiceType(providerId: string, serviceTypeId: string, overrides?: {
221
+ customDurationMinutes?: number;
222
+ customPriceCents?: number;
223
+ }): Promise<{
224
+ id: string;
225
+ providerId: string;
226
+ serviceTypeId: string;
227
+ createdAt: Date;
228
+ customDurationMinutes: number | null;
229
+ customPriceCents: number | null;
230
+ } | undefined>;
231
+ getProviderServices(providerId: string): Promise<{
232
+ id: string;
233
+ providerId: string;
234
+ serviceTypeId: string;
235
+ customDurationMinutes: number | null;
236
+ customPriceCents: number | null;
237
+ createdAt: Date;
238
+ }[]>;
239
+ setWeeklyAvailability(providerId: string, schedules: Array<{
240
+ dayOfWeek: number;
241
+ startTime: string;
242
+ endTime: string;
243
+ }>): Promise<{
244
+ id: string;
245
+ providerId: string;
246
+ startTime: string;
247
+ createdAt: Date;
248
+ dayOfWeek: number;
249
+ endTime: string;
250
+ }[]>;
251
+ getWeeklyAvailability(providerId: string): Promise<{
252
+ id: string;
253
+ providerId: string;
254
+ dayOfWeek: number;
255
+ startTime: string;
256
+ endTime: string;
257
+ createdAt: Date;
258
+ }[]>;
259
+ addBreak(providerId: string, data: {
260
+ dayOfWeek?: number | undefined;
261
+ startTime: string;
262
+ endTime: string;
263
+ label?: string | undefined;
264
+ }): Promise<{
265
+ id: string;
266
+ providerId: string;
267
+ startTime: string;
268
+ createdAt: Date;
269
+ dayOfWeek: number | null;
270
+ endTime: string;
271
+ label: string | null;
272
+ } | undefined>;
273
+ getBreaks(providerId: string): Promise<{
274
+ id: string;
275
+ providerId: string;
276
+ dayOfWeek: number | null;
277
+ startTime: string;
278
+ endTime: string;
279
+ label: string | null;
280
+ createdAt: Date;
281
+ }[]>;
282
+ deleteBreak(id: string): Promise<{
283
+ id: string;
284
+ providerId: string;
285
+ startTime: string;
286
+ createdAt: Date;
287
+ dayOfWeek: number | null;
288
+ endTime: string;
289
+ label: string | null;
290
+ } | null>;
291
+ addOverride(providerId: string, data: {
292
+ date: string;
293
+ isAvailable: boolean;
294
+ startTime?: string | undefined;
295
+ endTime?: string | undefined;
296
+ reason?: string | undefined;
297
+ }): Promise<{
298
+ id: string;
299
+ providerId: string;
300
+ startTime: string | null;
301
+ createdAt: Date;
302
+ date: string;
303
+ endTime: string | null;
304
+ isAvailable: boolean;
305
+ reason: string | null;
306
+ } | undefined>;
307
+ getOverrides(providerId: string): Promise<{
308
+ id: string;
309
+ providerId: string;
310
+ date: string;
311
+ isAvailable: boolean;
312
+ startTime: string | null;
313
+ endTime: string | null;
314
+ reason: string | null;
315
+ createdAt: Date;
316
+ }[]>;
317
+ deleteOverride(id: string): Promise<{
318
+ id: string;
319
+ providerId: string;
320
+ startTime: string | null;
321
+ createdAt: Date;
322
+ date: string;
323
+ endTime: string | null;
324
+ isAvailable: boolean;
325
+ reason: string | null;
326
+ } | null>;
327
+ }
328
+ //# sourceMappingURL=provider-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-service.d.ts","sourceRoot":"","sources":["../../src/services/provider-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,152 @@
1
+ import { eq, desc } from "@porulle/core/drizzle";
2
+ import { serviceTypes, providers, providerServices, weeklyAvailability, availabilityOverrides, breaks, } from "../schema.js";
3
+ export class ProviderService {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ // ─── Service Types ──────────────────────────────────────────────────────────
9
+ async createServiceType(orgId, data) {
10
+ const [row] = await this.db.insert(serviceTypes).values({
11
+ organizationId: orgId,
12
+ name: data.name,
13
+ slug: data.slug,
14
+ description: data.description,
15
+ durationMinutes: data.durationMinutes ?? 30,
16
+ priceCents: data.priceCents ?? 0,
17
+ currency: data.currency ?? "USD",
18
+ bufferBeforeMinutes: data.bufferBeforeMinutes ?? 0,
19
+ bufferAfterMinutes: data.bufferAfterMinutes ?? 0,
20
+ }).returning();
21
+ return row;
22
+ }
23
+ async getServiceType(id) {
24
+ const [row] = await this.db.select().from(serviceTypes).where(eq(serviceTypes.id, id));
25
+ return row ?? null;
26
+ }
27
+ async getServiceTypeBySlug(slug) {
28
+ const [row] = await this.db.select().from(serviceTypes).where(eq(serviceTypes.slug, slug));
29
+ return row ?? null;
30
+ }
31
+ async listServiceTypes() {
32
+ return this.db.select().from(serviceTypes).where(eq(serviceTypes.isActive, true)).orderBy(desc(serviceTypes.createdAt));
33
+ }
34
+ async updateServiceType(id, data) {
35
+ const [updated] = await this.db.update(serviceTypes)
36
+ .set({ ...data, updatedAt: new Date() })
37
+ .where(eq(serviceTypes.id, id))
38
+ .returning();
39
+ return updated ?? null;
40
+ }
41
+ async deleteServiceType(id) {
42
+ const [deleted] = await this.db.delete(serviceTypes).where(eq(serviceTypes.id, id)).returning();
43
+ return deleted ?? null;
44
+ }
45
+ // ─── Providers ──────────────────────────────────────────────────────────────
46
+ async createProvider(orgId, data) {
47
+ const [row] = await this.db.insert(providers).values({
48
+ organizationId: orgId,
49
+ name: data.name,
50
+ email: data.email,
51
+ phone: data.phone,
52
+ timezone: data.timezone ?? "UTC",
53
+ metadata: data.metadata ?? {},
54
+ }).returning();
55
+ return row;
56
+ }
57
+ async getProvider(id) {
58
+ const [row] = await this.db.select().from(providers).where(eq(providers.id, id));
59
+ return row ?? null;
60
+ }
61
+ async listProviders() {
62
+ return this.db.select().from(providers).where(eq(providers.isActive, true)).orderBy(desc(providers.createdAt));
63
+ }
64
+ async updateProvider(id, data) {
65
+ const [updated] = await this.db.update(providers)
66
+ .set({ ...data, updatedAt: new Date() })
67
+ .where(eq(providers.id, id))
68
+ .returning();
69
+ return updated ?? null;
70
+ }
71
+ async deleteProvider(id) {
72
+ const [deleted] = await this.db.delete(providers).where(eq(providers.id, id)).returning();
73
+ return deleted ?? null;
74
+ }
75
+ // ─── Provider–ServiceType Links ─────────────────────────────────────────────
76
+ async linkServiceType(providerId, serviceTypeId, overrides) {
77
+ const [row] = await this.db.insert(providerServices).values({
78
+ providerId,
79
+ serviceTypeId,
80
+ customDurationMinutes: overrides?.customDurationMinutes,
81
+ customPriceCents: overrides?.customPriceCents,
82
+ }).returning();
83
+ return row;
84
+ }
85
+ async getProviderServices(providerId) {
86
+ return this.db.select().from(providerServices)
87
+ .where(eq(providerServices.providerId, providerId));
88
+ }
89
+ // ─── Weekly Availability ────────────────────────────────────────────────────
90
+ async setWeeklyAvailability(providerId, schedules) {
91
+ // Delete existing weekly availability for this provider
92
+ await this.db.delete(weeklyAvailability)
93
+ .where(eq(weeklyAvailability.providerId, providerId));
94
+ if (schedules.length === 0)
95
+ return [];
96
+ const rows = await this.db.insert(weeklyAvailability)
97
+ .values(schedules.map((s) => ({
98
+ providerId,
99
+ dayOfWeek: s.dayOfWeek,
100
+ startTime: s.startTime,
101
+ endTime: s.endTime,
102
+ })))
103
+ .returning();
104
+ return rows;
105
+ }
106
+ async getWeeklyAvailability(providerId) {
107
+ return this.db.select().from(weeklyAvailability)
108
+ .where(eq(weeklyAvailability.providerId, providerId))
109
+ .orderBy(weeklyAvailability.dayOfWeek);
110
+ }
111
+ // ─── Breaks ─────────────────────────────────────────────────────────────────
112
+ async addBreak(providerId, data) {
113
+ const [row] = await this.db.insert(breaks).values({
114
+ providerId,
115
+ dayOfWeek: data.dayOfWeek,
116
+ startTime: data.startTime,
117
+ endTime: data.endTime,
118
+ label: data.label,
119
+ }).returning();
120
+ return row;
121
+ }
122
+ async getBreaks(providerId) {
123
+ return this.db.select().from(breaks)
124
+ .where(eq(breaks.providerId, providerId));
125
+ }
126
+ async deleteBreak(id) {
127
+ const [deleted] = await this.db.delete(breaks).where(eq(breaks.id, id)).returning();
128
+ return deleted ?? null;
129
+ }
130
+ // ─── Availability Overrides ─────────────────────────────────────────────────
131
+ async addOverride(providerId, data) {
132
+ const [row] = await this.db.insert(availabilityOverrides).values({
133
+ providerId,
134
+ date: data.date,
135
+ isAvailable: data.isAvailable,
136
+ startTime: data.startTime,
137
+ endTime: data.endTime,
138
+ reason: data.reason,
139
+ }).returning();
140
+ return row;
141
+ }
142
+ async getOverrides(providerId) {
143
+ return this.db.select().from(availabilityOverrides)
144
+ .where(eq(availabilityOverrides.providerId, providerId))
145
+ .orderBy(availabilityOverrides.date);
146
+ }
147
+ async deleteOverride(id) {
148
+ const [deleted] = await this.db.delete(availabilityOverrides)
149
+ .where(eq(availabilityOverrides.id, id)).returning();
150
+ return deleted ?? null;
151
+ }
152
+ }
@@ -0,0 +1,15 @@
1
+ import type { TimeSlot, SlotGenerationParams } from "../types.js";
2
+ /**
3
+ * Pure function: generates available time slots for a given date.
4
+ *
5
+ * No DB access — all inputs passed as parameters.
6
+ * Handles: schedule hours, breaks, buffer time, existing bookings,
7
+ * min notice, max advance, and timezone conversion.
8
+ */
9
+ export declare function generateSlots(params: SlotGenerationParams): TimeSlot[];
10
+ /**
11
+ * Parses a time string (HH:mm) on a given date in a specific timezone,
12
+ * returning a UTC Date object.
13
+ */
14
+ export declare function parseTimeInTimezone(date: Date, time: string, timezone: string): Date;
15
+ //# sourceMappingURL=slot-generation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-generation.d.ts","sourceRoot":"","sources":["../../src/services/slot-generation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAElE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,oBAAoB,GAAG,QAAQ,EAAE,CAyEtE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAmEpF"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pure function: generates available time slots for a given date.
3
+ *
4
+ * No DB access — all inputs passed as parameters.
5
+ * Handles: schedule hours, breaks, buffer time, existing bookings,
6
+ * min notice, max advance, and timezone conversion.
7
+ */
8
+ export function generateSlots(params) {
9
+ const { date, schedule, durationMinutes, bufferBeforeMinutes = 0, bufferAfterMinutes = 0, breaks = [], existingBookings = [], minNoticeMinutes = 0, maxAdvanceDays = 0, timezone, now = new Date(), } = params;
10
+ // Day off — no slots
11
+ if (!schedule)
12
+ return [];
13
+ // Parse schedule times into absolute Date objects for the given date in the provider's timezone
14
+ const dayStart = parseTimeInTimezone(date, schedule.startTime, timezone);
15
+ const dayEnd = parseTimeInTimezone(date, schedule.endTime, timezone);
16
+ if (dayEnd <= dayStart)
17
+ return [];
18
+ // Generate candidate slots
19
+ const totalSlotMinutes = bufferBeforeMinutes + durationMinutes + bufferAfterMinutes;
20
+ const candidates = [];
21
+ let cursor = dayStart.getTime();
22
+ while (cursor + totalSlotMinutes * 60_000 <= dayEnd.getTime()) {
23
+ const slotStart = new Date(cursor + bufferBeforeMinutes * 60_000);
24
+ const slotEnd = new Date(slotStart.getTime() + durationMinutes * 60_000);
25
+ const blockEnd = new Date(slotEnd.getTime() + bufferAfterMinutes * 60_000);
26
+ candidates.push({ start: slotStart, end: slotEnd });
27
+ // Advance by duration + buffer after (so buffers don't overlap)
28
+ cursor = blockEnd.getTime();
29
+ }
30
+ // Filter out slots that overlap with breaks
31
+ const breakRanges = breaks.map((b) => ({
32
+ start: parseTimeInTimezone(date, b.startTime, timezone),
33
+ end: parseTimeInTimezone(date, b.endTime, timezone),
34
+ }));
35
+ let filtered = candidates.filter((slot) => {
36
+ // The slot's full block (including buffers) must not overlap any break
37
+ const blockStart = new Date(slot.start.getTime() - bufferBeforeMinutes * 60_000);
38
+ const blockEnd = new Date(slot.end.getTime() + bufferAfterMinutes * 60_000);
39
+ return !breakRanges.some((br) => blockStart < br.end && blockEnd > br.start);
40
+ });
41
+ // Filter out slots that overlap with existing bookings
42
+ filtered = filtered.filter((slot) => {
43
+ return !existingBookings.some((booking) => slot.start < booking.endTime && slot.end > booking.startTime);
44
+ });
45
+ // Filter out slots that violate min notice
46
+ if (minNoticeMinutes > 0) {
47
+ const earliestStart = new Date(now.getTime() + minNoticeMinutes * 60_000);
48
+ filtered = filtered.filter((slot) => slot.start >= earliestStart);
49
+ }
50
+ // Filter out slots that violate max advance
51
+ if (maxAdvanceDays > 0) {
52
+ const latestStart = new Date(now.getTime() + maxAdvanceDays * 24 * 60 * 60_000);
53
+ filtered = filtered.filter((slot) => slot.start <= latestStart);
54
+ }
55
+ return filtered;
56
+ }
57
+ /**
58
+ * Parses a time string (HH:mm) on a given date in a specific timezone,
59
+ * returning a UTC Date object.
60
+ */
61
+ export function parseTimeInTimezone(date, time, timezone) {
62
+ const [hours, minutes] = time.split(":").map(Number);
63
+ // Build a date string in the target timezone
64
+ const year = date.getFullYear();
65
+ const month = String(date.getMonth() + 1).padStart(2, "0");
66
+ const day = String(date.getDate()).padStart(2, "0");
67
+ const h = String(hours).padStart(2, "0");
68
+ const m = String(minutes).padStart(2, "0");
69
+ // Create date in the provider's timezone using Intl
70
+ const dateStr = `${year}-${month}-${day}T${h}:${m}:00`;
71
+ // Use a formatter to figure out the UTC offset for this timezone at this date/time
72
+ const utcDate = new Date(dateStr + "Z");
73
+ const formatter = new Intl.DateTimeFormat("en-US", {
74
+ timeZone: timezone,
75
+ year: "numeric",
76
+ month: "2-digit",
77
+ day: "2-digit",
78
+ hour: "2-digit",
79
+ minute: "2-digit",
80
+ second: "2-digit",
81
+ hour12: false,
82
+ });
83
+ // Find the offset by comparing what UTC time produces the desired local time
84
+ // Binary search-like approach: try the UTC date, see what local time it maps to,
85
+ // and adjust.
86
+ const parts = formatter.formatToParts(utcDate);
87
+ const localHour = Number(parts.find((p) => p.type === "hour")?.value ?? 0);
88
+ const localMinute = Number(parts.find((p) => p.type === "minute")?.value ?? 0);
89
+ const localDay = Number(parts.find((p) => p.type === "day")?.value ?? 0);
90
+ const localMonth = Number(parts.find((p) => p.type === "month")?.value ?? 0);
91
+ // Calculate the difference between the local time at UTC and our target
92
+ const targetMinutes = hours * 60 + minutes;
93
+ let localMinutes = localHour * 60 + localMinute;
94
+ // Handle day/month boundary crossings
95
+ const targetDay = date.getDate();
96
+ if (localDay !== targetDay || localMonth !== date.getMonth() + 1) {
97
+ // Day changed — adjust by a full day if needed
98
+ if (localDay > targetDay || localMonth > date.getMonth() + 1) {
99
+ localMinutes += 24 * 60; // local is ahead
100
+ }
101
+ else {
102
+ localMinutes -= 24 * 60; // local is behind
103
+ }
104
+ }
105
+ const offsetMinutes = localMinutes - targetMinutes;
106
+ // The UTC time = target time + offset (since local = UTC - offset means UTC = local + offset)
107
+ // Actually: if local shows 14:30 when UTC is 09:00, offset = +5:30
108
+ // We want UTC time that corresponds to target local time
109
+ // UTC = targetTime - offset... no: local = UTC + offset => UTC = local - offset
110
+ // offsetMinutes = local - target. If we set UTC = utcDate - offsetMinutes * 60000
111
+ // that gives us: local_of_result = (utcDate - offsetMinutes*60000) + offset
112
+ // = utcDate + offset - offsetMinutes*60000
113
+ // = utcDate + offset - (local - target)*60000
114
+ // Hmm, let me think differently.
115
+ //
116
+ // We know: formatter(utcDate) = localTime
117
+ // We want: formatter(result) = targetTime
118
+ // So: result = utcDate - (localTime - targetTime) * 60000
119
+ // = utcDate - offsetMinutes * 60000
120
+ return new Date(utcDate.getTime() - offsetMinutes * 60_000);
121
+ }
@@ -0,0 +1,11 @@
1
+ import type { Db, TimeSlot } from "../types.js";
2
+ export declare class SlotService {
3
+ private db;
4
+ private defaults;
5
+ constructor(db: Db, defaults?: {
6
+ minNoticeMinutes: number;
7
+ maxAdvanceDays: number;
8
+ });
9
+ getAvailableSlots(providerId: string, serviceTypeId: string, date: Date, durationMinutes: number, bufferBeforeMinutes: number, bufferAfterMinutes: number, timezone: string, now?: Date): Promise<TimeSlot[]>;
10
+ }
11
+ //# sourceMappingURL=slot-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-service.d.ts","sourceRoot":"","sources":["../../src/services/slot-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAA6C,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE3F,qBAAa,WAAW;IAEpB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;gBADR,EAAE,EAAE,EAAE,EACN,QAAQ,GAAE;QAChB,gBAAgB,EAAE,MAAM,CAAC;QACzB,cAAc,EAAE,MAAM,CAAC;KACsB;IAG3C,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,MAAM,EACvB,mBAAmB,EAAE,MAAM,EAC3B,kBAAkB,EAAE,MAAM,EAC1B,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,IAAI,GACT,OAAO,CAAC,QAAQ,EAAE,CAAC;CAiGvB"}