import { Result } from '../../../shared/result/Result'; import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice'; import { CheckoutState } from '../../../domain/value-objects/CheckoutState'; import { CheckoutInfo } from '../../../application/ports/ICheckoutService'; interface Page { locator(selector: string): Locator; } interface Locator { getAttribute(name: string): Promise; innerHTML(): Promise; textContent(): Promise; } export class CheckoutPriceExtractor { private readonly selector = '.wizard-footer a.btn:has(span.label-pill)'; constructor(private readonly page: Page) {} async extractCheckoutInfo(): Promise> { try { // Prefer the explicit pill element which contains the price const pillLocator = this.page.locator('span.label-pill'); const pillText = await pillLocator.first().textContent().catch(() => null); let price: CheckoutPrice | null = null; let state = CheckoutState.unknown(); let buttonHtml = ''; if (pillText) { // Parse price if possible try { price = CheckoutPrice.fromString(pillText.trim()); } catch { price = null; } // Try to find the containing button and its classes/html // Primary: locate button via known selector that contains the pill const buttonLocator = this.page.locator(this.selector).first(); let classes = await buttonLocator.getAttribute('class').catch(() => null); let html = await buttonLocator.innerHTML().catch(() => ''); if (!classes) { // Fallback: find ancestor of the pill (XPath) const ancestorButton = pillLocator.first().locator('xpath=ancestor::a[1]'); classes = await ancestorButton.getAttribute('class').catch(() => null); html = await ancestorButton.innerHTML().catch(() => ''); } if (classes) { state = CheckoutState.fromButtonClasses(classes); buttonHtml = html ?? ''; } } else { // No pill found — attempt to read button directly (best-effort) const buttonLocator = this.page.locator(this.selector).first(); const classes = await buttonLocator.getAttribute('class').catch(() => null); const html = await buttonLocator.innerHTML().catch(() => ''); if (classes) { state = CheckoutState.fromButtonClasses(classes); buttonHtml = html ?? ''; } } // Additional fallback: search the wizard-footer for any price text if pill was not present or parsing failed if (!price) { try { const footerLocator = this.page.locator('.wizard-footer').first(); const footerText = await footerLocator.textContent().catch(() => null); if (footerText) { const match = footerText.match(/\$\d+\.\d{2}/); if (match) { try { price = CheckoutPrice.fromString(match[0]); } catch { price = null; } } } } catch { // ignore footer parse errors } } return Result.ok({ price, state, buttonHtml }); } catch (error) { // On any unexpected error, return an "unknown" result (do not throw) return Result.ok({ price: null, state: CheckoutState.unknown(), buttonHtml: '' }); } } }