@tummycrypt/acuity-middleware 0.1.0 → 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 (148) hide show
  1. package/dist/adapters/acuity-scraper.d.ts +8 -0
  2. package/dist/adapters/acuity-scraper.d.ts.map +1 -0
  3. package/dist/adapters/acuity-scraper.js +8 -0
  4. package/dist/adapters/acuity-scraper.js.map +1 -0
  5. package/dist/adapters/types.d.ts +8 -0
  6. package/dist/adapters/types.d.ts.map +1 -0
  7. package/dist/adapters/types.js +8 -0
  8. package/dist/adapters/types.js.map +1 -0
  9. package/dist/core/types.d.ts +10 -0
  10. package/dist/core/types.d.ts.map +1 -0
  11. package/dist/core/types.js +2 -0
  12. package/dist/core/types.js.map +1 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +18 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/acuity-wizard.d.ts +49 -0
  18. package/dist/middleware/acuity-wizard.d.ts.map +1 -0
  19. package/dist/middleware/acuity-wizard.js +265 -0
  20. package/dist/middleware/acuity-wizard.js.map +1 -0
  21. package/dist/middleware/browser-service.d.ts +53 -0
  22. package/dist/middleware/browser-service.d.ts.map +1 -0
  23. package/dist/middleware/browser-service.js +105 -0
  24. package/dist/middleware/browser-service.js.map +1 -0
  25. package/dist/middleware/errors.d.ts +58 -0
  26. package/dist/middleware/errors.d.ts.map +1 -0
  27. package/dist/middleware/errors.js +43 -0
  28. package/dist/middleware/errors.js.map +1 -0
  29. package/{src/middleware/index.ts → dist/middleware/index.d.ts} +5 -52
  30. package/dist/middleware/index.d.ts.map +1 -0
  31. package/dist/middleware/index.js +38 -0
  32. package/dist/middleware/index.js.map +1 -0
  33. package/dist/middleware/logger.d.ts +26 -0
  34. package/dist/middleware/logger.d.ts.map +1 -0
  35. package/dist/middleware/logger.js +65 -0
  36. package/dist/middleware/logger.js.map +1 -0
  37. package/dist/middleware/remote-adapter.d.ts +45 -0
  38. package/dist/middleware/remote-adapter.d.ts.map +1 -0
  39. package/dist/middleware/remote-adapter.js +178 -0
  40. package/dist/middleware/remote-adapter.js.map +1 -0
  41. package/dist/middleware/selector-health.d.ts +44 -0
  42. package/dist/middleware/selector-health.d.ts.map +1 -0
  43. package/dist/middleware/selector-health.js +144 -0
  44. package/dist/middleware/selector-health.js.map +1 -0
  45. package/dist/middleware/selectors.d.ts +108 -0
  46. package/dist/middleware/selectors.d.ts.map +1 -0
  47. package/dist/middleware/selectors.js +249 -0
  48. package/dist/middleware/selectors.js.map +1 -0
  49. package/dist/middleware/server.d.ts +34 -0
  50. package/dist/middleware/server.d.ts.map +1 -0
  51. package/dist/middleware/server.js +377 -0
  52. package/dist/middleware/server.js.map +1 -0
  53. package/dist/middleware/service-resolver.d.ts +46 -0
  54. package/dist/middleware/service-resolver.d.ts.map +1 -0
  55. package/dist/middleware/service-resolver.js +274 -0
  56. package/dist/middleware/service-resolver.js.map +1 -0
  57. package/dist/middleware/slot-parser.d.ts +29 -0
  58. package/dist/middleware/slot-parser.d.ts.map +1 -0
  59. package/dist/middleware/slot-parser.js +50 -0
  60. package/dist/middleware/slot-parser.js.map +1 -0
  61. package/dist/middleware/steps/__tests__/fixtures.d.ts +14 -0
  62. package/dist/middleware/steps/__tests__/fixtures.d.ts.map +1 -0
  63. package/dist/middleware/steps/__tests__/fixtures.js +204 -0
  64. package/dist/middleware/steps/__tests__/fixtures.js.map +1 -0
  65. package/dist/middleware/steps/bypass-payment.d.ts +54 -0
  66. package/dist/middleware/steps/bypass-payment.d.ts.map +1 -0
  67. package/dist/middleware/steps/bypass-payment.js +164 -0
  68. package/dist/middleware/steps/bypass-payment.js.map +1 -0
  69. package/dist/middleware/steps/extract-business.d.ts +93 -0
  70. package/dist/middleware/steps/extract-business.d.ts.map +1 -0
  71. package/dist/middleware/steps/extract-business.js +170 -0
  72. package/dist/middleware/steps/extract-business.js.map +1 -0
  73. package/dist/middleware/steps/extract.d.ts +41 -0
  74. package/dist/middleware/steps/extract.d.ts.map +1 -0
  75. package/dist/middleware/steps/extract.js +128 -0
  76. package/dist/middleware/steps/extract.js.map +1 -0
  77. package/dist/middleware/steps/fill-form.d.ts +45 -0
  78. package/dist/middleware/steps/fill-form.d.ts.map +1 -0
  79. package/dist/middleware/steps/fill-form.js +262 -0
  80. package/dist/middleware/steps/fill-form.js.map +1 -0
  81. package/dist/middleware/steps/index.d.ts +12 -0
  82. package/dist/middleware/steps/index.d.ts.map +1 -0
  83. package/dist/middleware/steps/index.js +12 -0
  84. package/dist/middleware/steps/index.js.map +1 -0
  85. package/dist/middleware/steps/navigate.d.ts +51 -0
  86. package/dist/middleware/steps/navigate.d.ts.map +1 -0
  87. package/dist/middleware/steps/navigate.js +391 -0
  88. package/dist/middleware/steps/navigate.js.map +1 -0
  89. package/dist/middleware/steps/read-availability.d.ts +37 -0
  90. package/dist/middleware/steps/read-availability.d.ts.map +1 -0
  91. package/dist/middleware/steps/read-availability.js +298 -0
  92. package/dist/middleware/steps/read-availability.js.map +1 -0
  93. package/dist/middleware/steps/read-slots.d.ts +33 -0
  94. package/dist/middleware/steps/read-slots.d.ts.map +1 -0
  95. package/dist/middleware/steps/read-slots.js +295 -0
  96. package/dist/middleware/steps/read-slots.js.map +1 -0
  97. package/dist/middleware/steps/read-via-url.d.ts +39 -0
  98. package/dist/middleware/steps/read-via-url.d.ts.map +1 -0
  99. package/dist/middleware/steps/read-via-url.js +141 -0
  100. package/dist/middleware/steps/read-via-url.js.map +1 -0
  101. package/dist/middleware/steps/submit.d.ts +22 -0
  102. package/dist/middleware/steps/submit.d.ts.map +1 -0
  103. package/dist/middleware/steps/submit.js +112 -0
  104. package/dist/middleware/steps/submit.js.map +1 -0
  105. package/dist/middleware/wizard-calendar.d.ts +37 -0
  106. package/dist/middleware/wizard-calendar.d.ts.map +1 -0
  107. package/dist/middleware/wizard-calendar.js +177 -0
  108. package/dist/middleware/wizard-calendar.js.map +1 -0
  109. package/dist/middleware/wizard-service.d.ts +30 -0
  110. package/dist/middleware/wizard-service.d.ts.map +1 -0
  111. package/dist/middleware/wizard-service.js +89 -0
  112. package/dist/middleware/wizard-service.js.map +1 -0
  113. package/dist/server.d.ts +6 -0
  114. package/dist/server.d.ts.map +1 -0
  115. package/{src/server.ts → dist/server.js} +1 -0
  116. package/dist/server.js.map +1 -0
  117. package/package.json +16 -4
  118. package/.github/workflows/build-paper.yml +0 -39
  119. package/.github/workflows/ci.yml +0 -37
  120. package/Dockerfile +0 -53
  121. package/docs/blog-post.mdx +0 -240
  122. package/docs/paper/IEEEtran.bst +0 -2409
  123. package/docs/paper/IEEEtran.cls +0 -6347
  124. package/docs/paper/acuity-middleware-paper.tex +0 -375
  125. package/docs/paper/balance.sty +0 -87
  126. package/docs/paper/references.bib +0 -231
  127. package/docs/paper.md +0 -400
  128. package/flake.nix +0 -32
  129. package/modal-app.py +0 -82
  130. package/src/adapters/acuity-scraper.ts +0 -543
  131. package/src/adapters/types.ts +0 -193
  132. package/src/core/types.ts +0 -325
  133. package/src/index.ts +0 -75
  134. package/src/middleware/acuity-wizard.ts +0 -456
  135. package/src/middleware/browser-service.ts +0 -183
  136. package/src/middleware/errors.ts +0 -70
  137. package/src/middleware/remote-adapter.ts +0 -246
  138. package/src/middleware/selectors.ts +0 -308
  139. package/src/middleware/server.ts +0 -372
  140. package/src/middleware/steps/bypass-payment.ts +0 -226
  141. package/src/middleware/steps/extract.ts +0 -174
  142. package/src/middleware/steps/fill-form.ts +0 -359
  143. package/src/middleware/steps/index.ts +0 -27
  144. package/src/middleware/steps/navigate.ts +0 -537
  145. package/src/middleware/steps/read-availability.ts +0 -399
  146. package/src/middleware/steps/read-slots.ts +0 -405
  147. package/src/middleware/steps/submit.ts +0 -168
  148. package/tsconfig.json +0 -25
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Wizard Step: Read Available Dates from Acuity Calendar
3
+ *
4
+ * Navigates to the service calendar via click-through (not query params)
5
+ * and reads which calendar tiles are enabled (not disabled).
6
+ *
7
+ * Returns available dates for the currently visible month.
8
+ * Callers should advance months if needed.
9
+ */
10
+ import { Effect } from 'effect';
11
+ import { BrowserService } from '../browser-service.js';
12
+ import { WizardStepError } from '../errors.js';
13
+ import { resolveSelector, Selectors } from '../selectors.js';
14
+ // =============================================================================
15
+ // IMPLEMENTATION
16
+ // =============================================================================
17
+ /**
18
+ * Read available dates by navigating through the Acuity wizard to the calendar.
19
+ *
20
+ * Flow:
21
+ * 1. Load service page → find service → click "Book"
22
+ * 2. Land on calendar page
23
+ * 3. Read enabled (non-disabled) tiles for current month
24
+ * 4. Optionally advance to next months and read more
25
+ */
26
+ export const readAvailableDates = (params) => Effect.gen(function* () {
27
+ const { acquirePage, config } = yield* BrowserService;
28
+ const page = yield* acquirePage;
29
+ // Step 1: Load service page
30
+ yield* Effect.tryPromise({
31
+ try: () => page.goto(config.baseUrl, { waitUntil: 'networkidle', timeout: config.timeout }),
32
+ catch: (e) => new WizardStepError({
33
+ step: 'read-availability',
34
+ message: `Failed to load service page: ${e instanceof Error ? e.message : String(e)}`,
35
+ cause: e,
36
+ }),
37
+ });
38
+ // Step 2: Click the target service's "Book" button
39
+ yield* clickServiceBook(page, params.serviceName, params.appointmentTypeId);
40
+ // Step 3: Read available dates from calendar
41
+ const monthsToScan = params.monthsToScan ?? 2;
42
+ const allDates = [];
43
+ // If a specific target month is requested, navigate to it first
44
+ if (params.targetMonth) {
45
+ yield* navigateToMonth(page, params.targetMonth);
46
+ }
47
+ for (let i = 0; i < monthsToScan; i++) {
48
+ const dates = yield* readCalendarDates(page);
49
+ allDates.push(...dates);
50
+ // Advance to next month if more scanning needed
51
+ if (i < monthsToScan - 1) {
52
+ const advanced = yield* advanceMonth(page);
53
+ if (!advanced)
54
+ break; // No more months available
55
+ }
56
+ }
57
+ return allDates;
58
+ });
59
+ // =============================================================================
60
+ // HELPERS
61
+ // =============================================================================
62
+ /**
63
+ * Find a service by name and click its "Book" button.
64
+ * Waits for calendar URL pattern after clicking.
65
+ */
66
+ const clickServiceBook = (page, serviceName, expectedId) => Effect.gen(function* () {
67
+ // Wait for service list
68
+ yield* resolveSelector(page, Selectors.serviceList, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
69
+ step: 'read-availability',
70
+ message: 'Service list did not load',
71
+ }))));
72
+ // Find matching service
73
+ const serviceItem = yield* Effect.tryPromise({
74
+ try: async () => {
75
+ const items = await page.$$(Selectors.serviceList[0]);
76
+ for (const item of items) {
77
+ const nameEl = await item.$(Selectors.serviceName[0]);
78
+ const name = await nameEl?.textContent();
79
+ if (name && name.trim().toLowerCase().includes(serviceName.toLowerCase())) {
80
+ return item;
81
+ }
82
+ }
83
+ return null;
84
+ },
85
+ catch: (e) => new WizardStepError({
86
+ step: 'read-availability',
87
+ message: `Error searching services: ${e instanceof Error ? e.message : String(e)}`,
88
+ cause: e,
89
+ }),
90
+ });
91
+ if (!serviceItem) {
92
+ return yield* Effect.fail(new WizardStepError({
93
+ step: 'read-availability',
94
+ message: `Service "${serviceName}" not found`,
95
+ }));
96
+ }
97
+ // Click "Book" button
98
+ const bookBtn = yield* Effect.tryPromise({
99
+ try: () => serviceItem.$(Selectors.serviceBookButton[0]),
100
+ catch: (e) => new WizardStepError({
101
+ step: 'read-availability',
102
+ message: `Book button error: ${e instanceof Error ? e.message : String(e)}`,
103
+ cause: e,
104
+ }),
105
+ });
106
+ if (!bookBtn) {
107
+ return yield* Effect.fail(new WizardStepError({
108
+ step: 'read-availability',
109
+ message: `"Book" button not found for "${serviceName}"`,
110
+ }));
111
+ }
112
+ yield* Effect.tryPromise({
113
+ try: async () => {
114
+ await bookBtn.click();
115
+ await page.waitForURL(/\/appointment\/\d+\/calendar\/\d+/, { timeout: 10000 });
116
+ },
117
+ catch: (e) => new WizardStepError({
118
+ step: 'read-availability',
119
+ message: `Failed to navigate to calendar: ${e instanceof Error ? e.message : String(e)}`,
120
+ cause: e,
121
+ }),
122
+ });
123
+ // Verify appointment type ID if provided
124
+ if (expectedId) {
125
+ const url = page.url();
126
+ const match = url.match(/\/appointment\/(\d+)/);
127
+ if (match && match[1] !== expectedId) {
128
+ return yield* Effect.fail(new WizardStepError({
129
+ step: 'read-availability',
130
+ message: `Expected appointment type ${expectedId} but got ${match[1]}`,
131
+ }));
132
+ }
133
+ }
134
+ });
135
+ /**
136
+ * Read all available (non-disabled) dates from the currently visible calendar month.
137
+ */
138
+ const readCalendarDates = (page) => Effect.gen(function* () {
139
+ // Wait for calendar
140
+ yield* resolveSelector(page, Selectors.calendar, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
141
+ step: 'read-availability',
142
+ message: 'Calendar did not load',
143
+ }))));
144
+ // Get current month/year from calendar label
145
+ const monthInfo = yield* getCalendarMonthInfo(page);
146
+ // Read all non-disabled, non-neighboring-month tiles
147
+ const dates = yield* Effect.tryPromise({
148
+ try: async () => {
149
+ const results = [];
150
+ const tiles = await page.$$(Selectors.calendarDay[0]);
151
+ for (const tile of tiles) {
152
+ const isDisabled = await tile.evaluate((el) => el.disabled);
153
+ if (isDisabled)
154
+ continue;
155
+ const classes = (await tile.getAttribute('class')) ?? '';
156
+ if (classes.includes('neighboringMonth'))
157
+ continue;
158
+ const text = await tile.textContent();
159
+ const dayNum = parseInt(text?.trim() ?? '', 10);
160
+ if (isNaN(dayNum) || dayNum < 1 || dayNum > 31)
161
+ continue;
162
+ // Build YYYY-MM-DD from month info + day
163
+ const dateStr = `${monthInfo.year}-${String(monthInfo.month + 1).padStart(2, '0')}-${String(dayNum).padStart(2, '0')}`;
164
+ results.push({ date: dateStr, slots: 1 });
165
+ }
166
+ return results;
167
+ },
168
+ catch: (e) => new WizardStepError({
169
+ step: 'read-availability',
170
+ message: `Error reading calendar tiles: ${e instanceof Error ? e.message : String(e)}`,
171
+ cause: e,
172
+ }),
173
+ });
174
+ return dates;
175
+ });
176
+ const MONTH_NAMES = [
177
+ 'january', 'february', 'march', 'april', 'may', 'june',
178
+ 'july', 'august', 'september', 'october', 'november', 'december',
179
+ ];
180
+ /**
181
+ * Get the currently displayed month and year from the calendar label.
182
+ * Retries up to 3 times with brief waits for React rendering.
183
+ */
184
+ const getCalendarMonthInfo = (page) => Effect.gen(function* () {
185
+ // Wait for calendar month label to appear
186
+ yield* Effect.tryPromise({
187
+ try: () => page.waitForSelector(Selectors.calendarMonth[0], { timeout: 5000 }),
188
+ catch: () => null,
189
+ }).pipe(Effect.orElseSucceed(() => null));
190
+ // Retry up to 3 times — React may still be rendering
191
+ for (let retry = 0; retry < 3; retry++) {
192
+ const info = yield* Effect.tryPromise({
193
+ try: async () => {
194
+ for (const selector of Selectors.calendarMonth) {
195
+ const text = await page.$eval(selector, (el) => el.textContent?.trim() ?? null).catch(() => null);
196
+ if (text) {
197
+ // Try "March 2026" or "March\n2026" or "March2026" (nested spans)
198
+ const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
199
+ if (match) {
200
+ const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
201
+ if (monthIndex >= 0) {
202
+ return { month: monthIndex, year: parseInt(match[2], 10) };
203
+ }
204
+ }
205
+ }
206
+ }
207
+ // Also try innerText which resolves visibility better than textContent
208
+ for (const selector of Selectors.calendarMonth) {
209
+ const text = await page.$eval(selector, (el) => el.innerText?.trim() ?? null).catch(() => null);
210
+ if (text) {
211
+ const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
212
+ if (match) {
213
+ const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
214
+ if (monthIndex >= 0) {
215
+ return { month: monthIndex, year: parseInt(match[2], 10) };
216
+ }
217
+ }
218
+ }
219
+ }
220
+ return null;
221
+ },
222
+ catch: () => null,
223
+ }).pipe(Effect.orElseSucceed(() => null));
224
+ if (info)
225
+ return info;
226
+ // Wait before retrying
227
+ yield* Effect.tryPromise({
228
+ try: () => page.waitForTimeout(1000),
229
+ catch: () => null,
230
+ }).pipe(Effect.orElseSucceed(() => null));
231
+ }
232
+ return yield* Effect.fail(new WizardStepError({
233
+ step: 'read-availability',
234
+ message: 'Could not determine calendar month after 3 retries',
235
+ }));
236
+ });
237
+ /**
238
+ * Navigate the calendar to a specific target month (YYYY-MM format).
239
+ */
240
+ const navigateToMonth = (page, targetMonth) => Effect.gen(function* () {
241
+ const [yearStr, monthStr] = targetMonth.split('-');
242
+ const targetYear = parseInt(yearStr, 10);
243
+ const targetMonthIdx = parseInt(monthStr, 10) - 1;
244
+ for (let i = 0; i < 12; i++) {
245
+ const current = yield* getCalendarMonthInfo(page);
246
+ if (current.month === targetMonthIdx && current.year === targetYear)
247
+ return;
248
+ const currentFirst = new Date(current.year, current.month, 1);
249
+ const targetFirst = new Date(targetYear, targetMonthIdx, 1);
250
+ const direction = targetFirst > currentFirst ? 'next' : 'prev';
251
+ const selectors = direction === 'prev' ? Selectors.calendarPrev : Selectors.calendarNext;
252
+ const btn = yield* resolveSelector(page, selectors, 3000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
253
+ step: 'read-availability',
254
+ message: `Calendar ${direction} button not found`,
255
+ }))));
256
+ yield* Effect.tryPromise({
257
+ try: async () => {
258
+ await btn.element.click();
259
+ await page.waitForTimeout(500);
260
+ },
261
+ catch: (e) => new WizardStepError({
262
+ step: 'read-availability',
263
+ message: `Calendar nav failed: ${e instanceof Error ? e.message : String(e)}`,
264
+ cause: e,
265
+ }),
266
+ });
267
+ }
268
+ });
269
+ /**
270
+ * Advance to the next month. Returns false if next button is not available.
271
+ */
272
+ const advanceMonth = (page) => Effect.gen(function* () {
273
+ const btn = yield* Effect.tryPromise({
274
+ try: () => page.$(Selectors.calendarNext[0]),
275
+ catch: () => null,
276
+ }).pipe(Effect.orElseSucceed(() => null));
277
+ if (!btn)
278
+ return false;
279
+ const isDisabled = yield* Effect.tryPromise({
280
+ try: () => btn.evaluate((el) => el.disabled),
281
+ catch: () => true,
282
+ }).pipe(Effect.orElseSucceed(() => true));
283
+ if (isDisabled)
284
+ return false;
285
+ yield* Effect.tryPromise({
286
+ try: async () => {
287
+ await btn.click();
288
+ await page.waitForTimeout(500);
289
+ },
290
+ catch: (e) => new WizardStepError({
291
+ step: 'read-availability',
292
+ message: `Failed to advance month: ${e instanceof Error ? e.message : String(e)}`,
293
+ cause: e,
294
+ }),
295
+ });
296
+ return true;
297
+ });
298
+ //# sourceMappingURL=read-availability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-availability.js","sourceRoot":"","sources":["../../../src/middleware/steps/read-availability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wizard Step: Read Time Slots from Acuity Calendar
3
+ *
4
+ * Navigates to the service calendar via click-through,
5
+ * advances to the target date, clicks the day tile,
6
+ * and reads all available time slot buttons.
7
+ */
8
+ import { Effect } from 'effect';
9
+ import { BrowserService } from '../browser-service.js';
10
+ import { WizardStepError } from '../errors.js';
11
+ export interface ReadSlotsParams {
12
+ /** Service name to match against the service list */
13
+ readonly serviceName: string;
14
+ /** Appointment type ID (used to verify correct service selected) */
15
+ readonly appointmentTypeId?: string;
16
+ /** Target date (YYYY-MM-DD) */
17
+ readonly date: string;
18
+ }
19
+ export interface SlotResult {
20
+ readonly datetime: string;
21
+ readonly available: boolean;
22
+ }
23
+ /**
24
+ * Read time slots for a specific date by navigating the Acuity wizard.
25
+ *
26
+ * Flow:
27
+ * 1. Load service page → find service → click "Book"
28
+ * 2. Navigate calendar to target month
29
+ * 3. Click target day tile
30
+ * 4. Read all time slot buttons
31
+ */
32
+ export declare const readTimeSlots: (params: ReadSlotsParams) => Effect.Effect<SlotResult[], import("../errors.js").BrowserError | WizardStepError, import("effect/Scope").Scope | BrowserService>;
33
+ //# sourceMappingURL=read-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-slots.d.ts","sourceRoot":"","sources":["../../../src/middleware/steps/read-slots.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO/C,MAAM,WAAW,eAAe;IAC/B,qDAAqD;IACrD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC5B;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,eAAe,sIA+BlD,CAAC"}
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Wizard Step: Read Time Slots from Acuity Calendar
3
+ *
4
+ * Navigates to the service calendar via click-through,
5
+ * advances to the target date, clicks the day tile,
6
+ * and reads all available time slot buttons.
7
+ */
8
+ import { Effect } from 'effect';
9
+ import { BrowserService } from '../browser-service.js';
10
+ import { WizardStepError } from '../errors.js';
11
+ import { resolveSelector, Selectors } from '../selectors.js';
12
+ // =============================================================================
13
+ // IMPLEMENTATION
14
+ // =============================================================================
15
+ /**
16
+ * Read time slots for a specific date by navigating the Acuity wizard.
17
+ *
18
+ * Flow:
19
+ * 1. Load service page → find service → click "Book"
20
+ * 2. Navigate calendar to target month
21
+ * 3. Click target day tile
22
+ * 4. Read all time slot buttons
23
+ */
24
+ export const readTimeSlots = (params) => Effect.gen(function* () {
25
+ const { acquirePage, config } = yield* BrowserService;
26
+ const page = yield* acquirePage;
27
+ // Step 1: Load service page
28
+ yield* Effect.tryPromise({
29
+ try: () => page.goto(config.baseUrl, { waitUntil: 'networkidle', timeout: config.timeout }),
30
+ catch: (e) => new WizardStepError({
31
+ step: 'read-slots',
32
+ message: `Failed to load service page: ${e instanceof Error ? e.message : String(e)}`,
33
+ cause: e,
34
+ }),
35
+ });
36
+ // Step 2: Click the target service's "Book" button
37
+ yield* clickServiceBook(page, params.serviceName, params.appointmentTypeId);
38
+ // Step 3: Navigate to the target month
39
+ const targetDate = new Date(params.date + 'T12:00:00');
40
+ const targetMonth = `${targetDate.getFullYear()}-${String(targetDate.getMonth() + 1).padStart(2, '0')}`;
41
+ yield* navigateToTargetMonth(page, targetMonth);
42
+ // Step 4: Click the target day
43
+ yield* clickDay(page, targetDate.getDate());
44
+ // Step 5: Read time slots
45
+ const slots = yield* readSlotButtons(page, params.date);
46
+ return slots;
47
+ });
48
+ // =============================================================================
49
+ // HELPERS
50
+ // =============================================================================
51
+ const clickServiceBook = (page, serviceName, expectedId) => Effect.gen(function* () {
52
+ yield* resolveSelector(page, Selectors.serviceList, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
53
+ step: 'read-slots',
54
+ message: 'Service list did not load',
55
+ }))));
56
+ const serviceItem = yield* Effect.tryPromise({
57
+ try: async () => {
58
+ const items = await page.$$(Selectors.serviceList[0]);
59
+ for (const item of items) {
60
+ const nameEl = await item.$(Selectors.serviceName[0]);
61
+ const name = await nameEl?.textContent();
62
+ if (name && name.trim().toLowerCase().includes(serviceName.toLowerCase())) {
63
+ return item;
64
+ }
65
+ }
66
+ return null;
67
+ },
68
+ catch: (e) => new WizardStepError({
69
+ step: 'read-slots',
70
+ message: `Error searching services: ${e instanceof Error ? e.message : String(e)}`,
71
+ cause: e,
72
+ }),
73
+ });
74
+ if (!serviceItem) {
75
+ return yield* Effect.fail(new WizardStepError({
76
+ step: 'read-slots',
77
+ message: `Service "${serviceName}" not found`,
78
+ }));
79
+ }
80
+ const bookBtn = yield* Effect.tryPromise({
81
+ try: () => serviceItem.$(Selectors.serviceBookButton[0]),
82
+ catch: (e) => new WizardStepError({
83
+ step: 'read-slots',
84
+ message: `Book button error: ${e instanceof Error ? e.message : String(e)}`,
85
+ cause: e,
86
+ }),
87
+ });
88
+ if (!bookBtn) {
89
+ return yield* Effect.fail(new WizardStepError({
90
+ step: 'read-slots',
91
+ message: `"Book" button not found for "${serviceName}"`,
92
+ }));
93
+ }
94
+ yield* Effect.tryPromise({
95
+ try: async () => {
96
+ await bookBtn.click();
97
+ await page.waitForURL(/\/appointment\/\d+\/calendar\/\d+/, { timeout: 10000 });
98
+ },
99
+ catch: (e) => new WizardStepError({
100
+ step: 'read-slots',
101
+ message: `Failed to navigate to calendar: ${e instanceof Error ? e.message : String(e)}`,
102
+ cause: e,
103
+ }),
104
+ });
105
+ if (expectedId) {
106
+ const url = page.url();
107
+ const match = url.match(/\/appointment\/(\d+)/);
108
+ if (match && match[1] !== expectedId) {
109
+ return yield* Effect.fail(new WizardStepError({
110
+ step: 'read-slots',
111
+ message: `Expected appointment type ${expectedId} but got ${match[1]}`,
112
+ }));
113
+ }
114
+ }
115
+ });
116
+ const MONTH_NAMES = [
117
+ 'january', 'february', 'march', 'april', 'may', 'june',
118
+ 'july', 'august', 'september', 'october', 'november', 'december',
119
+ ];
120
+ const navigateToTargetMonth = (page, targetMonth) => Effect.gen(function* () {
121
+ yield* resolveSelector(page, Selectors.calendar, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
122
+ step: 'read-slots',
123
+ message: 'Calendar did not load',
124
+ }))));
125
+ const [yearStr, monthStr] = targetMonth.split('-');
126
+ const targetYear = parseInt(yearStr, 10);
127
+ const targetMonthIdx = parseInt(monthStr, 10) - 1;
128
+ for (let i = 0; i < 12; i++) {
129
+ const current = yield* getCalendarMonth(page);
130
+ if (!current) {
131
+ return yield* Effect.fail(new WizardStepError({
132
+ step: 'read-slots',
133
+ message: 'Could not determine calendar month',
134
+ }));
135
+ }
136
+ if (current.month === targetMonthIdx && current.year === targetYear)
137
+ return;
138
+ const currentFirst = new Date(current.year, current.month, 1);
139
+ const targetFirst = new Date(targetYear, targetMonthIdx, 1);
140
+ const direction = targetFirst > currentFirst ? 'next' : 'prev';
141
+ const selectors = direction === 'prev' ? Selectors.calendarPrev : Selectors.calendarNext;
142
+ const btn = yield* resolveSelector(page, selectors, 3000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
143
+ step: 'read-slots',
144
+ message: `Calendar ${direction} button not found`,
145
+ }))));
146
+ yield* Effect.tryPromise({
147
+ try: async () => {
148
+ await btn.element.click();
149
+ await page.waitForTimeout(500);
150
+ },
151
+ catch: (e) => new WizardStepError({
152
+ step: 'read-slots',
153
+ message: `Calendar nav failed: ${e instanceof Error ? e.message : String(e)}`,
154
+ cause: e,
155
+ }),
156
+ });
157
+ }
158
+ });
159
+ const getCalendarMonth = (page) => Effect.gen(function* () {
160
+ // Wait for calendar month label to appear
161
+ yield* Effect.tryPromise({
162
+ try: () => page.waitForSelector(Selectors.calendarMonth[0], { timeout: 5000 }),
163
+ catch: () => null,
164
+ }).pipe(Effect.orElseSucceed(() => null));
165
+ // Retry up to 3 times — React may still be rendering
166
+ for (let retry = 0; retry < 3; retry++) {
167
+ const info = yield* Effect.tryPromise({
168
+ try: async () => {
169
+ for (const selector of Selectors.calendarMonth) {
170
+ const text = await page.$eval(selector, (el) => el.textContent?.trim() ?? null).catch(() => null);
171
+ if (text) {
172
+ const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
173
+ if (match) {
174
+ const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
175
+ if (monthIndex >= 0) {
176
+ return { month: monthIndex, year: parseInt(match[2], 10) };
177
+ }
178
+ }
179
+ }
180
+ }
181
+ // Also try innerText which resolves visibility better than textContent
182
+ for (const selector of Selectors.calendarMonth) {
183
+ const text = await page.$eval(selector, (el) => el.innerText?.trim() ?? null).catch(() => null);
184
+ if (text) {
185
+ const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
186
+ if (match) {
187
+ const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
188
+ if (monthIndex >= 0) {
189
+ return { month: monthIndex, year: parseInt(match[2], 10) };
190
+ }
191
+ }
192
+ }
193
+ }
194
+ return null;
195
+ },
196
+ catch: () => null,
197
+ }).pipe(Effect.orElseSucceed(() => null));
198
+ if (info)
199
+ return info;
200
+ // Wait before retrying
201
+ yield* Effect.tryPromise({
202
+ try: () => page.waitForTimeout(1000),
203
+ catch: () => null,
204
+ }).pipe(Effect.orElseSucceed(() => null));
205
+ }
206
+ return null;
207
+ });
208
+ /**
209
+ * Click the calendar tile for a specific day number.
210
+ */
211
+ const clickDay = (page, dayOfMonth) => Effect.gen(function* () {
212
+ const clicked = yield* Effect.tryPromise({
213
+ try: async () => {
214
+ const tiles = await page.$$(Selectors.calendarDay[0]);
215
+ for (const tile of tiles) {
216
+ const isDisabled = await tile.evaluate((el) => el.disabled);
217
+ if (isDisabled)
218
+ continue;
219
+ const classes = (await tile.getAttribute('class')) ?? '';
220
+ if (classes.includes('neighboringMonth'))
221
+ continue;
222
+ const text = await tile.textContent();
223
+ const num = parseInt(text?.trim() ?? '', 10);
224
+ if (num === dayOfMonth) {
225
+ await tile.click();
226
+ return true;
227
+ }
228
+ }
229
+ return false;
230
+ },
231
+ catch: (e) => new WizardStepError({
232
+ step: 'read-slots',
233
+ message: `Error clicking day ${dayOfMonth}: ${e instanceof Error ? e.message : String(e)}`,
234
+ cause: e,
235
+ }),
236
+ });
237
+ if (!clicked) {
238
+ return yield* Effect.fail(new WizardStepError({
239
+ step: 'read-slots',
240
+ message: `Day ${dayOfMonth} not available on calendar`,
241
+ }));
242
+ }
243
+ // Wait for time slots to appear
244
+ yield* resolveSelector(page, Selectors.timeSlotContainer, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new WizardStepError({
245
+ step: 'read-slots',
246
+ message: 'Time slots did not appear after clicking day',
247
+ }))));
248
+ });
249
+ /**
250
+ * Read all time slot buttons and return structured data.
251
+ * Slot text format: "10:00 AM1 spot left" or "2:30 PM"
252
+ */
253
+ const readSlotButtons = (page, dateStr) => Effect.tryPromise({
254
+ try: async () => {
255
+ const results = [];
256
+ const slots = await page.$$(Selectors.timeSlot[0]);
257
+ for (const slot of slots) {
258
+ const text = await slot.textContent();
259
+ if (!text)
260
+ continue;
261
+ // Extract time from slot text: "10:00 AM1 spot left" → "10:00 AM"
262
+ const timeMatch = text.trim().match(/^(\d{1,2}:\d{2}\s*[AP]M)/i);
263
+ if (!timeMatch)
264
+ continue;
265
+ const timeStr = timeMatch[1].trim();
266
+ // Convert to ISO 8601 datetime
267
+ const datetime = buildIsoDatetime(dateStr, timeStr);
268
+ results.push({ datetime, available: true });
269
+ }
270
+ return results;
271
+ },
272
+ catch: (e) => new WizardStepError({
273
+ step: 'read-slots',
274
+ message: `Error reading slots: ${e instanceof Error ? e.message : String(e)}`,
275
+ cause: e,
276
+ }),
277
+ });
278
+ /**
279
+ * Build ISO datetime from date string and time string.
280
+ * "2026-03-15" + "10:00 AM" → "2026-03-15T10:00:00"
281
+ */
282
+ const buildIsoDatetime = (dateStr, timeStr) => {
283
+ const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
284
+ if (!match)
285
+ return `${dateStr}T00:00:00`;
286
+ let hours = parseInt(match[1], 10);
287
+ const minutes = match[2];
288
+ const ampm = match[3].toUpperCase();
289
+ if (ampm === 'PM' && hours !== 12)
290
+ hours += 12;
291
+ if (ampm === 'AM' && hours === 12)
292
+ hours = 0;
293
+ return `${dateStr}T${String(hours).padStart(2, '0')}:${minutes}:00`;
294
+ };
295
+ //# sourceMappingURL=read-slots.js.map