@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
@@ -1,399 +0,0 @@
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
-
11
- import { Effect } from 'effect';
12
- import type { Page, ElementHandle } from 'playwright-core';
13
- import { BrowserService } from '../browser-service.js';
14
- import { WizardStepError } from '../errors.js';
15
- import { resolveSelector, Selectors } from '../selectors.js';
16
-
17
- // =============================================================================
18
- // TYPES
19
- // =============================================================================
20
-
21
- export interface ReadAvailabilityParams {
22
- /** Service name to match against the service list */
23
- readonly serviceName: string;
24
- /** Appointment type ID (used to verify correct service selected) */
25
- readonly appointmentTypeId?: string;
26
- /** Target month (YYYY-MM) — navigates calendar if provided */
27
- readonly targetMonth?: string;
28
- /** How many months ahead to scan (default 2) */
29
- readonly monthsToScan?: number;
30
- }
31
-
32
- export interface AvailableDateResult {
33
- readonly date: string; // YYYY-MM-DD
34
- readonly slots: number; // estimated (1 = available, we don't know exact count without clicking)
35
- }
36
-
37
- // =============================================================================
38
- // IMPLEMENTATION
39
- // =============================================================================
40
-
41
- /**
42
- * Read available dates by navigating through the Acuity wizard to the calendar.
43
- *
44
- * Flow:
45
- * 1. Load service page → find service → click "Book"
46
- * 2. Land on calendar page
47
- * 3. Read enabled (non-disabled) tiles for current month
48
- * 4. Optionally advance to next months and read more
49
- */
50
- export const readAvailableDates = (params: ReadAvailabilityParams) =>
51
- Effect.gen(function* () {
52
- const { acquirePage, config } = yield* BrowserService;
53
- const page: Page = yield* acquirePage;
54
-
55
- // Step 1: Load service page
56
- yield* Effect.tryPromise({
57
- try: () => page.goto(config.baseUrl, { waitUntil: 'networkidle', timeout: config.timeout }),
58
- catch: (e) =>
59
- new WizardStepError({
60
- step: 'read-availability',
61
- message: `Failed to load service page: ${e instanceof Error ? e.message : String(e)}`,
62
- cause: e,
63
- }),
64
- });
65
-
66
- // Step 2: Click the target service's "Book" button
67
- yield* clickServiceBook(page, params.serviceName, params.appointmentTypeId);
68
-
69
- // Step 3: Read available dates from calendar
70
- const monthsToScan = params.monthsToScan ?? 2;
71
- const allDates: AvailableDateResult[] = [];
72
-
73
- // If a specific target month is requested, navigate to it first
74
- if (params.targetMonth) {
75
- yield* navigateToMonth(page, params.targetMonth);
76
- }
77
-
78
- for (let i = 0; i < monthsToScan; i++) {
79
- const dates = yield* readCalendarDates(page);
80
- allDates.push(...dates);
81
-
82
- // Advance to next month if more scanning needed
83
- if (i < monthsToScan - 1) {
84
- const advanced = yield* advanceMonth(page);
85
- if (!advanced) break; // No more months available
86
- }
87
- }
88
-
89
- return allDates;
90
- });
91
-
92
- // =============================================================================
93
- // HELPERS
94
- // =============================================================================
95
-
96
- /**
97
- * Find a service by name and click its "Book" button.
98
- * Waits for calendar URL pattern after clicking.
99
- */
100
- const clickServiceBook = (
101
- page: Page,
102
- serviceName: string,
103
- expectedId?: string,
104
- ) =>
105
- Effect.gen(function* () {
106
- // Wait for service list
107
- yield* resolveSelector(page, Selectors.serviceList, 10000).pipe(
108
- Effect.catchTag('SelectorError', () =>
109
- Effect.fail(
110
- new WizardStepError({
111
- step: 'read-availability',
112
- message: 'Service list did not load',
113
- }),
114
- ),
115
- ),
116
- );
117
-
118
- // Find matching service
119
- const serviceItem: ElementHandle | null = yield* Effect.tryPromise({
120
- try: async () => {
121
- const items = await page.$$(Selectors.serviceList[0]);
122
- for (const item of items) {
123
- const nameEl = await item.$(Selectors.serviceName[0]);
124
- const name = await nameEl?.textContent();
125
- if (name && name.trim().toLowerCase().includes(serviceName.toLowerCase())) {
126
- return item;
127
- }
128
- }
129
- return null;
130
- },
131
- catch: (e) =>
132
- new WizardStepError({
133
- step: 'read-availability',
134
- message: `Error searching services: ${e instanceof Error ? e.message : String(e)}`,
135
- cause: e,
136
- }),
137
- });
138
-
139
- if (!serviceItem) {
140
- return yield* Effect.fail(
141
- new WizardStepError({
142
- step: 'read-availability',
143
- message: `Service "${serviceName}" not found`,
144
- }),
145
- );
146
- }
147
-
148
- // Click "Book" button
149
- const bookBtn = yield* Effect.tryPromise({
150
- try: () => serviceItem.$(Selectors.serviceBookButton[0]),
151
- catch: (e) =>
152
- new WizardStepError({
153
- step: 'read-availability',
154
- message: `Book button error: ${e instanceof Error ? e.message : String(e)}`,
155
- cause: e,
156
- }),
157
- });
158
-
159
- if (!bookBtn) {
160
- return yield* Effect.fail(
161
- new WizardStepError({
162
- step: 'read-availability',
163
- message: `"Book" button not found for "${serviceName}"`,
164
- }),
165
- );
166
- }
167
-
168
- yield* Effect.tryPromise({
169
- try: async () => {
170
- await bookBtn.click();
171
- await page.waitForURL(/\/appointment\/\d+\/calendar\/\d+/, { timeout: 10000 });
172
- },
173
- catch: (e) =>
174
- new WizardStepError({
175
- step: 'read-availability',
176
- message: `Failed to navigate to calendar: ${e instanceof Error ? e.message : String(e)}`,
177
- cause: e,
178
- }),
179
- });
180
-
181
- // Verify appointment type ID if provided
182
- if (expectedId) {
183
- const url = page.url();
184
- const match = url.match(/\/appointment\/(\d+)/);
185
- if (match && match[1] !== expectedId) {
186
- return yield* Effect.fail(
187
- new WizardStepError({
188
- step: 'read-availability',
189
- message: `Expected appointment type ${expectedId} but got ${match[1]}`,
190
- }),
191
- );
192
- }
193
- }
194
- });
195
-
196
- /**
197
- * Read all available (non-disabled) dates from the currently visible calendar month.
198
- */
199
- const readCalendarDates = (page: Page): Effect.Effect<AvailableDateResult[], WizardStepError> =>
200
- Effect.gen(function* () {
201
- // Wait for calendar
202
- yield* resolveSelector(page, Selectors.calendar, 10000).pipe(
203
- Effect.catchTag('SelectorError', () =>
204
- Effect.fail(
205
- new WizardStepError({
206
- step: 'read-availability',
207
- message: 'Calendar did not load',
208
- }),
209
- ),
210
- ),
211
- );
212
-
213
- // Get current month/year from calendar label
214
- const monthInfo = yield* getCalendarMonthInfo(page);
215
-
216
- // Read all non-disabled, non-neighboring-month tiles
217
- const dates = yield* Effect.tryPromise({
218
- try: async () => {
219
- const results: AvailableDateResult[] = [];
220
- const tiles = await page.$$(Selectors.calendarDay[0]);
221
-
222
- for (const tile of tiles) {
223
- const isDisabled = await tile.evaluate((el) => (el as HTMLButtonElement).disabled);
224
- if (isDisabled) continue;
225
-
226
- const classes = (await tile.getAttribute('class')) ?? '';
227
- if (classes.includes('neighboringMonth')) continue;
228
-
229
- const text = await tile.textContent();
230
- const dayNum = parseInt(text?.trim() ?? '', 10);
231
- if (isNaN(dayNum) || dayNum < 1 || dayNum > 31) continue;
232
-
233
- // Build YYYY-MM-DD from month info + day
234
- const dateStr = `${monthInfo.year}-${String(monthInfo.month + 1).padStart(2, '0')}-${String(dayNum).padStart(2, '0')}`;
235
- results.push({ date: dateStr, slots: 1 });
236
- }
237
-
238
- return results;
239
- },
240
- catch: (e) =>
241
- new WizardStepError({
242
- step: 'read-availability',
243
- message: `Error reading calendar tiles: ${e instanceof Error ? e.message : String(e)}`,
244
- cause: e,
245
- }),
246
- });
247
-
248
- return dates;
249
- });
250
-
251
- const MONTH_NAMES = [
252
- 'january', 'february', 'march', 'april', 'may', 'june',
253
- 'july', 'august', 'september', 'october', 'november', 'december',
254
- ];
255
-
256
- /**
257
- * Get the currently displayed month and year from the calendar label.
258
- * Retries up to 3 times with brief waits for React rendering.
259
- */
260
- const getCalendarMonthInfo = (
261
- page: Page,
262
- ): Effect.Effect<{ month: number; year: number }, WizardStepError> =>
263
- Effect.gen(function* () {
264
- // Wait for calendar month label to appear
265
- yield* Effect.tryPromise({
266
- try: () => page.waitForSelector(Selectors.calendarMonth[0], { timeout: 5000 }),
267
- catch: () => null,
268
- }).pipe(Effect.orElseSucceed(() => null));
269
-
270
- // Retry up to 3 times — React may still be rendering
271
- for (let retry = 0; retry < 3; retry++) {
272
- const info = yield* Effect.tryPromise({
273
- try: async () => {
274
- for (const selector of Selectors.calendarMonth) {
275
- const text = await page.$eval(selector, (el) => el.textContent?.trim() ?? null).catch(() => null);
276
- if (text) {
277
- // Try "March 2026" or "March\n2026" or "March2026" (nested spans)
278
- const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
279
- if (match) {
280
- const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
281
- if (monthIndex >= 0) {
282
- return { month: monthIndex, year: parseInt(match[2], 10) };
283
- }
284
- }
285
- }
286
- }
287
- // Also try innerText which resolves visibility better than textContent
288
- for (const selector of Selectors.calendarMonth) {
289
- const text = await page.$eval(selector, (el) => (el as HTMLElement).innerText?.trim() ?? null).catch(() => null);
290
- if (text) {
291
- const match = text.match(/([A-Za-z]+)\s*(\d{4})/);
292
- if (match) {
293
- const monthIndex = MONTH_NAMES.indexOf(match[1].toLowerCase());
294
- if (monthIndex >= 0) {
295
- return { month: monthIndex, year: parseInt(match[2], 10) };
296
- }
297
- }
298
- }
299
- }
300
- return null;
301
- },
302
- catch: () => null,
303
- }).pipe(Effect.orElseSucceed(() => null));
304
-
305
- if (info) return info;
306
-
307
- // Wait before retrying
308
- yield* Effect.tryPromise({
309
- try: () => page.waitForTimeout(1000),
310
- catch: () => null,
311
- }).pipe(Effect.orElseSucceed(() => null));
312
- }
313
-
314
- return yield* Effect.fail(
315
- new WizardStepError({
316
- step: 'read-availability',
317
- message: 'Could not determine calendar month after 3 retries',
318
- }),
319
- );
320
- });
321
-
322
- /**
323
- * Navigate the calendar to a specific target month (YYYY-MM format).
324
- */
325
- const navigateToMonth = (page: Page, targetMonth: string): Effect.Effect<void, WizardStepError> =>
326
- Effect.gen(function* () {
327
- const [yearStr, monthStr] = targetMonth.split('-');
328
- const targetYear = parseInt(yearStr, 10);
329
- const targetMonthIdx = parseInt(monthStr, 10) - 1;
330
-
331
- for (let i = 0; i < 12; i++) {
332
- const current = yield* getCalendarMonthInfo(page);
333
- if (current.month === targetMonthIdx && current.year === targetYear) return;
334
-
335
- const currentFirst = new Date(current.year, current.month, 1);
336
- const targetFirst = new Date(targetYear, targetMonthIdx, 1);
337
- const direction = targetFirst > currentFirst ? 'next' : 'prev';
338
- const selectors = direction === 'prev' ? Selectors.calendarPrev : Selectors.calendarNext;
339
-
340
- const btn = yield* resolveSelector(page, selectors, 3000).pipe(
341
- Effect.catchTag('SelectorError', () =>
342
- Effect.fail(
343
- new WizardStepError({
344
- step: 'read-availability',
345
- message: `Calendar ${direction} button not found`,
346
- }),
347
- ),
348
- ),
349
- );
350
-
351
- yield* Effect.tryPromise({
352
- try: async () => {
353
- await btn.element.click();
354
- await page.waitForTimeout(500);
355
- },
356
- catch: (e) =>
357
- new WizardStepError({
358
- step: 'read-availability',
359
- message: `Calendar nav failed: ${e instanceof Error ? e.message : String(e)}`,
360
- cause: e,
361
- }),
362
- });
363
- }
364
- });
365
-
366
- /**
367
- * Advance to the next month. Returns false if next button is not available.
368
- */
369
- const advanceMonth = (page: Page): Effect.Effect<boolean, WizardStepError> =>
370
- Effect.gen(function* () {
371
- const btn = yield* Effect.tryPromise({
372
- try: () => page.$(Selectors.calendarNext[0]),
373
- catch: () => null,
374
- }).pipe(Effect.orElseSucceed(() => null));
375
-
376
- if (!btn) return false;
377
-
378
- const isDisabled = yield* Effect.tryPromise({
379
- try: () => btn.evaluate((el) => (el as HTMLButtonElement).disabled),
380
- catch: () => true,
381
- }).pipe(Effect.orElseSucceed(() => true));
382
-
383
- if (isDisabled) return false;
384
-
385
- yield* Effect.tryPromise({
386
- try: async () => {
387
- await btn.click();
388
- await page.waitForTimeout(500);
389
- },
390
- catch: (e) =>
391
- new WizardStepError({
392
- step: 'read-availability',
393
- message: `Failed to advance month: ${e instanceof Error ? e.message : String(e)}`,
394
- cause: e,
395
- }),
396
- });
397
-
398
- return true;
399
- });