102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
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<string | null>;
|
|
innerHTML(): Promise<string>;
|
|
textContent(): Promise<string | null>;
|
|
}
|
|
|
|
export class CheckoutPriceExtractor {
|
|
private readonly selector = '.wizard-footer a.btn:has(span.label-pill)';
|
|
|
|
constructor(private readonly page: Page) {}
|
|
|
|
async extractCheckoutInfo(): Promise<Result<CheckoutInfo>> {
|
|
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 <a> 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: ''
|
|
});
|
|
}
|
|
}
|
|
} |