import assert from 'node:assert/strict'; import { supabaseService } from '../src/services/SupabaseService.js'; type Row = Record; type Filter = | { op: 'eq'; field: string; value: any } | { op: 'in'; field: string; value: any[] } | { op: 'lt'; field: string; value: any }; class FakeSupabaseClient { public orders: Row[]; private rowId = 0; constructor(seedOrders: Row[] = []) { this.orders = seedOrders.map((row) => ({ ...row })); for (const row of this.orders) { this.rowId = Math.max(this.rowId, Number(String(row.id || '').replace(/[^0-9]/g, '')) || 0); } } public nextId(): string { this.rowId += 1; return `row-${this.rowId}`; } from(table: string) { if (table !== 'orders') { throw new Error(`Fake client only supports orders table (received: ${table})`); } return new FakeOrdersQuery(this); } } class FakeOrdersQuery implements PromiseLike<{ data: any; error: any }> { private mode: 'select' | 'update' = 'select'; private filters: Filter[] = []; private sortField: string | null = null; private sortAscending = true; private limitCount: number | null = null; private updatePayload: Row = {}; constructor(private readonly db: FakeSupabaseClient) { } select(_columns: string) { this.mode = 'select'; return this; } eq(field: string, value: any) { this.filters.push({ op: 'eq', field, value }); return this; } in(field: string, value: any[]) { this.filters.push({ op: 'in', field, value }); return this; } lt(field: string, value: any) { this.filters.push({ op: 'lt', field, value }); return this; } order(field: string, options?: { ascending?: boolean }) { this.sortField = field; this.sortAscending = options?.ascending !== false; return this; } limit(count: number) { this.limitCount = count; return this; } update(payload: Row) { this.mode = 'update'; this.updatePayload = { ...payload }; return this; } insert(rows: Row[]) { const inserted = rows.map((row) => { const id = row.id || this.db.nextId(); const createdAt = row.created_at || new Date().toISOString(); return { ...row, id, created_at: createdAt }; }); this.db.orders.push(...inserted); return Promise.resolve({ data: inserted, error: null }); } private matchesFilters(row: Row): boolean { return this.filters.every((filter) => { const value = row[filter.field]; if (filter.op === 'eq') { return filter.value === null ? value == null : value === filter.value; } if (filter.op === 'in') { return filter.value.includes(value); } const leftRaw = value; const rightRaw = filter.value; const leftTs = Date.parse(String(leftRaw || '')); const rightTs = Date.parse(String(rightRaw || '')); if (Number.isFinite(leftTs) && Number.isFinite(rightTs)) { return leftTs < rightTs; } return String(leftRaw || '') < String(rightRaw || ''); }); } private executeSelect(): { data: any; error: any } { let rows = this.db.orders.filter((row) => this.matchesFilters(row)).map((row) => ({ ...row })); if (this.sortField) { const field = this.sortField; const direction = this.sortAscending ? 1 : -1; rows = rows.sort((a, b) => { const aValue = a[field]; const bValue = b[field]; const aTs = Date.parse(String(aValue || '')); const bTs = Date.parse(String(bValue || '')); if (Number.isFinite(aTs) && Number.isFinite(bTs) && aTs !== bTs) { return direction * (aTs - bTs); } if (aValue === bValue) return 0; return direction * (String(aValue || '').localeCompare(String(bValue || ''))); }); } if (this.limitCount !== null) { rows = rows.slice(0, this.limitCount); } return { data: rows, error: null }; } private executeUpdate(): { data: any; error: any } { const updatedRows: Row[] = []; for (const row of this.db.orders) { if (!this.matchesFilters(row)) continue; Object.assign(row, this.updatePayload); updatedRows.push({ ...row }); } return { data: updatedRows, error: null }; } then( onfulfilled?: ((value: { data: any; error: any }) => TResult1 | PromiseLike) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { try { const result = this.mode === 'update' ? this.executeUpdate() : this.executeSelect(); if (!onfulfilled) { return Promise.resolve(result as TResult1); } return Promise.resolve(onfulfilled(result)); } catch (error) { if (onrejected) { return Promise.resolve(onrejected(error)); } return Promise.reject(error); } } } async function run() { const originalClient = (supabaseService as any).client; const fakeClient = new FakeSupabaseClient(); (supabaseService as any).client = fakeClient as any; try { const now = Date.now(); await supabaseService.logOrder({ user_id: 'u-1', profile_id: 'p-1', order_id: 'ORD-1', symbol: 'BTC/USD', type: 'market', side: 'BUY', qty: 1, price: 100, status: 'pending_new', timestamp: now, trade_id: 'TRD-1', action: 'ENTRY', sub_tag: 'BL:PAPER:PTEST:TTEST:ENTRY' }); assert.equal(fakeClient.orders.length, 1, 'Expected first order insert to create one row.'); await supabaseService.logOrder({ user_id: 'u-1', profile_id: 'p-1', order_id: 'ORD-1', symbol: 'BTC/USD', type: 'market', side: 'BUY', qty: 1, price: 100, status: 'pending_new', timestamp: now + 1000, trade_id: 'TRD-1', action: 'ENTRY' }); assert.equal(fakeClient.orders.length, 1, 'Duplicate order_id within same profile must be upserted, not inserted.'); await supabaseService.updateOrderStatus('ORD-1', 'filled', new Date(now + 2000), 101, 1.5); await supabaseService.logOrder({ user_id: 'u-1', profile_id: 'p-1', order_id: 'ORD-1', symbol: 'BTC/USD', type: 'market', side: 'BUY', qty: 1, price: 99, status: 'pending_new', timestamp: now + 3000, trade_id: 'TRD-1', action: 'ENTRY' }); const persisted = fakeClient.orders.find((row) => row.order_id === 'ORD-1' && row.profile_id === 'p-1'); assert(persisted, 'Expected persisted order row to exist after updates.'); assert.equal(persisted.status, 'filled', 'Order status must not downgrade from terminal status on duplicate log.'); assert.equal(Number(persisted.qty), 1.5, 'Order qty must retain terminal filled quantity.'); assert.equal(Number(persisted.price), 101, 'Order price must retain terminal fill price.'); assert.equal(persisted.sub_tag, 'BL:PAPER:PTEST:TTEST:ENTRY', 'Order sub_tag must persist for traceability.'); await supabaseService.logOrder({ user_id: 'u-1', profile_id: 'p-2', order_id: 'ORD-SHARED', symbol: 'ETH/USD', type: 'market', side: 'BUY', qty: 1, price: 2000, status: 'pending_new', timestamp: now, trade_id: 'TRD-SHARED-1', action: 'ENTRY' }); await supabaseService.logOrder({ user_id: 'u-1', profile_id: 'p-3', order_id: 'ORD-SHARED', symbol: 'ETH/USD', type: 'market', side: 'BUY', qty: 1, price: 2000, status: 'pending_new', timestamp: now + 10, trade_id: 'TRD-SHARED-2', action: 'ENTRY' }); const sharedRows = fakeClient.orders.filter((row) => row.order_id === 'ORD-SHARED'); assert.equal(sharedRows.length, 2, 'Same exchange order id across different profiles must remain isolated.'); const staleCreatedAt = new Date(Date.now() - 10 * 60_000).toISOString(); fakeClient.orders.push( { id: fakeClient.nextId(), order_id: 'ORD-PN', status: 'pending_new', created_at: staleCreatedAt, symbol: 'BTC/USD' }, { id: fakeClient.nextId(), order_id: 'ORD-P', status: 'pending', created_at: staleCreatedAt, symbol: 'BTC/USD' }, { id: fakeClient.nextId(), order_id: 'ORD-A', status: 'accepted', created_at: staleCreatedAt, symbol: 'BTC/USD' }, { id: fakeClient.nextId(), order_id: 'ORD-N', status: 'new', created_at: staleCreatedAt, symbol: 'BTC/USD' }, { id: fakeClient.nextId(), order_id: 'ORD-F', status: 'filled', created_at: staleCreatedAt, symbol: 'BTC/USD' }, { id: fakeClient.nextId(), order_id: 'ORD-YOUNG', status: 'pending_new', created_at: new Date().toISOString(), symbol: 'BTC/USD' } ); const stale = await supabaseService.getStaleOrders(5); const staleIds = new Set((stale || []).map((row: any) => String(row.order_id || row.id))); assert(staleIds.has('ORD-PN'), 'Expected pending_new to be considered stale.'); assert(staleIds.has('ORD-P'), 'Expected pending to be considered stale.'); assert(staleIds.has('ORD-A'), 'Expected accepted to be considered stale.'); assert(staleIds.has('ORD-N'), 'Expected new to be considered stale.'); assert(!staleIds.has('ORD-F'), 'Filled orders must not be included in stale-pending query.'); assert(!staleIds.has('ORD-YOUNG'), 'Recent pending_new orders must not be included as stale.'); console.log('[supabase-order-persistence-regressions] OK: order upsert and stale status selection are stable'); } finally { (supabaseService as any).client = originalClient; } } await run();