86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
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)
|
|
}
|