feat(gl): compare external settlement evidence
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user