@thebookingkit/server 0.1.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.
Files changed (126) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-test.log +20 -0
  3. package/CHANGELOG.md +9 -0
  4. package/dist/__tests__/api.test.d.ts +2 -0
  5. package/dist/__tests__/api.test.d.ts.map +1 -0
  6. package/dist/__tests__/api.test.js +280 -0
  7. package/dist/__tests__/api.test.js.map +1 -0
  8. package/dist/__tests__/auth.test.d.ts +2 -0
  9. package/dist/__tests__/auth.test.d.ts.map +1 -0
  10. package/dist/__tests__/auth.test.js +78 -0
  11. package/dist/__tests__/auth.test.js.map +1 -0
  12. package/dist/__tests__/concurrent-booking.test.d.ts +2 -0
  13. package/dist/__tests__/concurrent-booking.test.d.ts.map +1 -0
  14. package/dist/__tests__/concurrent-booking.test.js +111 -0
  15. package/dist/__tests__/concurrent-booking.test.js.map +1 -0
  16. package/dist/__tests__/multi-tenancy.test.d.ts +2 -0
  17. package/dist/__tests__/multi-tenancy.test.d.ts.map +1 -0
  18. package/dist/__tests__/multi-tenancy.test.js +196 -0
  19. package/dist/__tests__/multi-tenancy.test.js.map +1 -0
  20. package/dist/__tests__/serialization-retry.test.d.ts +2 -0
  21. package/dist/__tests__/serialization-retry.test.d.ts.map +1 -0
  22. package/dist/__tests__/serialization-retry.test.js +53 -0
  23. package/dist/__tests__/serialization-retry.test.js.map +1 -0
  24. package/dist/__tests__/webhooks.test.d.ts +2 -0
  25. package/dist/__tests__/webhooks.test.d.ts.map +1 -0
  26. package/dist/__tests__/webhooks.test.js +286 -0
  27. package/dist/__tests__/webhooks.test.js.map +1 -0
  28. package/dist/__tests__/workflows.test.d.ts +2 -0
  29. package/dist/__tests__/workflows.test.d.ts.map +1 -0
  30. package/dist/__tests__/workflows.test.js +299 -0
  31. package/dist/__tests__/workflows.test.js.map +1 -0
  32. package/dist/adapters/calendar-adapter.d.ts +47 -0
  33. package/dist/adapters/calendar-adapter.d.ts.map +1 -0
  34. package/dist/adapters/calendar-adapter.js +2 -0
  35. package/dist/adapters/calendar-adapter.js.map +1 -0
  36. package/dist/adapters/email-adapter.d.ts +65 -0
  37. package/dist/adapters/email-adapter.d.ts.map +1 -0
  38. package/dist/adapters/email-adapter.js +40 -0
  39. package/dist/adapters/email-adapter.js.map +1 -0
  40. package/dist/adapters/index.d.ts +9 -0
  41. package/dist/adapters/index.d.ts.map +1 -0
  42. package/dist/adapters/index.js +3 -0
  43. package/dist/adapters/index.js.map +1 -0
  44. package/dist/adapters/job-adapter.d.ts +26 -0
  45. package/dist/adapters/job-adapter.d.ts.map +1 -0
  46. package/dist/adapters/job-adapter.js +13 -0
  47. package/dist/adapters/job-adapter.js.map +1 -0
  48. package/dist/adapters/payment-adapter.d.ts +106 -0
  49. package/dist/adapters/payment-adapter.d.ts.map +1 -0
  50. package/dist/adapters/payment-adapter.js +8 -0
  51. package/dist/adapters/payment-adapter.js.map +1 -0
  52. package/dist/adapters/sms-adapter.d.ts +33 -0
  53. package/dist/adapters/sms-adapter.d.ts.map +1 -0
  54. package/dist/adapters/sms-adapter.js +8 -0
  55. package/dist/adapters/sms-adapter.js.map +1 -0
  56. package/dist/adapters/storage-adapter.d.ts +12 -0
  57. package/dist/adapters/storage-adapter.d.ts.map +1 -0
  58. package/dist/adapters/storage-adapter.js +2 -0
  59. package/dist/adapters/storage-adapter.js.map +1 -0
  60. package/dist/api.d.ts +223 -0
  61. package/dist/api.d.ts.map +1 -0
  62. package/dist/api.js +271 -0
  63. package/dist/api.js.map +1 -0
  64. package/dist/auth.d.ts +71 -0
  65. package/dist/auth.d.ts.map +1 -0
  66. package/dist/auth.js +81 -0
  67. package/dist/auth.js.map +1 -0
  68. package/dist/booking-tokens.d.ts +23 -0
  69. package/dist/booking-tokens.d.ts.map +1 -0
  70. package/dist/booking-tokens.js +52 -0
  71. package/dist/booking-tokens.js.map +1 -0
  72. package/dist/email-templates.d.ts +36 -0
  73. package/dist/email-templates.d.ts.map +1 -0
  74. package/dist/email-templates.js +112 -0
  75. package/dist/email-templates.js.map +1 -0
  76. package/dist/index.d.ts +13 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +22 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/multi-tenancy.d.ts +132 -0
  81. package/dist/multi-tenancy.d.ts.map +1 -0
  82. package/dist/multi-tenancy.js +188 -0
  83. package/dist/multi-tenancy.js.map +1 -0
  84. package/dist/notification-jobs.d.ts +143 -0
  85. package/dist/notification-jobs.d.ts.map +1 -0
  86. package/dist/notification-jobs.js +278 -0
  87. package/dist/notification-jobs.js.map +1 -0
  88. package/dist/serialization-retry.d.ts +28 -0
  89. package/dist/serialization-retry.d.ts.map +1 -0
  90. package/dist/serialization-retry.js +71 -0
  91. package/dist/serialization-retry.js.map +1 -0
  92. package/dist/webhooks.d.ts +164 -0
  93. package/dist/webhooks.d.ts.map +1 -0
  94. package/dist/webhooks.js +228 -0
  95. package/dist/webhooks.js.map +1 -0
  96. package/dist/workflows.d.ts +169 -0
  97. package/dist/workflows.d.ts.map +1 -0
  98. package/dist/workflows.js +251 -0
  99. package/dist/workflows.js.map +1 -0
  100. package/package.json +32 -0
  101. package/src/__tests__/api.test.ts +354 -0
  102. package/src/__tests__/auth.test.ts +111 -0
  103. package/src/__tests__/concurrent-booking.test.ts +170 -0
  104. package/src/__tests__/multi-tenancy.test.ts +267 -0
  105. package/src/__tests__/serialization-retry.test.ts +76 -0
  106. package/src/__tests__/webhooks.test.ts +412 -0
  107. package/src/__tests__/workflows.test.ts +422 -0
  108. package/src/adapters/calendar-adapter.ts +49 -0
  109. package/src/adapters/email-adapter.ts +108 -0
  110. package/src/adapters/index.ts +36 -0
  111. package/src/adapters/job-adapter.ts +26 -0
  112. package/src/adapters/payment-adapter.ts +118 -0
  113. package/src/adapters/sms-adapter.ts +35 -0
  114. package/src/adapters/storage-adapter.ts +11 -0
  115. package/src/api.ts +446 -0
  116. package/src/auth.ts +146 -0
  117. package/src/booking-tokens.ts +61 -0
  118. package/src/email-templates.ts +140 -0
  119. package/src/index.ts +192 -0
  120. package/src/multi-tenancy.ts +301 -0
  121. package/src/notification-jobs.ts +428 -0
  122. package/src/serialization-retry.ts +94 -0
  123. package/src/webhooks.ts +378 -0
  124. package/src/workflows.ts +441 -0
  125. package/tsconfig.json +9 -0
  126. package/vitest.config.ts +7 -0
