import { JSDOM } from "jsdom";
import { SuperstoreAuth } from './Auth.js';
import { DirectAuth } from './DirectAuth.js';
import { Order, OrderDetails, OrderItem, DeliveryInfo } from './types.js';
import { ErrorHandler } from './ErrorHandler.js';
import { fetchHistoricalOrdersApi, fetchCurrentOrdersApi, fetchOrderDetailsApi } from './ordersApi.js';
export class SuperstoreOrders {
private auth: SuperstoreAuth | DirectAuth;
private baseUrl = 'https://www.realcanadiansuperstore.ca';
private ordersUrl = `${this.baseUrl}/account/orders`;
private cache: Map<string, any> = new Map();
private cacheTimeout = 300000; // 5 minutes
constructor(auth: SuperstoreAuth | DirectAuth) {
this.auth = auth;
}
/**
* Get orders with pagination support
*/
async getOrders(limit: number = 10, offset: number = 0): Promise<{ orders: Order[], total: number, hasMore: boolean }> {
return await ErrorHandler.handleNetworkError(async () => {
const cacheKey = `orders_${limit}_${offset}`;
const cached = this.getCachedData(cacheKey);
if (cached) return cached;
if (!await this.auth.isAuthenticated()) {
await this.auth.refreshSession();
}
let orders: Order[] = [];
// If DirectAuth (token-based), skip HTML scraping and go straight to API
const isDirectAuth = this.auth.constructor.name === 'DirectAuth';
if (isDirectAuth) {
const authToken = this.auth.getAuthToken();
const extraHeaders: Record<string, string> | undefined = authToken ? { 'authorization': `bearer ${authToken}` } : undefined;
orders = await fetchHistoricalOrdersApi([], limit, offset, extraHeaders);
if (orders.length === 0) {
orders = await fetchCurrentOrdersApi([], limit, offset, extraHeaders);
}
} else {
// Puppeteer mode: try HTML scraping first
const response = await (this.auth as any).fetchWithAuth(this.ordersUrl);
const html = await response.text();
const dom = new (JSDOM as any)(html);
const document = dom.window.document;
orders = await this.parseOrdersFromHTML(document);
// Fallback: if no orders found, try JSDOM dynamic render
if (orders.length === 0) {
try {
const cookies = await this.auth.getCookies();
const { openWithCookies, waitForSelector } = await import('./jsdom/JsdomSession.js');
const dynamicDom: any = await openWithCookies(this.ordersUrl, cookies as any[], {
enableScripts: true,
consolePrefix: '[orders-jsdom]'
});
const doc: Document = dynamicDom.window.document;
await waitForSelector(doc, '[data-testid*="order"], .order, .order-item, .order-card', 5000);
orders = await this.parseOrdersFromHTML(doc);
} catch (e) {
// ignore
}
}
// Final fallback: API
if (orders.length === 0) {
try {
const cookies = await this.auth.getCookieJarLike();
const authToken = this.auth.getAuthToken();
const extraHeaders: Record<string, string> | undefined = authToken ? { 'authorization': `bearer ${authToken}` } : undefined;
orders = await fetchHistoricalOrdersApi(cookies as any[], limit, offset, extraHeaders);
if (orders.length === 0) {
orders = await fetchCurrentOrdersApi(cookies as any[], limit, offset, extraHeaders);
}
} catch (apiErr) {
console.error('BFF API fallback failed:', apiErr);
}
}
}
const result = {
orders,
total: orders.length,
hasMore: offset + limit < orders.length
};
this.setCachedData(cacheKey, result);
return result;
}, 'get_orders');
}
/**
* Get detailed information for a specific order
*/
async getOrderDetails(orderId: string): Promise<OrderDetails | null> {
try {
const cacheKey = `order_details_${orderId}`;
const cached = this.getCachedData(cacheKey);
if (cached) return cached;
if (!await this.auth.isAuthenticated()) {
await this.auth.refreshSession();
}
// Try BFF API first (works for both DirectAuth and Puppeteer)
try {
const cookies = await this.auth.getCookieJarLike();
const authToken = this.auth.getAuthToken();
const extraHeaders: Record<string, string> | undefined = authToken ? { 'authorization': `bearer ${authToken}` } : undefined;
const orderDetails = await fetchOrderDetailsApi(cookies as any[], orderId, extraHeaders);
if (orderDetails) {
this.setCachedData(cacheKey, orderDetails);
return orderDetails;
}
} catch (apiErr) {
console.log('BFF API failed:', apiErr);
}
// Fallback to HTML scraping (only if Puppeteer mode)
if (this.auth.constructor.name !== 'DirectAuth') {
const orderUrl = `${this.ordersUrl}/${orderId}`;
const response = await (this.auth as any).fetchWithAuth(orderUrl);
const html = await response.text();
const dom = new (JSDOM as any)(html);
const document = dom.window.document;
const orderDetails = await this.parseOrderDetailsFromHTML(document, orderId);
if (orderDetails) {
this.setCachedData(cacheKey, orderDetails);
}
return orderDetails;
}
return null;
} catch (error) {
console.error(`Error fetching order details for ${orderId}:`, error);
throw new Error(`Failed to fetch order details: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get orders from the last N days
*/
async getRecentOrders(days: number = 30): Promise<Order[]> {
try {
const allOrders = await this.getOrders(100, 0); // Get more orders to filter by date
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
return allOrders.orders.filter(order => {
const orderDate = new Date(order.date);
return orderDate >= cutoffDate;
});
} catch (error) {
console.error('Error fetching recent orders:', error);
throw new Error(`Failed to fetch recent orders: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Parse orders from HTML document
*/
private async parseOrdersFromHTML(document: Document): Promise<Order[]> {
const orders: Order[] = [];
// Try multiple selectors for order containers
const orderSelectors = [
'[data-testid*="order"]',
'[class*="order"]',
'[class*="order-item"]',
'.order',
'.order-item',
'.order-card'
];
let orderElements: Element[] = [];
for (const selector of orderSelectors) {
const elements = Array.from(document.querySelectorAll(selector));
if (elements.length > 0) {
orderElements = elements;
break;
}
}
// If no specific order containers found, look for table rows or list items
if (orderElements.length === 0) {
const tableRows = Array.from(document.querySelectorAll('tr'));
const listItems = Array.from(document.querySelectorAll('li'));
// Filter for elements that look like orders (contain order-like text)
orderElements = [...tableRows, ...listItems].filter(el => {
const text = el.textContent?.toLowerCase() || '';
return text.includes('order') || text.includes('$') || text.includes('total');
});
}
for (const element of orderElements) {
try {
const order = await this.parseOrderElement(element);
if (order) {
orders.push(order);
}
} catch (error) {
console.error('Error parsing order element:', error);
continue;
}
}
return orders;
}
/**
* Parse individual order element
*/
private async parseOrderElement(element: Element): Promise<Order | null> {
try {
// Extract order ID
const orderIdSelectors = [
'[data-testid*="order-id"]',
'[class*="order-id"]',
'[class*="order-number"]',
'strong',
'b'
];
let orderId = '';
for (const selector of orderIdSelectors) {
const idElement = element.querySelector(selector);
if (idElement) {
orderId = idElement.textContent?.trim() || '';
if (orderId) break;
}
}
// If no specific ID found, try to extract from text
if (!orderId) {
const text = element.textContent || '';
const idMatch = text.match(/(?:order|#)\s*([A-Z0-9-]+)/i);
if (idMatch) {
orderId = idMatch[1];
}
}
if (!orderId) return null;
// Extract date
const dateSelectors = [
'[data-testid*="date"]',
'[class*="date"]',
'[class*="order-date"]',
'time'
];
let date = '';
for (const selector of dateSelectors) {
const dateElement = element.querySelector(selector);
if (dateElement) {
date = dateElement.textContent?.trim() || dateElement.getAttribute('datetime') || '';
if (date) break;
}
}
// Extract status
const statusSelectors = [
'[data-testid*="status"]',
'[class*="status"]',
'[class*="order-status"]'
];
let status = 'Unknown';
for (const selector of statusSelectors) {
const statusElement = element.querySelector(selector);
if (statusElement) {
status = statusElement.textContent?.trim() || 'Unknown';
break;
}
}
// Extract total
const totalSelectors = [
'[data-testid*="total"]',
'[class*="total"]',
'[class*="price"]',
'[class*="amount"]'
];
let total = 0;
for (const selector of totalSelectors) {
const totalElement = element.querySelector(selector);
if (totalElement) {
const totalText = totalElement.textContent?.trim() || '';
const totalMatch = totalText.match(/\$?(\d+\.?\d*)/);
if (totalMatch) {
total = parseFloat(totalMatch[1]);
break;
}
}
}
// Extract items (simplified for now)
const items: OrderItem[] = [];
const itemElements = element.querySelectorAll('[class*="item"], [data-testid*="item"]');
for (const itemElement of itemElements) {
const name = itemElement.textContent?.trim() || 'Unknown Item';
const priceText = itemElement.textContent?.match(/\$?(\d+\.?\d*)/);
const price = priceText ? parseFloat(priceText[1]) : 0;
items.push({
product_id: '',
name,
quantity: 1,
price,
image_url: undefined
});
}
return {
id: orderId,
date: date || new Date().toISOString(),
status,
total,
items,
delivery_info: undefined
};
} catch (error) {
console.error('Error parsing order element:', error);
return null;
}
}
/**
* Parse order details from HTML document
*/
private async parseOrderDetailsFromHTML(document: Document, orderId: string): Promise<OrderDetails | null> {
try {
// First try to get basic order info
const basicOrder = await this.parseOrdersFromHTML(document);
const order = basicOrder.find(o => o.id === orderId);
if (!order) return null;
// Extract additional details
const orderNumber = orderId;
// Extract payment method
const paymentSelectors = [
'[data-testid*="payment"]',
'[class*="payment"]',
'[class*="payment-method"]'
];
let paymentMethod = 'Unknown';
for (const selector of paymentSelectors) {
const paymentElement = document.querySelector(selector);
if (paymentElement) {
paymentMethod = paymentElement.textContent?.trim() || 'Unknown';
break;
}
}
// Extract subtotal, tax, shipping
const subtotal = order.total * 0.9; // Rough estimate
const tax = order.total * 0.1; // Rough estimate
const shipping = 0; // Default
// Extract delivery info
const deliveryInfo: DeliveryInfo = {
address: '',
estimated_delivery: undefined,
tracking_number: undefined
};
const addressSelectors = [
'[data-testid*="address"]',
'[class*="address"]',
'[class*="delivery"]'
];
for (const selector of addressSelectors) {
const addressElement = document.querySelector(selector);
if (addressElement) {
deliveryInfo.address = addressElement.textContent?.trim() || '';
break;
}
}
return {
...order,
order_number: orderNumber,
payment_method: paymentMethod,
subtotal,
tax,
shipping,
discount: undefined,
notes: undefined,
delivery_info: deliveryInfo
};
} catch (error) {
console.error('Error parsing order details:', error);
return null;
}
}
/**
* Cache management
*/
private getCachedData(key: string): any {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
this.cache.delete(key);
return null;
}
private setCachedData(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
/**
* Clear cache
*/
clearCache(): void {
this.cache.clear();
}
}