61 lines
1.6 KiB
Go
61 lines
1.6 KiB
Go
// 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
|
|
}
|