@@ -0,0 +1,422 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ resolveTemplateVariables,
4
+ evaluateConditions,
5
+ validateWorkflow,
6
+ matchWorkflows,
7
+ DEFAULT_TEMPLATES,
8
+ WorkflowValidationError,
9
+ type WorkflowDefinition,
10
+ type WorkflowContext,
11
+ type WorkflowCondition,
12
+ } from "../workflows.js";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fixtures
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function makeWorkflow(
19
+ overrides?: Partial<WorkflowDefinition>,
20
+ ): WorkflowDefinition {
21
+ return {
22
+ id: "wf-1",
23
+ name: "Confirmation Email",
24
+ trigger: "booking_created",
25
+ conditions: [],
26
+ actions: [
27
+ {
28
+ type: "send_email",
29
+ to: "customer",
30
+ subject: "Booking Confirmed: {booking.title}",
31
+ body: "Hi {attendee.name}, your booking is confirmed.",
32
+ },
33
+ ],
34
+ isActive: true,
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ const sampleContext: WorkflowContext = {
40
+ bookingId: "bk-1",
41
+ eventTypeId: "evt-1",
42
+ providerId: "prov-1",
43
+ customerEmail: "jane@example.com",
44
+ customerName: "Jane Doe",
45
+ hostName: "Dr. Smith",
46
+ eventTitle: "Consultation",
47
+ eventDuration: 30,
48
+ eventLocation: "123 Main St",
49
+ startsAt: new Date("2026-03-15T14:00:00Z"),
50
+ endsAt: new Date("2026-03-15T14:30:00Z"),
51
+ managementUrl: "https://example.com/manage/bk-1",
52
+ status: "confirmed",
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // resolveTemplateVariables
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe("resolveTemplateVariables", () => {
60
+ it("resolves all standard variables", () => {
61
+ const template =
62
+ "Hi {attendee.name}, your {booking.title} with {host.name} is on {booking.date} at {booking.startTime}–{booking.endTime}. Duration: {event.duration}. Location: {event.location}. Manage: {booking.managementUrl}";
63
+ const result = resolveTemplateVariables(template, sampleContext);
64
+
65
+ expect(result).toContain("Jane Doe");
66
+ expect(result).toContain("Consultation");
67
+ expect(result).toContain("Dr. Smith");
68
+ expect(result).toContain("30 minutes");
69
+ expect(result).toContain("123 Main St");
70
+ expect(result).toContain("https://example.com/manage/bk-1");
71
+ expect(result).not.toContain("{");
72
+ });
73
+
74
+ it("replaces missing variables with empty strings", () => {
75
+ const template = "Hi {attendee.name}, location: {event.location}";
76
+ const result = resolveTemplateVariables(template, {
77
+ customerName: "Jane",
78
+ });
79
+
80
+ expect(result).toBe("Hi Jane, location: ");
81
+ });
82
+
83
+ it("handles template with no variables", () => {
84
+ const result = resolveTemplateVariables("Hello world", sampleContext);
85
+ expect(result).toBe("Hello world");
86
+ });
87
+
88
+ it("handles empty template", () => {
89
+ const result = resolveTemplateVariables("", sampleContext);
90
+ expect(result).toBe("");
91
+ });
92
+
93
+ it("resolves default confirmation template", () => {
94
+ const result = resolveTemplateVariables(
95
+ DEFAULT_TEMPLATES.confirmation.body,
96
+ sampleContext,
97
+ );
98
+
99
+ expect(result).toContain("Jane Doe");
100
+ expect(result).toContain("Consultation");
101
+ expect(result).toContain("Dr. Smith");
102
+ expect(result).not.toContain("{attendee.name}");
103
+ });
104
+
105
+ it("resolves default reminder template", () => {
106
+ const result = resolveTemplateVariables(
107
+ DEFAULT_TEMPLATES.reminder_24h.body,
108
+ sampleContext,
109
+ );
110
+
111
+ expect(result).toContain("Jane Doe");
112
+ expect(result).toContain("Consultation");
113
+ });
114
+
115
+ it("resolves default cancellation template", () => {
116
+ const result = resolveTemplateVariables(
117
+ DEFAULT_TEMPLATES.cancellation.body,
118
+ sampleContext,
119
+ );
120
+
121
+ expect(result).toContain("Jane Doe");
122
+ expect(result).toContain("Consultation");
123
+ });
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // evaluateConditions
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe("evaluateConditions", () => {
131
+ it("returns true for empty conditions (unconditional)", () => {
132
+ expect(evaluateConditions([], sampleContext)).toBe(true);
133
+ });
134
+
135
+ it("equals operator matches", () => {
136
+ const conditions: WorkflowCondition[] = [
137
+ { field: "eventTypeId", operator: "equals", value: "evt-1" },
138
+ ];
139
+ expect(evaluateConditions(conditions, sampleContext)).toBe(true);
140
+ });
141
+
142
+ it("equals operator rejects non-match", () => {
143
+ const conditions: WorkflowCondition[] = [
144
+ { field: "eventTypeId", operator: "equals", value: "evt-2" },
145
+ ];
146
+ expect(evaluateConditions(conditions, sampleContext)).toBe(false);
147
+ });
148
+
149
+ it("not_equals operator works", () => {
150
+ const conditions: WorkflowCondition[] = [
151
+ { field: "status", operator: "not_equals", value: "cancelled" },
152
+ ];
153
+ expect(evaluateConditions(conditions, sampleContext)).toBe(true);
154
+ });
155
+
156
+ it("contains operator works (case-insensitive)", () => {
157
+ const conditions: WorkflowCondition[] = [
158
+ { field: "customerEmail", operator: "contains", value: "EXAMPLE" },
159
+ ];
160
+ expect(evaluateConditions(conditions, sampleContext)).toBe(true);
161
+ });
162
+
163
+ it("in operator works with array", () => {
164
+ const conditions: WorkflowCondition[] = [
165
+ { field: "status", operator: "in", value: ["confirmed", "pending"] },
166
+ ];
167
+ expect(evaluateConditions(conditions, sampleContext)).toBe(true);
168
+ });
169
+
170
+ it("in operator rejects when not in array", () => {
171
+ const conditions: WorkflowCondition[] = [
172
+ { field: "status", operator: "in", value: ["cancelled", "pending"] },
173
+ ];
174
+ expect(evaluateConditions(conditions, sampleContext)).toBe(false);
175
+ });
176
+
177
+ it("requires ALL conditions to match (AND logic)", () => {
178
+ const conditions: WorkflowCondition[] = [
179
+ { field: "eventTypeId", operator: "equals", value: "evt-1" },
180
+ { field: "status", operator: "equals", value: "cancelled" }, // doesn't match
181
+ ];
182
+ expect(evaluateConditions(conditions, sampleContext)).toBe(false);
183
+ });
184
+
185
+ it("handles missing context fields gracefully", () => {
186
+ const conditions: WorkflowCondition[] = [
187
+ { field: "nonexistentField", operator: "equals", value: "" },
188
+ ];
189
+ expect(evaluateConditions(conditions, sampleContext)).toBe(true); // "" === ""
190
+ });
191
+ });
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // validateWorkflow
195
+ // ---------------------------------------------------------------------------
196
+
197
+ describe("validateWorkflow", () => {
198
+ it("accepts valid workflow", () => {
199
+ expect(() => validateWorkflow(makeWorkflow())).not.toThrow();
200
+ });
201
+
202
+ it("rejects empty name", () => {
203
+ expect(() => validateWorkflow(makeWorkflow({ name: "" }))).toThrow(
204
+ WorkflowValidationError,
205
+ );
206
+ expect(() => validateWorkflow(makeWorkflow({ name: "" }))).toThrow(
207
+ "name is required",
208
+ );
209
+ });
210
+
211
+ it("rejects invalid trigger", () => {
212
+ expect(() =>
213
+ validateWorkflow(makeWorkflow({ trigger: "invalid" as never })),
214
+ ).toThrow("Invalid trigger");
215
+ });
216
+
217
+ it("rejects empty actions", () => {
218
+ expect(() =>
219
+ validateWorkflow(makeWorkflow({ actions: [] })),
220
+ ).toThrow("at least one action");
221
+ });
222
+
223
+ it("rejects email action without to", () => {
224
+ expect(() =>
225
+ validateWorkflow(
226
+ makeWorkflow({
227
+ actions: [
228
+ { type: "send_email", to: "", subject: "Hi", body: "Hello" },
229
+ ],
230
+ }),
231
+ ),
232
+ ).toThrow("'to' field");
233
+ });
234
+
235
+ it("rejects email action without subject", () => {
236
+ expect(() =>
237
+ validateWorkflow(
238
+ makeWorkflow({
239
+ actions: [
240
+ { type: "send_email", to: "customer", subject: "", body: "Hello" },
241
+ ],
242
+ }),
243
+ ),
244
+ ).toThrow("'subject' field");
245
+ });
246
+
247
+ it("rejects email action without body", () => {
248
+ expect(() =>
249
+ validateWorkflow(
250
+ makeWorkflow({
251
+ actions: [
252
+ { type: "send_email", to: "customer", subject: "Hi", body: "" },
253
+ ],
254
+ }),
255
+ ),
256
+ ).toThrow("'body' field");
257
+ });
258
+
259
+ it("rejects SMS action without to", () => {
260
+ expect(() =>
261
+ validateWorkflow(
262
+ makeWorkflow({
263
+ actions: [{ type: "send_sms", to: "", body: "Hello" }],
264
+ }),
265
+ ),
266
+ ).toThrow("'to' field");
267
+ });
268
+
269
+ it("rejects webhook action without url", () => {
270
+ expect(() =>
271
+ validateWorkflow(
272
+ makeWorkflow({
273
+ actions: [{ type: "fire_webhook", url: "" }],
274
+ }),
275
+ ),
276
+ ).toThrow("'url' field");
277
+ });
278
+
279
+ it("rejects status update action without status", () => {
280
+ expect(() =>
281
+ validateWorkflow(
282
+ makeWorkflow({
283
+ actions: [{ type: "update_status", status: "" }],
284
+ }),
285
+ ),
286
+ ).toThrow("'status' field");
287
+ });
288
+
289
+ it("accepts calendar event action with no extra fields", () => {
290
+ expect(() =>
291
+ validateWorkflow(
292
+ makeWorkflow({
293
+ actions: [{ type: "create_calendar_event" }],
294
+ }),
295
+ ),
296
+ ).not.toThrow();
297
+ });
298
+
299
+ it("rejects condition with empty field", () => {
300
+ expect(() =>
301
+ validateWorkflow(
302
+ makeWorkflow({
303
+ conditions: [{ field: "", operator: "equals", value: "x" }],
304
+ }),
305
+ ),
306
+ ).toThrow("Condition field is required");
307
+ });
308
+
309
+ it("rejects invalid condition operator", () => {
310
+ expect(() =>
311
+ validateWorkflow(
312
+ makeWorkflow({
313
+ conditions: [
314
+ { field: "status", operator: "invalid" as never, value: "x" },
315
+ ],
316
+ }),
317
+ ),
318
+ ).toThrow("Invalid condition operator");
319
+ });
320
+
321
+ it("validates all supported triggers", () => {
322
+ const triggers = [
323
+ "booking_created",
324
+ "booking_confirmed",
325
+ "booking_cancelled",
326
+ "booking_rescheduled",
327
+ "before_event",
328
+ "after_event",
329
+ "payment_received",
330
+ "payment_failed",
331
+ "no_show_confirmed",
332
+ "form_submitted",
333
+ ] as const;
334
+
335
+ for (const trigger of triggers) {
336
+ expect(() =>
337
+ validateWorkflow(makeWorkflow({ trigger })),
338
+ ).not.toThrow();
339
+ }
340
+ });
341
+ });
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // matchWorkflows
345
+ // ---------------------------------------------------------------------------
346
+
347
+ describe("matchWorkflows", () => {
348
+ const workflows: WorkflowDefinition[] = [
349
+ makeWorkflow({ id: "wf-1", trigger: "booking_created" }),
350
+ makeWorkflow({
351
+ id: "wf-2",
352
+ trigger: "booking_cancelled",
353
+ conditions: [
354
+ { field: "eventTypeId", operator: "equals", value: "evt-1" },
355
+ ],
356
+ }),
357
+ makeWorkflow({
358
+ id: "wf-3",
359
+ trigger: "booking_created",
360
+ isActive: false,
361
+ }),
362
+ makeWorkflow({
363
+ id: "wf-4",
364
+ trigger: "booking_created",
365
+ conditions: [
366
+ { field: "eventTypeId", operator: "equals", value: "evt-99" },
367
+ ],
368
+ }),
369
+ ];
370
+
371
+ it("matches active workflows with matching trigger", () => {
372
+ const matched = matchWorkflows(workflows, "booking_created", sampleContext);
373
+ expect(matched).toHaveLength(1);
374
+ expect(matched[0].id).toBe("wf-1");
375
+ });
376
+
377
+ it("excludes inactive workflows", () => {
378
+ const matched = matchWorkflows(workflows, "booking_created", sampleContext);
379
+ expect(matched.find((w) => w.id === "wf-3")).toBeUndefined();
380
+ });
381
+
382
+ it("excludes workflows with unmet conditions", () => {
383
+ const matched = matchWorkflows(workflows, "booking_created", sampleContext);
384
+ expect(matched.find((w) => w.id === "wf-4")).toBeUndefined();
385
+ });
386
+
387
+ it("matches workflows with met conditions", () => {
388
+ const matched = matchWorkflows(
389
+ workflows,
390
+ "booking_cancelled",
391
+ sampleContext,
392
+ );
393
+ expect(matched).toHaveLength(1);
394
+ expect(matched[0].id).toBe("wf-2");
395
+ });
396
+
397
+ it("returns empty array when no workflows match", () => {
398
+ const matched = matchWorkflows(workflows, "payment_received", sampleContext);
399
+ expect(matched).toHaveLength(0);
400
+ });
401
+ });
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // DEFAULT_TEMPLATES
405
+ // ---------------------------------------------------------------------------
406
+
407
+ describe("DEFAULT_TEMPLATES", () => {
408
+ it("has all expected templates", () => {
409
+ expect(DEFAULT_TEMPLATES).toHaveProperty("confirmation");
410
+ expect(DEFAULT_TEMPLATES).toHaveProperty("reminder_24h");
411
+ expect(DEFAULT_TEMPLATES).toHaveProperty("reminder_1h");
412
+ expect(DEFAULT_TEMPLATES).toHaveProperty("cancellation");
413
+ expect(DEFAULT_TEMPLATES).toHaveProperty("followup");
414
+ });
415
+
416
+ it("each template has subject and body", () => {
417
+ for (const [, template] of Object.entries(DEFAULT_TEMPLATES)) {
418
+ expect(template.subject).toBeTruthy();
419
+ expect(template.body).toBeTruthy();
420
+ }
421
+ });
422
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Calendar sync adapter interface.
3
+ * Default implementation uses Google Calendar OAuth.
4
+ * Swap to Outlook Calendar or CalDAV by implementing this interface.
5
+ */
6
+ export interface CalendarAdapter {
7
+ /** Create a calendar event for a booking */
8
+ createEvent(options: CalendarEventOptions): Promise<CalendarEventResult>;
9
+ /** Update an existing calendar event */
10
+ updateEvent(eventId: string, options: Partial<CalendarEventOptions>): Promise<CalendarEventResult>;
11
+ /** Delete a calendar event */
12
+ deleteEvent(eventId: string): Promise<void>;
13
+ /** Get conflicts (busy times) from external calendar */
14
+ getConflicts(timeMin: Date, timeMax: Date): Promise<CalendarConflict[]>;
15
+ }
16
+
17
+ /** Options for creating/updating a calendar event */
18
+ export interface CalendarEventOptions {
19
+ /** Event title */
20
+ title: string;
21
+ /** Event description/notes */
22
+ description?: string;
23
+ /** Event start time */
24
+ startsAt: Date;
25
+ /** Event end time */
26
+ endsAt: Date;
27
+ /** Event location */
28
+ location?: string;
29
+ /** Attendee email addresses */
30
+ attendees?: string[];
31
+ /** Provider's timezone */
32
+ timezone?: string;
33
+ }
34
+
35
+ /** Result of creating/updating a calendar event */
36
+ export interface CalendarEventResult {
37
+ /** External calendar event ID */
38
+ eventId: string;
39
+ /** Link to the calendar event */
40
+ eventUrl?: string;
41
+ }
42
+
43
+ /** A busy time period from an external calendar */
44
+ export interface CalendarConflict {
45
+ /** Start of busy period */
46
+ start: Date;
47
+ /** End of busy period */
48
+ end: Date;
49
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Email sending adapter interface.
3
+ * Default implementation uses Resend. Swap to SendGrid, AWS SES,
4
+ * or Postmark by implementing this interface.
5
+ */
6
+ export interface EmailAdapter {
7
+ /** Send a single email */
8
+ send(options: SendEmailOptions): Promise<EmailResult>;
9
+ /** Send multiple emails in a batch */
10
+ sendBatch(emails: SendEmailOptions[]): Promise<EmailResult[]>;
11
+ /** Get delivery status of a previously sent email */
12
+ getDeliveryStatus?(messageId: string): Promise<EmailDeliveryStatus>;
13
+ }
14
+
15
+ /** Options for sending an email */
16
+ export interface SendEmailOptions {
17
+ /** Recipient email address */
18
+ to: string;
19
+ /** Subject line */
20
+ subject: string;
21
+ /** HTML body */
22
+ html: string;
23
+ /** Plain text body (fallback) */
24
+ text?: string;
25
+ /** From address (defaults to configured sender) */
26
+ from?: string;
27
+ /** Reply-to address */
28
+ replyTo?: string;
29
+ /** Additional headers (List-Unsubscribe, etc.) */
30
+ headers?: Record<string, string>;
31
+ /** File attachments */
32
+ attachments?: EmailAttachment[];
33
+ /** Tags for categorization */
34
+ tags?: Record<string, string>;
35
+ }
36
+
37
+ /** An email attachment */
38
+ export interface EmailAttachment {
39
+ filename: string;
40
+ content: string | Buffer;
41
+ contentType?: string;
42
+ }
43
+
44
+ /** Result of sending an email */
45
+ export interface EmailResult {
46
+ success: boolean;
47
+ messageId?: string;
48
+ error?: string;
49
+ }
50
+
51
+ /** Delivery status of an email */
52
+ export interface EmailDeliveryStatus {
53
+ status: "sent" | "delivered" | "bounced" | "complained" | "failed";
54
+ timestamp: Date;
55
+ }
56
+
57
+ /**
58
+ * Generate an ICS calendar attachment for a booking.
59
+ */
60
+ export function generateICSAttachment(booking: {
61
+ id: string;
62
+ title: string;
63
+ startsAt: Date;
64
+ endsAt: Date;
65
+ location?: string;
66
+ description?: string;
67
+ organizerEmail?: string;
68
+ attendeeEmail: string;
69
+ }): EmailAttachment {
70
+ const formatICSDate = (d: Date) =>
71
+ d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
72
+
73
+ const ics = [
74
+ "BEGIN:VCALENDAR",
75
+ "VERSION:2.0",
76
+ "PRODID:-//SlotKit//EN",
77
+ "CALSCALE:GREGORIAN",
78
+ "METHOD:PUBLISH",
79
+ "BEGIN:VEVENT",
80
+ `UID:${booking.id}@slotkit`,
81
+ `DTSTART:${formatICSDate(booking.startsAt)}`,
82
+ `DTEND:${formatICSDate(booking.endsAt)}`,
83
+ `SUMMARY:${escapeICS(booking.title)}`,
84
+ booking.location ? `LOCATION:${escapeICS(booking.location)}` : "",
85
+ booking.description ? `DESCRIPTION:${escapeICS(booking.description)}` : "",
86
+ booking.organizerEmail ? `ORGANIZER:mailto:${booking.organizerEmail}` : "",
87
+ `ATTENDEE:mailto:${booking.attendeeEmail}`,
88
+ `DTSTAMP:${formatICSDate(new Date())}`,
89
+ "END:VEVENT",
90
+ "END:VCALENDAR",
91
+ ]
92
+ .filter(Boolean)
93
+ .join("\r\n");
94
+
95
+ return {
96
+ filename: "booking.ics",
97
+ content: ics,
98
+ contentType: "text/calendar",
99
+ };
100
+ }
101
+
102
+ function escapeICS(text: string): string {
103
+ return text
104
+ .replace(/\\/g, "\\\\")
105
+ .replace(/;/g, "\\;")
106
+ .replace(/,/g, "\\,")
107
+ .replace(/\n/g, "\\n");
108
+ }
@@ -0,0 +1,36 @@
1
+ export type {
2
+ EmailAdapter,
3
+ SendEmailOptions,
4
+ EmailResult,
5
+ EmailDeliveryStatus,
6
+ EmailAttachment,
7
+ } from "./email-adapter.js";
8
+ export { generateICSAttachment } from "./email-adapter.js";
9
+
10
+ export type {
11
+ CalendarAdapter,
12
+ CalendarEventOptions,
13
+ CalendarEventResult,
14
+ CalendarConflict,
15
+ } from "./calendar-adapter.js";
16
+
17
+ export type { JobAdapter } from "./job-adapter.js";
18
+ export { JOB_NAMES } from "./job-adapter.js";
19
+
20
+ export type { StorageAdapter } from "./storage-adapter.js";
21
+
22
+ export type {
23
+ SmsAdapter,
24
+ SendSmsOptions,
25
+ SmsResult,
26
+ } from "./sms-adapter.js";
27
+
28
+ export type {
29
+ PaymentAdapter,
30
+ CreatePaymentIntentOptions,
31
+ CreatePaymentIntentResult,
32
+ CreateSetupIntentOptions,
33
+ CreateSetupIntentResult,
34
+ CaptureResult,
35
+ RefundResult,
36
+ } from "./payment-adapter.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Background job adapter interface.
3
+ * Default implementation uses Inngest. Swap to Trigger.dev,
4
+ * BullMQ, or Vercel Cron by implementing this interface.
5
+ */
6
+ export interface JobAdapter {
7
+ /** Enqueue a job for immediate execution */
8
+ enqueue<T>(jobName: string, payload: T): Promise<void>;
9
+ /** Schedule a job for future execution */
10
+ schedule<T>(jobName: string, payload: T, runAt: Date): Promise<string>;
11
+ /** Cancel a previously scheduled job */
12
+ cancel(jobId: string): Promise<void>;
13
+ }
14
+
15
+ /** Common job names used by SlotKit */
16
+ export const JOB_NAMES = {
17
+ SEND_CONFIRMATION_EMAIL: "slotkit/send-confirmation-email",
18
+ SEND_REMINDER_EMAIL: "slotkit/send-reminder-email",
19
+ SEND_CANCELLATION_EMAIL: "slotkit/send-cancellation-email",
20
+ SEND_RESCHEDULE_EMAIL: "slotkit/send-reschedule-email",
21
+ SYNC_CALENDAR_EVENT: "slotkit/sync-calendar-event",
22
+ DELETE_CALENDAR_EVENT: "slotkit/delete-calendar-event",
23
+ CHECK_CALENDAR_CONFLICTS: "slotkit/check-calendar-conflicts",
24
+ AUTO_REJECT_PENDING: "slotkit/auto-reject-pending-booking",
25
+ PROCESS_WEBHOOK: "slotkit/process-webhook",
26
+ } as const;