// Package reconciliation provides read-only integrity checks for the GL. package reconciliation import ( "context" "fmt" "gl/domain/ledger" ) type Repository interface { List(context.Context, ledger.JournalFilter) ([]ledger.Journal, error) } type Report struct { Scanned int Valid int Invalid []Issue } type Issue struct { JournalID string Error string } // Run scans all posted journals without mutating the ledger. func Run(ctx context.Context, repository Repository, pageSize int) (Report, error) { if repository == nil { return Report{}, fmt.Errorf("reconciliation repository is required") } if pageSize <= 0 || pageSize > 200 { pageSize = 100 } report := Report{Invalid: make([]Issue, 0)} seen := make(map[string]string) for offset := 0; ; offset += pageSize { journals, err := repository.List(ctx, ledger.JournalFilter{Limit: pageSize, Offset: offset}) if err != nil { return Report{}, fmt.Errorf("list journals at offset %d: %w", offset, err) } for _, journal := range journals { report.Scanned++ if err := journal.Validate(); err != nil { report.Invalid = append(report.Invalid, Issue{JournalID: journal.ID, Error: err.Error()}) continue } key := evidenceKey(journal.SourceService, journal.SourceTransactionID, journal.EventVersion) if previous, exists := seen[key]; exists { report.Invalid = append(report.Invalid, Issue{JournalID: journal.ID, Error: fmt.Sprintf("duplicate source transaction/version; first journal %s", previous)}) continue } seen[key] = journal.ID report.Valid++ } if len(journals) < pageSize { break } } return report, nil }