package reconciliation import ( "context" "fmt" "gl/domain/ledger" ) // Evidence is a normalized record supplied by Wallet, Kuknos, or another // authoritative settlement source. Adapters remain outside the GL domain. type Evidence struct { SourceService string SourceTransactionID string EventVersion uint32 BlockchainNetwork string TransactionHash string } type EvidenceReport struct { Compared int Missing []Evidence Duplicates []Evidence Mismatched []EvidenceMismatch } type EvidenceMismatch struct { Evidence Evidence JournalID string Field string Expected string Actual string } // CompareEvidence compares normalized external evidence with immutable GL // journals. It is read-only and safe to run repeatedly after outages. func CompareEvidence(ctx context.Context, repository Repository, evidence []Evidence, pageSize int) (EvidenceReport, error) { if repository == nil { return EvidenceReport{}, fmt.Errorf("reconciliation repository is required") } if pageSize <= 0 || pageSize > 200 { pageSize = 100 } journals := make(map[string]ledger.Journal) for offset := 0; ; offset += pageSize { page, err := repository.List(ctx, ledger.JournalFilter{Limit: pageSize, Offset: offset}) if err != nil { return EvidenceReport{}, fmt.Errorf("list journals at offset %d: %w", offset, err) } for _, journal := range page { journals[evidenceKey(journal.SourceService, journal.SourceTransactionID, journal.EventVersion)] = journal } if len(page) < pageSize { break } } report := EvidenceReport{Missing: make([]Evidence, 0), Duplicates: make([]Evidence, 0), Mismatched: make([]EvidenceMismatch, 0)} seen := make(map[string]struct{}, len(evidence)) for _, item := range evidence { report.Compared++ key := evidenceKey(item.SourceService, item.SourceTransactionID, item.EventVersion) if _, exists := seen[key]; exists { report.Duplicates = append(report.Duplicates, item) continue } seen[key] = struct{}{} journal, exists := journals[key] if !exists { report.Missing = append(report.Missing, item) continue } if item.BlockchainNetwork != "" && item.BlockchainNetwork != journal.Blockchain.Network { report.Mismatched = append(report.Mismatched, EvidenceMismatch{Evidence: item, JournalID: journal.ID, Field: "blockchain_network", Expected: item.BlockchainNetwork, Actual: journal.Blockchain.Network}) } if item.TransactionHash != "" && item.TransactionHash != journal.Blockchain.TransactionHash { report.Mismatched = append(report.Mismatched, EvidenceMismatch{Evidence: item, JournalID: journal.ID, Field: "transaction_hash", Expected: item.TransactionHash, Actual: journal.Blockchain.TransactionHash}) } } return report, nil } func evidenceKey(sourceService, sourceTransactionID string, eventVersion uint32) string { return sourceService + "\x00" + sourceTransactionID + "\x00" + fmt.Sprint(eventVersion) }