@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,54 @@
1
+ /**
2
+ * Wizard Step: Bypass Payment
3
+ *
4
+ * Applies a 100% gift certificate code on Acuity's payment page to bypass
5
+ * the credit card requirement. This allows the booking to complete at $0,
6
+ * since actual payment is handled by our Venmo/Cash adapters.
7
+ *
8
+ * Strategy: A pre-configured gift certificate in Acuity admin covers the full amount.
9
+ * The certificate code is passed as ACUITY_BYPASS_COUPON env var.
10
+ *
11
+ * Acuity's payment page coupon flow (verified 2026-02-26):
12
+ * 1. Page is at URL .../datetime/<ISO>/payment
13
+ * 2. Click "Package, gift, or coupon code" toggle to expand the coupon section
14
+ * 3. Enter the gift certificate code in the "Enter code" input
15
+ * 4. Click "Apply" to validate the code
16
+ * 5. Acuity calls POST /api/scheduling/v1/appointments/order-summary
17
+ * with certificateCode in the body; response includes discount and total
18
+ * 6. If successful: order summary shows "Gift certificate [CODE] -$X.XX"
19
+ * and total drops to $0.00
20
+ * 7. "PAY & CONFIRM" button can now be clicked without entering card details
21
+ *
22
+ * Note: There IS a separate payment page (URL ends in /payment).
23
+ * The "Check Code Balance" modal on the client form is INFORMATIONAL ONLY.
24
+ */
25
+ import { Effect } from 'effect';
26
+ import { BrowserService } from '../browser-service.js';
27
+ import { CouponError } from '../errors.js';
28
+ export interface BypassPaymentResult {
29
+ readonly couponApplied: boolean;
30
+ readonly code: string;
31
+ readonly totalAfterCoupon: string | null;
32
+ }
33
+ /**
34
+ * Apply a gift certificate code on the payment page to bypass card entry.
35
+ *
36
+ * Prerequisite: The wizard must already be on the payment page
37
+ * (URL contains /payment). Call after fillFormFields + advancePastForm.
38
+ *
39
+ * Flow: Expand "Package, gift, or coupon code" → enter code → click "Apply"
40
+ */
41
+ export declare const bypassPayment: (couponCode: string) => Effect.Effect<{
42
+ couponApplied: boolean;
43
+ code: string;
44
+ totalAfterCoupon: string | null;
45
+ }, import("../errors.js").BrowserError | CouponError, import("effect/Scope").Scope | BrowserService>;
46
+ /**
47
+ * Generate a unique coupon code for a payment reference.
48
+ * Format: ALT-{PROCESSOR}-{SHORT_REF}
49
+ *
50
+ * Note: For MVP, we use a single reusable coupon code from env.
51
+ * This function is here for future per-transaction coupon support.
52
+ */
53
+ export declare const generateCouponCode: (_paymentRef: string, _processor: string, envCouponCode?: string) => string;
54
+ //# sourceMappingURL=bypass-payment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass-payment.d.ts","sourceRoot":"","sources":["../../../src/middleware/steps/bypass-payment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAO3C,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM;;;;oGAiJ7C,CAAC;AAMJ;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC9B,aAAa,MAAM,EACnB,YAAY,MAAM,EAClB,gBAAgB,MAAM,KACpB,MAUF,CAAC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Wizard Step: Bypass Payment
3
+ *
4
+ * Applies a 100% gift certificate code on Acuity's payment page to bypass
5
+ * the credit card requirement. This allows the booking to complete at $0,
6
+ * since actual payment is handled by our Venmo/Cash adapters.
7
+ *
8
+ * Strategy: A pre-configured gift certificate in Acuity admin covers the full amount.
9
+ * The certificate code is passed as ACUITY_BYPASS_COUPON env var.
10
+ *
11
+ * Acuity's payment page coupon flow (verified 2026-02-26):
12
+ * 1. Page is at URL .../datetime/<ISO>/payment
13
+ * 2. Click "Package, gift, or coupon code" toggle to expand the coupon section
14
+ * 3. Enter the gift certificate code in the "Enter code" input
15
+ * 4. Click "Apply" to validate the code
16
+ * 5. Acuity calls POST /api/scheduling/v1/appointments/order-summary
17
+ * with certificateCode in the body; response includes discount and total
18
+ * 6. If successful: order summary shows "Gift certificate [CODE] -$X.XX"
19
+ * and total drops to $0.00
20
+ * 7. "PAY & CONFIRM" button can now be clicked without entering card details
21
+ *
22
+ * Note: There IS a separate payment page (URL ends in /payment).
23
+ * The "Check Code Balance" modal on the client form is INFORMATIONAL ONLY.
24
+ */
25
+ import { Effect } from 'effect';
26
+ import { BrowserService } from '../browser-service.js';
27
+ import { CouponError } from '../errors.js';
28
+ import { resolveSelector, Selectors } from '../selectors.js';
29
+ // =============================================================================
30
+ // IMPLEMENTATION
31
+ // =============================================================================
32
+ /**
33
+ * Apply a gift certificate code on the payment page to bypass card entry.
34
+ *
35
+ * Prerequisite: The wizard must already be on the payment page
36
+ * (URL contains /payment). Call after fillFormFields + advancePastForm.
37
+ *
38
+ * Flow: Expand "Package, gift, or coupon code" → enter code → click "Apply"
39
+ */
40
+ export const bypassPayment = (couponCode) => Effect.gen(function* () {
41
+ const { acquirePage } = yield* BrowserService;
42
+ const page = yield* acquirePage;
43
+ // Verify we're on the payment page
44
+ const url = page.url();
45
+ if (!url.includes('/payment')) {
46
+ return yield* Effect.fail(new CouponError({
47
+ code: couponCode,
48
+ message: `Not on payment page (URL: ${url}). ` +
49
+ 'The wizard must advance past the client form first.',
50
+ }));
51
+ }
52
+ // Step 1: Click "Package, gift, or coupon code" to expand the coupon section
53
+ const couponToggle = yield* resolveSelector(page, Selectors.paymentCouponToggle, 10000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new CouponError({
54
+ code: couponCode,
55
+ message: '"Package, gift, or coupon code" toggle not found on payment page.',
56
+ }))));
57
+ yield* Effect.tryPromise({
58
+ try: async () => {
59
+ await couponToggle.element.click();
60
+ // Wait for the coupon input to appear after expansion
61
+ await page.waitForSelector('input[placeholder="Enter code"]', { timeout: 5000 });
62
+ },
63
+ catch: (e) => new CouponError({
64
+ code: couponCode,
65
+ message: `Failed to expand coupon section: ${e instanceof Error ? e.message : String(e)}`,
66
+ }),
67
+ });
68
+ // Step 2: Enter the gift certificate code
69
+ const couponInput = yield* resolveSelector(page, Selectors.paymentCouponInput, 5000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new CouponError({
70
+ code: couponCode,
71
+ message: 'Coupon code input not found after expanding section',
72
+ }))));
73
+ yield* Effect.tryPromise({
74
+ try: async () => {
75
+ await couponInput.element.click();
76
+ await couponInput.element.fill(couponCode);
77
+ },
78
+ catch: (e) => new CouponError({
79
+ code: couponCode,
80
+ message: `Failed to enter coupon code: ${e instanceof Error ? e.message : String(e)}`,
81
+ }),
82
+ });
83
+ // Step 3: Click "Apply" to validate the code
84
+ const applyBtn = yield* resolveSelector(page, Selectors.paymentCouponApply, 5000).pipe(Effect.catchTag('SelectorError', () => Effect.fail(new CouponError({
85
+ code: couponCode,
86
+ message: '"Apply" button not found in coupon section',
87
+ }))));
88
+ yield* Effect.tryPromise({
89
+ try: () => applyBtn.element.click(),
90
+ catch: (e) => new CouponError({
91
+ code: couponCode,
92
+ message: `Failed to click "Apply": ${e instanceof Error ? e.message : String(e)}`,
93
+ }),
94
+ });
95
+ // Step 4: Wait for the order-summary API response
96
+ // Acuity calls POST /api/scheduling/v1/appointments/order-summary
97
+ // with certificateCode in the body.
98
+ yield* Effect.tryPromise({
99
+ try: () => page.waitForTimeout(3000),
100
+ catch: () => new CouponError({ code: couponCode, message: 'Timeout waiting for coupon validation' }),
101
+ });
102
+ // Step 5: Check if the coupon was applied
103
+ // On success: "Gift certificate [CODE]" and "-$X.XX" appear in order summary
104
+ // On error: Acuity may show an error message or the total remains unchanged
105
+ const result = yield* Effect.tryPromise({
106
+ try: async () => {
107
+ const bodyText = await page.evaluate(() => document.body.textContent ?? '');
108
+ const hasGiftCert = bodyText.includes('Gift certificate') && bodyText.includes(couponCode);
109
+ const hasDiscount = bodyText.includes('-$');
110
+ const totalMatch = bodyText.match(/Total\s*\$?([\d.]+)/);
111
+ const total = totalMatch ? totalMatch[1] : null;
112
+ return { hasGiftCert, hasDiscount, total };
113
+ },
114
+ catch: () => ({ hasGiftCert: false, hasDiscount: false, total: null }),
115
+ }).pipe(Effect.orElseSucceed(() => ({ hasGiftCert: false, hasDiscount: false, total: null })));
116
+ if (!result.hasGiftCert) {
117
+ // Check for error indicators
118
+ const errorText = yield* Effect.tryPromise({
119
+ try: async () => {
120
+ const errs = [];
121
+ const errEls = await page.$$('[class*="error"], [role="alert"]');
122
+ for (const el of errEls) {
123
+ const text = await el.textContent().catch(() => null);
124
+ if (text && text.trim().length > 0)
125
+ errs.push(text.trim());
126
+ }
127
+ return errs.join('; ') || null;
128
+ },
129
+ catch: () => null,
130
+ }).pipe(Effect.orElseSucceed(() => null));
131
+ if (errorText) {
132
+ return yield* Effect.fail(new CouponError({
133
+ code: couponCode,
134
+ message: `Coupon rejected: ${errorText}`,
135
+ }));
136
+ }
137
+ }
138
+ const totalAfterCoupon = result.total ? `$${result.total}` : null;
139
+ return {
140
+ couponApplied: result.hasGiftCert && result.hasDiscount,
141
+ code: couponCode,
142
+ totalAfterCoupon,
143
+ };
144
+ });
145
+ // =============================================================================
146
+ // HELPERS
147
+ // =============================================================================
148
+ /**
149
+ * Generate a unique coupon code for a payment reference.
150
+ * Format: ALT-{PROCESSOR}-{SHORT_REF}
151
+ *
152
+ * Note: For MVP, we use a single reusable coupon code from env.
153
+ * This function is here for future per-transaction coupon support.
154
+ */
155
+ export const generateCouponCode = (_paymentRef, _processor, envCouponCode) => {
156
+ // MVP: Use pre-configured reusable coupon
157
+ if (envCouponCode)
158
+ return envCouponCode;
159
+ // Future: Generate per-transaction code
160
+ // return `ALT-${processor.toUpperCase()}-${paymentRef.slice(0, 8)}`;
161
+ throw new Error('ACUITY_BYPASS_COUPON environment variable is required. ' +
162
+ 'Create a 100% gift certificate in Acuity admin and set this env var.');
163
+ };
164
+ //# sourceMappingURL=bypass-payment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass-payment.js","sourceRoot":"","sources":["../../../src/middleware/steps/bypass-payment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Extract BUSINESS Object from Acuity React SPA
3
+ *
4
+ * Acuity's scheduling page (Pylon/Squarespace) embeds a `BUSINESS`
5
+ * JavaScript variable in the initial HTML containing the full service
6
+ * catalog, calendars, forms, products, and configuration.
7
+ *
8
+ * This approach is far more reliable than DOM scraping because:
9
+ * - The data is present in the raw HTML before React hydrates
10
+ * - No CSS selectors needed (immune to UI redesigns)
11
+ * - Contains Acuity's canonical numeric IDs for all services
12
+ * - Works even without a browser (plain HTTP fetch + regex)
13
+ *
14
+ * Two extraction modes:
15
+ * - `extractBusinessFromPage()` — Playwright page.evaluate (browser context)
16
+ * - `extractBusinessFromHtml()` — Regex on raw HTML (no browser needed)
17
+ */
18
+ import { Effect } from 'effect';
19
+ import { BrowserService } from '../browser-service.js';
20
+ import { WizardStepError } from '../errors.js';
21
+ import type { Service } from '../../core/types.js';
22
+ /** A single appointment type from Acuity's BUSINESS.appointmentTypes */
23
+ export interface AcuityAppointmentType {
24
+ readonly id: number;
25
+ readonly name: string;
26
+ readonly active: boolean;
27
+ readonly description: string;
28
+ readonly duration: number;
29
+ readonly price: string;
30
+ readonly category: string;
31
+ readonly color: string;
32
+ readonly private: boolean;
33
+ readonly type: string;
34
+ readonly calendarIDs: readonly number[];
35
+ readonly formIDs: readonly number[];
36
+ readonly addonIDs: readonly number[];
37
+ readonly paddingAfter: number;
38
+ readonly paddingBefore: number;
39
+ readonly paymentRequired: boolean;
40
+ readonly classSize: number | null;
41
+ }
42
+ /** A calendar (provider) from BUSINESS.calendars */
43
+ export interface AcuityCalendar {
44
+ readonly id: number;
45
+ readonly name: string;
46
+ readonly description: string;
47
+ readonly location: string;
48
+ readonly timezone: string;
49
+ readonly thumbnail: string;
50
+ readonly image: string;
51
+ }
52
+ /** Subset of BUSINESS we care about */
53
+ export interface AcuityBusinessData {
54
+ readonly id: number;
55
+ readonly ownerKey: string;
56
+ readonly name: string;
57
+ readonly timezone: string;
58
+ readonly appointmentTypes: Record<string, AcuityAppointmentType[]>;
59
+ readonly calendars: Record<string, AcuityCalendar[]>;
60
+ readonly products: Record<string, unknown[]>;
61
+ readonly forms: unknown[];
62
+ readonly addons: unknown[];
63
+ }
64
+ /**
65
+ * Extract the BUSINESS object from the Acuity page using Playwright.
66
+ * Loads the page and evaluates `window.BUSINESS` in the page context.
67
+ */
68
+ export declare const extractBusinessFromPage: Effect.Effect<AcuityBusinessData, import("../errors.js").BrowserError | WizardStepError, import("effect/Scope").Scope | BrowserService>;
69
+ /**
70
+ * Extract the BUSINESS object from raw HTML using regex.
71
+ * Works without a browser — suitable for lightweight health checks.
72
+ */
73
+ export declare const extractBusinessFromHtml: (html: string) => AcuityBusinessData | null;
74
+ /**
75
+ * Fetch the Acuity page via HTTP and extract the BUSINESS object.
76
+ * No browser needed — uses plain fetch + regex.
77
+ */
78
+ export declare const fetchBusinessData: (baseUrl: string) => Promise<AcuityBusinessData | null>;
79
+ /**
80
+ * Convert Acuity's BUSINESS.appointmentTypes to our Service[] format.
81
+ * Flattens the category-grouped structure and filters to active services.
82
+ *
83
+ * Critical: Uses Acuity's numeric ID (e.g. 53178494) as the service ID.
84
+ * Our internal IDs (e.g. 'urgent-pain-relief') are PG-side only.
85
+ * The wizard/scraper/availability endpoints need Acuity's numeric IDs.
86
+ */
87
+ export declare const businessToServices: (business: AcuityBusinessData) => Service[];
88
+ /**
89
+ * Effect program: extract services from BUSINESS object via browser.
90
+ * Combines extractBusinessFromPage + businessToServices.
91
+ */
92
+ export declare const extractBusinessServices: Effect.Effect<Service[], import("../errors.js").BrowserError | WizardStepError, import("effect/Scope").Scope | BrowserService>;
93
+ //# sourceMappingURL=extract-business.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-business.d.ts","sourceRoot":"","sources":["../../../src/middleware/steps/extract-business.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,wEAAwE;AACxE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;CAC3B;AAMD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,yIAgDlC,CAAC;AAMH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,kBAAkB,GAAG,IA0B3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAc1F,CAAC;AAMF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAU,kBAAkB,KAAG,OAAO,EAoBxE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,gIAWlC,CAAC"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Extract BUSINESS Object from Acuity React SPA
3
+ *
4
+ * Acuity's scheduling page (Pylon/Squarespace) embeds a `BUSINESS`
5
+ * JavaScript variable in the initial HTML containing the full service
6
+ * catalog, calendars, forms, products, and configuration.
7
+ *
8
+ * This approach is far more reliable than DOM scraping because:
9
+ * - The data is present in the raw HTML before React hydrates
10
+ * - No CSS selectors needed (immune to UI redesigns)
11
+ * - Contains Acuity's canonical numeric IDs for all services
12
+ * - Works even without a browser (plain HTTP fetch + regex)
13
+ *
14
+ * Two extraction modes:
15
+ * - `extractBusinessFromPage()` — Playwright page.evaluate (browser context)
16
+ * - `extractBusinessFromHtml()` — Regex on raw HTML (no browser needed)
17
+ */
18
+ import { Effect } from 'effect';
19
+ import { BrowserService } from '../browser-service.js';
20
+ import { WizardStepError } from '../errors.js';
21
+ // =============================================================================
22
+ // EXTRACTION — Browser context (Effect program)
23
+ // =============================================================================
24
+ /**
25
+ * Extract the BUSINESS object from the Acuity page using Playwright.
26
+ * Loads the page and evaluates `window.BUSINESS` in the page context.
27
+ */
28
+ export const extractBusinessFromPage = Effect.gen(function* () {
29
+ const { acquirePage, config } = yield* BrowserService;
30
+ const page = yield* acquirePage;
31
+ yield* Effect.tryPromise({
32
+ try: () => page.goto(config.baseUrl, { waitUntil: 'domcontentloaded', timeout: config.timeout }),
33
+ catch: (e) => new WizardStepError({
34
+ step: 'extract-business',
35
+ message: `Failed to load Acuity page: ${e instanceof Error ? e.message : String(e)}`,
36
+ }),
37
+ });
38
+ const business = yield* Effect.tryPromise({
39
+ try: () => page.evaluate(() => {
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const b = window.BUSINESS;
42
+ if (!b)
43
+ return null;
44
+ return {
45
+ id: b.id,
46
+ ownerKey: b.ownerKey,
47
+ name: b.name,
48
+ timezone: b.timezone,
49
+ appointmentTypes: b.appointmentTypes,
50
+ calendars: b.calendars,
51
+ products: b.products,
52
+ forms: b.forms,
53
+ addons: b.addons,
54
+ };
55
+ }),
56
+ catch: (e) => new WizardStepError({
57
+ step: 'extract-business',
58
+ message: `Failed to evaluate BUSINESS: ${e instanceof Error ? e.message : String(e)}`,
59
+ }),
60
+ });
61
+ if (!business) {
62
+ return yield* Effect.fail(new WizardStepError({
63
+ step: 'extract-business',
64
+ message: 'window.BUSINESS not found on Acuity page — page structure may have changed',
65
+ }));
66
+ }
67
+ return business;
68
+ });
69
+ // =============================================================================
70
+ // EXTRACTION — Plain HTTP (no browser needed)
71
+ // =============================================================================
72
+ /**
73
+ * Extract the BUSINESS object from raw HTML using regex.
74
+ * Works without a browser — suitable for lightweight health checks.
75
+ */
76
+ export const extractBusinessFromHtml = (html) => {
77
+ // The BUSINESS object is assigned as: var BUSINESS = {...};
78
+ // It's a large JSON-like object. We find the assignment and parse it.
79
+ const match = html.match(/var\s+BUSINESS\s*=\s*(\{[\s\S]*?\});\s*(?:var\s|$)/);
80
+ if (!match)
81
+ return null;
82
+ try {
83
+ // The object may contain single-quoted strings or unquoted keys,
84
+ // but Acuity outputs valid JSON-like syntax. Try parsing directly.
85
+ const parsed = JSON.parse(match[1]);
86
+ return {
87
+ id: parsed.id,
88
+ ownerKey: parsed.ownerKey,
89
+ name: parsed.name,
90
+ timezone: parsed.timezone,
91
+ appointmentTypes: parsed.appointmentTypes ?? {},
92
+ calendars: parsed.calendars ?? {},
93
+ products: parsed.products ?? {},
94
+ forms: parsed.forms ?? [],
95
+ addons: parsed.addons ?? [],
96
+ };
97
+ }
98
+ catch {
99
+ // If JSON.parse fails, the page may use non-standard JS syntax.
100
+ // Fall back to eval in a browser context (caller should use extractBusinessFromPage).
101
+ return null;
102
+ }
103
+ };
104
+ /**
105
+ * Fetch the Acuity page via HTTP and extract the BUSINESS object.
106
+ * No browser needed — uses plain fetch + regex.
107
+ */
108
+ export const fetchBusinessData = async (baseUrl) => {
109
+ try {
110
+ const response = await fetch(baseUrl, {
111
+ headers: {
112
+ 'User-Agent': 'Mozilla/5.0 (compatible; scheduling-kit/1.0)',
113
+ },
114
+ redirect: 'follow',
115
+ });
116
+ if (!response.ok)
117
+ return null;
118
+ const html = await response.text();
119
+ return extractBusinessFromHtml(html);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ };
125
+ // =============================================================================
126
+ // TRANSFORM — BUSINESS → Service[]
127
+ // =============================================================================
128
+ /**
129
+ * Convert Acuity's BUSINESS.appointmentTypes to our Service[] format.
130
+ * Flattens the category-grouped structure and filters to active services.
131
+ *
132
+ * Critical: Uses Acuity's numeric ID (e.g. 53178494) as the service ID.
133
+ * Our internal IDs (e.g. 'urgent-pain-relief') are PG-side only.
134
+ * The wizard/scraper/availability endpoints need Acuity's numeric IDs.
135
+ */
136
+ export const businessToServices = (business) => {
137
+ const services = [];
138
+ for (const [category, types] of Object.entries(business.appointmentTypes)) {
139
+ for (const apt of types) {
140
+ if (!apt.active || apt.private)
141
+ continue;
142
+ services.push({
143
+ id: String(apt.id),
144
+ name: apt.name,
145
+ duration: apt.duration,
146
+ price: Math.round(parseFloat(apt.price) * 100), // "155.00" → 15500 cents
147
+ currency: 'USD',
148
+ category: category.replace(/^\d+\.?\s*/, ''), // Strip numeric prefix: "2 TMD" → "TMD"
149
+ active: true,
150
+ });
151
+ }
152
+ }
153
+ return services;
154
+ };
155
+ /**
156
+ * Effect program: extract services from BUSINESS object via browser.
157
+ * Combines extractBusinessFromPage + businessToServices.
158
+ */
159
+ export const extractBusinessServices = Effect.gen(function* () {
160
+ const business = yield* extractBusinessFromPage;
161
+ const services = businessToServices(business);
162
+ if (services.length === 0) {
163
+ console.warn('[extract-business] BUSINESS object found but contained 0 active services');
164
+ }
165
+ else {
166
+ console.log(`[extract-business] Extracted ${services.length} services from BUSINESS object`);
167
+ }
168
+ return services;
169
+ });
170
+ //# sourceMappingURL=extract-business.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-business.js","sourceRoot":"","sources":["../../../src/middleware/steps/extract-business.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAoD/C,gFAAgF;AAChF,gDAAgD;AAChD,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC1D,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC;IACtD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC;IAEhC,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QACxB,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QAChG,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACZ,IAAI,eAAe,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,+BAA+B,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;SACpF,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QACzC,GAAG,EAAE,GAAG,EAAE,CACT,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YAClB,8DAA8D;YAC9D,MAAM,CAAC,GAAI,MAAc,CAAC,QAAQ,CAAC;YACnC,IAAI,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpB,OAAO;gBACN,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,gBAAgB,EAAE,CAAC,CAAC,gBAAgB;gBACpC,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,MAAM,EAAE,CAAC,CAAC,MAAM;aAChB,CAAC;QACH,CAAC,CAAC;QACH,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CACZ,IAAI,eAAe,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,gCAAgC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;SACrF,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CACxB,IAAI,eAAe,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,4EAA4E;SACrF,CAAC,CACF,CAAC;IACH,CAAC;IAED,OAAO,QAA8B,CAAC;AACvC,CAAC,CAAC,CAAC;AAEH,gFAAgF;AAChF,8CAA8C;AAC9C,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,IAAY,EAA6B,EAAE;IAClF,4DAA4D;IAC5D,sEAAsE;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;IAC/E,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC;QACJ,iEAAiE;QACjE,mEAAmE;QACnE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,OAAO;YACN,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,IAAI,EAAE;YAC/C,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;YACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,EAAE;YAC/B,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;SAC3B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,gEAAgE;QAChE,sFAAsF;QACtF,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,OAAe,EAAsC,EAAE;IAC9F,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE;YACrC,OAAO,EAAE;gBACR,YAAY,EAAE,8CAA8C;aAC5D;YACD,QAAQ,EAAE,QAAQ;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC,CAAC;AAEF,gFAAgF;AAChF,mCAAmC;AACnC,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,QAA4B,EAAa,EAAE;IAC7E,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC3E,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO;gBAAE,SAAS;YAEzC,QAAQ,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,EAAE,yBAAyB;gBACzE,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,wCAAwC;gBACtF,MAAM,EAAE,IAAI;aACZ,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,uBAAuB,CAAC;IAChD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;IAC1F,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,gCAAgC,QAAQ,CAAC,MAAM,gCAAgC,CAAC,CAAC;IAC9F,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Wizard Step: Extract Confirmation Data
3
+ *
4
+ * Reads the confirmation page to extract booking details:
5
+ * appointment ID, confirmation code, service, date/time, provider.
6
+ * Maps the scraped data to the Booking type.
7
+ */
8
+ import { Effect } from 'effect';
9
+ import { BrowserService } from '../browser-service.js';
10
+ import { WizardStepError } from '../errors.js';
11
+ import type { Booking, BookingRequest } from '../../core/types.js';
12
+ export interface ConfirmationData {
13
+ readonly appointmentId: string | null;
14
+ readonly confirmationCode: string | null;
15
+ readonly serviceName: string | null;
16
+ readonly datetime: string | null;
17
+ readonly providerName: string | null;
18
+ readonly rawText: string;
19
+ }
20
+ /**
21
+ * Extract booking confirmation data from the current page.
22
+ * Assumes we're already on the confirmation page.
23
+ */
24
+ export declare const extractConfirmation: () => Effect.Effect<{
25
+ appointmentId: string | null;
26
+ confirmationCode: string | null;
27
+ serviceName: string | null;
28
+ datetime: string | null;
29
+ providerName: null;
30
+ rawText: string;
31
+ }, import("../errors.js").BrowserError | WizardStepError, import("effect/Scope").Scope | BrowserService>;
32
+ /**
33
+ * Map extracted confirmation data + original request into a Booking object.
34
+ */
35
+ export declare const toBooking: (confirmation: ConfirmationData, request: BookingRequest, paymentRef: string, paymentProcessor: string, service?: {
36
+ name: string;
37
+ duration: number;
38
+ price: number;
39
+ currency: string;
40
+ }) => Booking;
41
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../../src/middleware/steps/extract.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;AAE/C,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAM/E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAMD;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;wGAyD7B,CAAC;AAMJ;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,cAAc,gBAAgB,EAC9B,SAAS,cAAc,EACvB,YAAY,MAAM,EAClB,kBAAkB,MAAM,EACxB,UAAU;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC3E,OAiBD,CAAC"}