@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.
- package/dist/adapters/acuity-scraper.d.ts +8 -0
- package/dist/adapters/acuity-scraper.d.ts.map +1 -0
- package/dist/adapters/acuity-scraper.js +8 -0
- package/dist/adapters/acuity-scraper.js.map +1 -0
- package/dist/adapters/types.d.ts +8 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +8 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/acuity-wizard.d.ts +49 -0
- package/dist/middleware/acuity-wizard.d.ts.map +1 -0
- package/dist/middleware/acuity-wizard.js +265 -0
- package/dist/middleware/acuity-wizard.js.map +1 -0
- package/dist/middleware/browser-service.d.ts +53 -0
- package/dist/middleware/browser-service.d.ts.map +1 -0
- package/dist/middleware/browser-service.js +105 -0
- package/dist/middleware/browser-service.js.map +1 -0
- package/dist/middleware/errors.d.ts +58 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +43 -0
- package/dist/middleware/errors.js.map +1 -0
- package/{src/middleware/index.ts → dist/middleware/index.d.ts} +5 -52
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +38 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +26 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +65 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/remote-adapter.d.ts +45 -0
- package/dist/middleware/remote-adapter.d.ts.map +1 -0
- package/dist/middleware/remote-adapter.js +178 -0
- package/dist/middleware/remote-adapter.js.map +1 -0
- package/dist/middleware/selector-health.d.ts +44 -0
- package/dist/middleware/selector-health.d.ts.map +1 -0
- package/dist/middleware/selector-health.js +144 -0
- package/dist/middleware/selector-health.js.map +1 -0
- package/dist/middleware/selectors.d.ts +108 -0
- package/dist/middleware/selectors.d.ts.map +1 -0
- package/dist/middleware/selectors.js +249 -0
- package/dist/middleware/selectors.js.map +1 -0
- package/dist/middleware/server.d.ts +34 -0
- package/dist/middleware/server.d.ts.map +1 -0
- package/dist/middleware/server.js +377 -0
- package/dist/middleware/server.js.map +1 -0
- package/dist/middleware/service-resolver.d.ts +46 -0
- package/dist/middleware/service-resolver.d.ts.map +1 -0
- package/dist/middleware/service-resolver.js +274 -0
- package/dist/middleware/service-resolver.js.map +1 -0
- package/dist/middleware/slot-parser.d.ts +29 -0
- package/dist/middleware/slot-parser.d.ts.map +1 -0
- package/dist/middleware/slot-parser.js +50 -0
- package/dist/middleware/slot-parser.js.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts +14 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.js +204 -0
- package/dist/middleware/steps/__tests__/fixtures.js.map +1 -0
- package/dist/middleware/steps/bypass-payment.d.ts +54 -0
- package/dist/middleware/steps/bypass-payment.d.ts.map +1 -0
- package/dist/middleware/steps/bypass-payment.js +164 -0
- package/dist/middleware/steps/bypass-payment.js.map +1 -0
- package/dist/middleware/steps/extract-business.d.ts +93 -0
- package/dist/middleware/steps/extract-business.d.ts.map +1 -0
- package/dist/middleware/steps/extract-business.js +170 -0
- package/dist/middleware/steps/extract-business.js.map +1 -0
- package/dist/middleware/steps/extract.d.ts +41 -0
- package/dist/middleware/steps/extract.d.ts.map +1 -0
- package/dist/middleware/steps/extract.js +128 -0
- package/dist/middleware/steps/extract.js.map +1 -0
- package/dist/middleware/steps/fill-form.d.ts +45 -0
- package/dist/middleware/steps/fill-form.d.ts.map +1 -0
- package/dist/middleware/steps/fill-form.js +262 -0
- package/dist/middleware/steps/fill-form.js.map +1 -0
- package/dist/middleware/steps/index.d.ts +12 -0
- package/dist/middleware/steps/index.d.ts.map +1 -0
- package/dist/middleware/steps/index.js +12 -0
- package/dist/middleware/steps/index.js.map +1 -0
- package/dist/middleware/steps/navigate.d.ts +51 -0
- package/dist/middleware/steps/navigate.d.ts.map +1 -0
- package/dist/middleware/steps/navigate.js +391 -0
- package/dist/middleware/steps/navigate.js.map +1 -0
- package/dist/middleware/steps/read-availability.d.ts +37 -0
- package/dist/middleware/steps/read-availability.d.ts.map +1 -0
- package/dist/middleware/steps/read-availability.js +298 -0
- package/dist/middleware/steps/read-availability.js.map +1 -0
- package/dist/middleware/steps/read-slots.d.ts +33 -0
- package/dist/middleware/steps/read-slots.d.ts.map +1 -0
- package/dist/middleware/steps/read-slots.js +295 -0
- package/dist/middleware/steps/read-slots.js.map +1 -0
- package/dist/middleware/steps/read-via-url.d.ts +39 -0
- package/dist/middleware/steps/read-via-url.d.ts.map +1 -0
- package/dist/middleware/steps/read-via-url.js +141 -0
- package/dist/middleware/steps/read-via-url.js.map +1 -0
- package/dist/middleware/steps/submit.d.ts +22 -0
- package/dist/middleware/steps/submit.d.ts.map +1 -0
- package/dist/middleware/steps/submit.js +112 -0
- package/dist/middleware/steps/submit.js.map +1 -0
- package/dist/middleware/wizard-calendar.d.ts +37 -0
- package/dist/middleware/wizard-calendar.d.ts.map +1 -0
- package/dist/middleware/wizard-calendar.js +177 -0
- package/dist/middleware/wizard-calendar.js.map +1 -0
- package/dist/middleware/wizard-service.d.ts +30 -0
- package/dist/middleware/wizard-service.d.ts.map +1 -0
- package/dist/middleware/wizard-service.js +89 -0
- package/dist/middleware/wizard-service.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/{src/server.ts → dist/server.js} +1 -0
- package/dist/server.js.map +1 -0
- package/package.json +16 -4
- package/.github/workflows/build-paper.yml +0 -39
- package/.github/workflows/ci.yml +0 -37
- package/Dockerfile +0 -53
- package/docs/blog-post.mdx +0 -240
- package/docs/paper/IEEEtran.bst +0 -2409
- package/docs/paper/IEEEtran.cls +0 -6347
- package/docs/paper/acuity-middleware-paper.tex +0 -375
- package/docs/paper/balance.sty +0 -87
- package/docs/paper/references.bib +0 -231
- package/docs/paper.md +0 -400
- package/flake.nix +0 -32
- package/modal-app.py +0 -82
- package/src/adapters/acuity-scraper.ts +0 -543
- package/src/adapters/types.ts +0 -193
- package/src/core/types.ts +0 -325
- package/src/index.ts +0 -75
- package/src/middleware/acuity-wizard.ts +0 -456
- package/src/middleware/browser-service.ts +0 -183
- package/src/middleware/errors.ts +0 -70
- package/src/middleware/remote-adapter.ts +0 -246
- package/src/middleware/selectors.ts +0 -308
- package/src/middleware/server.ts +0 -372
- package/src/middleware/steps/bypass-payment.ts +0 -226
- package/src/middleware/steps/extract.ts +0 -174
- package/src/middleware/steps/fill-form.ts +0 -359
- package/src/middleware/steps/index.ts +0 -27
- package/src/middleware/steps/navigate.ts +0 -537
- package/src/middleware/steps/read-availability.ts +0 -399
- package/src/middleware/steps/read-slots.ts +0 -405
- package/src/middleware/steps/submit.ts +0 -168
- 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
|