// Reported here rather than at the UI call sites so that no caller can add an // item or complete a checkout without it being counted (#56). Every one of // these calls is a no-op unless a consenting customer is signed in and the // environment has a key, so importing this into the API layer does not make the // cart depend on analytics being configured. import { brevoTrack } from '../brevo'; export interface CartItem { item_id: number; name: string; price_cents: number; status: string; added_at: string; expires_at: string; images: { id: number; image_path: string }[]; } export interface ShippingAddress { id: number; full_name: string; address_line1: string; address_line2: string | null; city: string; state: string; postal_code: string; country: string; is_default: boolean; usps_validated: boolean; } async function handle(res: Response): Promise { if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || 'Request failed'); } return res.json(); } export function fetchCart(): Promise<{ items: CartItem[] }> { return fetch('/api/cart').then(res => handle(res)); } export function addToCart(itemId: number): Promise<{ itemId: number; expiresAt: string }> { // After handle(), which throws on a non-OK response — so a refused add (the // item was already reserved by someone else) is not reported as one. return fetch(`/api/cart/items/${itemId}`, { method: 'POST' }) .then(res => handle<{ itemId: number; expiresAt: string }>(res)) .then(result => { brevoTrack('added_to_cart', { itemId }); return result; }); } export function removeFromCart(itemId: number): Promise { return fetch(`/api/cart/items/${itemId}`, { method: 'DELETE' }).then(() => undefined); } export function fetchAddresses(): Promise { return fetch('/api/customers/me/addresses').then(res => handle(res)); } export interface AddressInput { fullName: string; addressLine1: string; addressLine2?: string; city: string; state: string; postalCode: string; country?: string; isDefault?: boolean; } export function createAddress(input: AddressInput): Promise<{ address: ShippingAddress; uspsCheck: { validated: boolean; deliverable: boolean | null; reason?: string }; uspsConfigured: boolean }> { return fetch('/api/customers/me/addresses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) }).then(res => handle(res)); } export function deleteAddress(id: number): Promise { return fetch(`/api/customers/me/addresses/${id}`, { method: 'DELETE' }).then(() => undefined); } export function setDefaultAddress(id: number): Promise { return fetch(`/api/customers/me/addresses/${id}/set-default`, { method: 'POST' }).then(res => handle(res)); } export function createCartPaypalOrder(shippingAddressId: number): Promise<{ orderID: string }> { return fetch('/api/checkout/cart/paypal/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shippingAddressId }) }).then(res => handle(res)); } // The two checkout completions are separate functions rather than one, so both // report the event and both say which they were. Without the processor these // would be indistinguishable in Brevo, and a demo purchase charges nothing — // counting it as a sale would overstate revenue. QA cannot reach here anyway // (no key), but DEMO_MODE is not exclusive to QA, so the distinction is real. export function captureCartPaypalOrder(orderID: string): Promise { return fetch('/api/checkout/cart/paypal/capture', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderID }) }) .then(res => handle(res)) .then(() => { brevoTrack('checkout_completed', { processor: 'paypal' }); }); } export function demoCartPurchase(shippingAddressId: number): Promise { return fetch('/api/checkout/cart/demo/purchase', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shippingAddressId }) }) .then(res => handle(res)) .then(() => { brevoTrack('checkout_completed', { processor: 'demo' }); }); }