import fetch from 'node-fetch';
import { CookieJar } from 'tough-cookie';
import { Order, OrderDetails, OrderItem, DeliveryInfo } from './types.js';
export type BrowserCookie = {
name: string;
value: string;
domain?: string;
path?: string;
expires?: number;
secure?: boolean;
httpOnly?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None' | string;
};
function buildCookieJar(cookies: BrowserCookie[], defaultDomain: string): CookieJar {
const jar = new CookieJar();
const origin = `https://${defaultDomain}`;
for (const cookie of cookies) {
const parts: string[] = [];
parts.push(`${cookie.name}=${cookie.value}`);
parts.push(`Domain=${cookie.domain ?? defaultDomain}`);
parts.push(`Path=${cookie.path ?? '/'}`);
if (cookie.secure) parts.push('Secure');
if (cookie.httpOnly) parts.push('HttpOnly');
if (cookie.sameSite) parts.push(`SameSite=${cookie.sameSite}`);
if (cookie.expires && Number.isFinite(cookie.expires)) {
const date = new Date(cookie.expires * 1000);
parts.push(`Expires=${date.toUTCString()}`);
}
const cookieString = parts.join('; ');
try {
jar.setCookieSync(cookieString, origin);
} catch {
// skip malformed cookies
}
}
return jar;
}
async function cookieHeader(jar: CookieJar, url: string): Promise<string> {
return await new Promise<string>((resolve) => {
jar.getCookieString(url, {}, (_err, str) => resolve(str || ''));
});
}
const BFF_BASE = 'https://api.pcexpress.ca/pcx-bff/api/v1';
const BFF_API_KEY = 'C1xujSegT5j3ap3yexJjqhOfELwGKYvz';
function buildHeaders(cookie: string, extra?: Record<string, string>) {
return {
'accept': 'application/json',
'site-banner': 'superstore',
'x-apikey': BFF_API_KEY,
'business-user-agent': 'PCXWEB',
'is-helios-account': 'true',
'x-application-type': 'WEB',
'x-channel': 'WEB',
...(cookie ? { 'cookie': cookie } : {}),
...(extra ?? {}),
} as Record<string, string>;
}
function toOrder(obj: any): Order | null {
if (!obj || typeof obj !== 'object') return null;
// Handle order details response (has orderNumber at root)
const id = String(
obj.orderNumber ?? obj.id ?? obj.orderId ?? obj.number ?? obj.reference ?? ''
);
if (!id) return null;
const date = String(
obj.placed ?? obj.completedAt ?? obj.submittedAt ?? obj.createdAt ?? obj.updatedAt ?? new Date().toISOString()
);
const status = String(
obj.orderType ?? obj.status ?? obj.state ?? 'Unknown'
);
const total = Number(
obj.totalPrice ?? obj.total ?? obj?.totals?.grandTotal?.amount ?? obj?.totalAmount ?? obj?.grandTotal ?? 0
) || 0;
// Extract items from various sources
let itemsSource = obj.entries ?? obj.items ?? obj.lineItems ?? obj.products;
if (!Array.isArray(itemsSource) && obj.cart?.entries) {
itemsSource = obj.cart.entries;
}
const items: OrderItem[] = Array.isArray(itemsSource)
? itemsSource.map((it: any) => {
// Handle cart entry format {product: {...}, quantity, totalPrice}
const product = it.product ?? it;
return {
product_id: String(product.id ?? product.productId ?? product.articleNumber ?? ''),
name: String(product.productName ?? product.name ?? product.title ?? 'Item'),
quantity: Number(it.quantity ?? 1) || 1,
price: Number(it.totalPrice ?? it.price ?? it.unitPrice ?? product.price ?? 0) || 0,
image_url: product.primaryImage ?? product.imageUrl ?? product.image ?? undefined,
};
})
: [];
return { id, date, status, total, items };
}
export async function fetchHistoricalOrdersApi(
cookies: BrowserCookie[],
limit: number,
offset: number,
extraHeaders?: Record<string, string>,
): Promise<Order[]> {
const jar = buildCookieJar(cookies, 'api.pcexpress.ca');
const url = `${BFF_BASE}/ecommerce/v2/superstore/customers/historical-orders`;
const cookie = await cookieHeader(jar, url);
const headers = buildHeaders(cookie, extraHeaders);
console.log('[API] Calling historical-orders with headers:', {
hasAuth: !!extraHeaders?.authorization,
hasCookie: !!cookie,
url
});
const res = await fetch(url, { headers } as any);
console.log('[API] Response:', {
status: res.status,
statusText: res.statusText,
ok: res.ok
});
if (!res.ok) {
const errorText = await res.text();
console.error('[API] Error response:', errorText);
throw new Error(`historical-orders failed: ${res.status} - ${errorText}`);
}
const data: any = await res.json();
console.log('[API] Response data keys:', Object.keys(data || {}));
const rawList: any[] = Array.isArray(data?.orderHistory) ? data.orderHistory :
Array.isArray(data?.orders) ? data.orders :
Array.isArray(data) ? data : [];
console.log('[API] Found orders:', rawList.length);
const orders = rawList.map(toOrder).filter(Boolean) as Order[];
const sliced = orders.slice(offset, offset + limit);
return sliced;
}
export async function fetchCurrentOrdersApi(
cookies: BrowserCookie[],
limit: number,
offset: number,
extraHeaders?: Record<string, string>,
): Promise<Order[]> {
const jar = buildCookieJar(cookies, 'api.pcexpress.ca');
const url = `${BFF_BASE}/ecommerce/v2/superstore/customers/orders?status=SUBMITTED,READY_FOR_ACTION,READY_FOR_PICK_UP,COMPLETED`;
const cookie = await cookieHeader(jar, url);
const headers = buildHeaders(cookie, extraHeaders);
console.log('[API] Calling current orders:', { url, hasAuth: !!extraHeaders?.authorization });
const res = await fetch(url, { headers } as any);
console.log('[API] Current orders response:', { status: res.status, ok: res.ok });
if (!res.ok) {
const errorText = await res.text();
console.error('[API] Error response:', errorText);
throw new Error(`customers/orders failed: ${res.status} - ${errorText}`);
}
const data: any = await res.json();
console.log('[API] Current orders data keys:', Object.keys(data || {}));
const rawList: any[] = Array.isArray(data?.orders) ? data.orders : Array.isArray(data) ? data : [];
console.log('[API] Found current orders:', rawList.length);
const orders = rawList.map(toOrder).filter(Boolean) as Order[];
const sliced = orders.slice(offset, offset + limit);
return sliced;
}
export async function fetchOrderDetailsApi(
cookies: BrowserCookie[],
orderNumber: string,
extraHeaders?: Record<string, string>,
): Promise<OrderDetails | null> {
const jar = buildCookieJar(cookies, 'api.pcexpress.ca');
const url = `${BFF_BASE}/ecommerce/v2/superstore/customers/historical-orders/${encodeURIComponent(orderNumber)}`;
const cookie = await cookieHeader(jar, url);
const headers = buildHeaders(cookie, extraHeaders);
console.log('[API] Calling order details:', { orderNumber, url, hasAuth: !!extraHeaders?.authorization });
const res = await fetch(url, { headers } as any);
console.log('[API] Order details response:', { status: res.status, ok: res.ok });
if (!res.ok) {
const errorText = await res.text();
console.error('[API] Order details error:', errorText);
throw new Error(`order details failed: ${res.status} - ${errorText}`);
}
const data: any = await res.json();
console.log('[API] Order details data keys:', Object.keys(data || {}));
// Unwrap orderDetails if present
const orderData = data.orderDetails ?? data;
console.log('[API] Using orderData keys:', Object.keys(orderData || {}));
const base = toOrder(orderData);
if (!base) {
console.warn('[API] toOrder returned null for orderData:', JSON.stringify(orderData).substring(0, 200));
return null;
}
const order: OrderDetails = {
...base,
order_number: orderNumber,
payment_method: String(
data?.paymentMethod ?? data?.payment?.method ?? data?.paymentType ?? 'Unknown'
),
subtotal: Number(data?.totals?.subtotal?.amount ?? 0) || 0,
tax: Number(data?.totals?.tax?.amount ?? data?.taxAmount ?? 0) || 0,
shipping: Number(data?.totals?.shipping?.amount ?? data?.deliveryFee ?? 0) || 0,
discount: Number(data?.totals?.discount?.amount ?? data?.discountAmount ?? 0) || undefined,
notes: data?.notes ?? undefined,
};
const address = data?.shippingAddress?.formatted ?? data?.address?.formatted ?? '';
const deliveryInfo: DeliveryInfo = {
address: String(address || ''),
estimated_delivery: data?.delivery?.estimatedTime ?? data?.pickup?.slot ?? undefined,
tracking_number: data?.trackingNumber ?? undefined,
};
order.delivery_info = deliveryInfo;
return order;
}