Description
Debugging payment disputes or auditing invoice lifecycles requires a chronologically ordered view of every state change and payment event for an invoice, but these events are scattered across Soroban event streams and Horizon payment records. This issue adds a PaymentTimelineReconstructor that aggregates events from both sources, normalizes them into a unified TimelineEntry format, and returns a sorted, deduplicated timeline for a given invoice.
Technical Context
- New file:
src/timeline/PaymentTimelineReconstructor.ts; types: TimelineEntry, TimelineSource, ReconstructedTimeline in src/types/timeline.ts
TimelineEntry: { timestamp: number; ledger: number; type: TimelineEventType; data: Record<string, unknown>; source: 'soroban'|'horizon' }
TimelineEventType union: 'invoice_created'|'payment_received'|'status_changed'|'recipient_added'|'recipient_rerouted'|'sla_breached'
- Deduplication uses
(ledger, txHash, type) as the composite key to eliminate events that appear in both sources
reconstructor.rebuild(invoiceId, options?: { from?: number; to?: number; types?: TimelineEventType[] }): Promise<ReconstructedTimeline> is the primary API
Acceptance Criteria
Description
Debugging payment disputes or auditing invoice lifecycles requires a chronologically ordered view of every state change and payment event for an invoice, but these events are scattered across Soroban event streams and Horizon payment records. This issue adds a
PaymentTimelineReconstructorthat aggregates events from both sources, normalizes them into a unifiedTimelineEntryformat, and returns a sorted, deduplicated timeline for a given invoice.Technical Context
src/timeline/PaymentTimelineReconstructor.ts; types:TimelineEntry,TimelineSource,ReconstructedTimelineinsrc/types/timeline.tsTimelineEntry:{ timestamp: number; ledger: number; type: TimelineEventType; data: Record<string, unknown>; source: 'soroban'|'horizon' }TimelineEventTypeunion:'invoice_created'|'payment_received'|'status_changed'|'recipient_added'|'recipient_rerouted'|'sla_breached'(ledger, txHash, type)as the composite key to eliminate events that appear in both sourcesreconstructor.rebuild(invoiceId, options?: { from?: number; to?: number; types?: TimelineEventType[] }): Promise<ReconstructedTimeline>is the primary APIAcceptance Criteria
timestampascendingfrom/toledger range filters exclude entries outside the rangetypesfilter returns only entries whosetypeis in the provided arrayReconstructedTimelineincludes{ totalEvents, sources: { soroban: number; horizon: number }, deduplicatedCount }in its metadata