Files
PHANTOM/e2e/lib/api-client.ts
Claude c8ac2cb609 Add dynamic pricing E2E test suite with Playwright
Implement comprehensive E2E tests to validate the surge pricing pipeline:
- Test SimpleSurgePricer with configurable thresholds (high=3, surge=1.5x)
- Verify discount pricing when demand is below low_threshold
- Test multi-product differential pricing based on demand signals
- Validate price propagation from pipeline through Redis to API

Test infrastructure:
- Playwright configuration with custom fixtures
- Python pipeline worker for direct test execution (bypasses Airflow)
- API client for event ingestion and price verification
- Event generator for creating realistic interaction sequences
- docker-compose.e2e.yml with minimal services for testing
2025-12-26 09:35:07 +00:00

192 lines
4.8 KiB
TypeScript

import { testConfig } from '../playwright.config';
/**
* Event payload structure matching the backend API
*/
export interface EventPayload {
sessionId: string;
experimentId?: string;
eventName: string;
page: string;
productId?: string;
metadata?: Record<string, unknown>;
storeMode: 'hotel' | 'airline';
userAgent?: string;
ts?: string;
}
/**
* Price log payload structure
*/
export interface PriceLogPayload {
productId: string;
price: number;
sessionId: string;
experimentId?: string;
storeMode: 'hotel' | 'airline';
ts?: string;
}
/**
* Price response from the pricing provider
*/
export interface PriceResponse {
productId: string;
price: number;
base_price: number;
markup: number;
elasticity: number | null;
model_version: string;
}
/**
* API client for interacting with PHANTOM services
*/
export class PhantomApiClient {
private backendUrl: string;
private providerUrl: string;
constructor(
backendUrl: string = testConfig.backendUrl,
providerUrl: string = testConfig.providerUrl
) {
this.backendUrl = backendUrl;
this.providerUrl = providerUrl;
}
/**
* Send a user interaction event to the ingestion API
*/
async ingestEvent(event: EventPayload): Promise<{ success: boolean }> {
const payload: EventPayload = {
...event,
ts: event.ts || new Date().toISOString(),
};
const response = await fetch(`${this.backendUrl}/api/kafka/ingest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to ingest event: ${response.status} ${await response.text()}`);
}
return response.json();
}
/**
* Send multiple events in rapid succession
*/
async ingestEvents(events: EventPayload[], delayMs: number = 100): Promise<void> {
for (const event of events) {
await this.ingestEvent(event);
if (delayMs > 0) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
/**
* Log a price observation
*/
async logPrice(priceLog: PriceLogPayload): Promise<{ success: boolean }> {
const payload: PriceLogPayload = {
...priceLog,
ts: priceLog.ts || new Date().toISOString(),
};
const response = await fetch(`${this.backendUrl}/api/kafka/price-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to log price: ${response.status} ${await response.text()}`);
}
return response.json();
}
/**
* Get the current price for a product from the pricing provider
*/
async getPrice(
mode: 'hotel' | 'airline',
productId: string,
sessionId?: string
): Promise<PriceResponse> {
const params = new URLSearchParams();
if (sessionId) {
params.set('sessionId', sessionId);
}
const url = `${this.providerUrl}/api/${mode}/price/${productId}${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to get price: ${response.status} ${await response.text()}`);
}
return response.json();
}
/**
* Dump events from Kafka topic for debugging
*/
async dumpKafkaEvents(
topic: 'user-interactions' | 'price-logs' = 'user-interactions',
lastN?: number
): Promise<{ success: boolean; count: number; data: unknown[] }> {
const params = new URLSearchParams({ topic });
if (lastN) {
params.set('last_n', String(lastN));
}
const response = await fetch(`${this.backendUrl}/api/kafka/dump?${params.toString()}`);
if (!response.ok) {
throw new Error(`Failed to dump Kafka events: ${response.status}`);
}
return response.json();
}
/**
* Check health of backend service
*/
async checkBackendHealth(): Promise<{ status: string; kafka: string }> {
const response = await fetch(`${this.backendUrl}/health`);
return response.json();
}
/**
* Check health of pricing provider
*/
async checkProviderHealth(): Promise<{ status: string; redis: boolean }> {
const response = await fetch(`${this.providerUrl}/health`);
return response.json();
}
/**
* List registered models in the pricing provider
*/
async listModels(): Promise<Record<string, unknown>> {
const response = await fetch(`${this.providerUrl}/models`);
return response.json();
}
/**
* Reload models in the pricing provider
*/
async reloadModels(): Promise<{ elasticity_loaded: boolean; pricing_model_loaded: boolean }> {
const response = await fetch(`${this.providerUrl}/models/reload`, { method: 'POST' });
return response.json();
}
}
// Singleton instance for convenience
export const apiClient = new PhantomApiClient();