Files
nexarch/archive/internal/reconcilereportmigration/reconcilereportmigration.go
T
patrickandClaude Sonnet 5 0696906daf
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Archive-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
MAI-04: reconciliation-reports-archivierung
Migriert historische Reconciliation-Reports aus archivmail als
reinen Nachweis nach Archive BAK-05 (neue Tabelle
reconciliation_report_archive statt wormstore, da chattr +i auf
dem ZFS-Testhost nicht durchsetzbar ist). Tenant-Zuordnung
strukturell ueber die Zieldatenbankverbindung (Modell C).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-14 20:03:48 +00:00

124 lines
3.9 KiB
Go

// Package reconcilereportmigration implementiert MAI-04: Übernahme
// historischer Reconciliation-Reports aus archivmail als Nachweis in
// Archive BAK-05 (archive/internal/reconcile) — kein aktiver
// Datenbestand, rein historischer Beleg.
//
// Tenant-Zuordnung ist strukturell über die Zieldatenbankverbindung
// garantiert (Modell C, Core TEN-01: eine physisch isolierte Datenbank
// je Mandant, wie bereits in archive/internal/reconcile.ListDBStorageKeys)
// — kein tenant_slug-Feld in reconciliation_report_archive nötig, jede
// Verbindung spricht nur mit genau einer Mandanten-Datenbank.
package reconcilereportmigration
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// SourceReport ist eine Zeile aus archivmails historischer
// Reconciliation-Report-Tabelle (SQL-Export). IndexedAt wird absichtlich
// nicht übernommen (Produkt-DNA: kein Übernehmen von indexed_at-
// Statusfeldern).
type SourceReport struct {
SourceReportID string
GeneratedAt time.Time
ReportJSON []byte
IndexedAt *int64
}
// Source kapselt den lesenden SQL-Export-Zugriff auf archivmails
// historische Reconciliation-Reports.
type Source interface {
Reports(ctx context.Context) ([]SourceReport, error)
}
// Migrator überträgt historische Reports einer Quelle in genau eine
// Ziel-Mandantendatenbank.
type Migrator struct {
source Source
pool *pgxpool.Pool
}
func NewMigrator(source Source, pool *pgxpool.Pool) *Migrator {
return &Migrator{source: source, pool: pool}
}
// Result fasst den Ausgang eines Migrationslaufs zusammen.
type Result struct {
ReportsMigrated int
ReportsSkipped int
}
// ErrNotFound meldet, dass unter der angefragten Quell-ID kein
// archivierter Report gefunden wurde.
var ErrNotFound = errors.New("reconcilereportmigration: report nicht gefunden")
// MigrateAll überträgt alle Reports (Akzeptanzkriterium 1). Ein bereits
// migrierter Report (gleiche source_report_id bereits vorhanden) wird
// übersprungen, statt erneut geschrieben zu werden (Idempotenz).
func (m *Migrator) MigrateAll(ctx context.Context) (Result, error) {
var res Result
reports, err := m.source.Reports(ctx)
if err != nil {
return res, fmt.Errorf("reconcilereportmigration: reports aus quelle lesen: %w", err)
}
for _, r := range reports {
migrated, err := m.migrateReport(ctx, r)
if err != nil {
return res, err
}
if migrated {
res.ReportsMigrated++
} else {
res.ReportsSkipped++
}
}
return res, nil
}
func (m *Migrator) migrateReport(ctx context.Context, r SourceReport) (migrated bool, err error) {
tag, err := m.pool.Exec(ctx, `
INSERT INTO reconciliation_report_archive (source_report_id, generated_at, report_json)
VALUES ($1, $2, $3)
ON CONFLICT (source_report_id) DO NOTHING
`, r.SourceReportID, r.GeneratedAt, r.ReportJSON)
if err != nil {
return false, fmt.Errorf("reconcilereportmigration: report %q speichern: %w", r.SourceReportID, err)
}
return tag.RowsAffected() > 0, nil
}
// ArchivedReport ist ein bereits migrierter, auffindbarer Report
// (Akzeptanzkriterium 1).
type ArchivedReport struct {
SourceReportID string
GeneratedAt time.Time
ReportJSON []byte
MigratedAt time.Time
}
// FindBySourceID macht einen migrierten Report in der Zieldatenbank
// auffindbar. Die Tenant-Zuordnung ergibt sich daraus, mit welcher
// Mandantendatenbank pool verbunden ist.
func FindBySourceID(ctx context.Context, pool *pgxpool.Pool, sourceReportID string) (ArchivedReport, error) {
var ar ArchivedReport
ar.SourceReportID = sourceReportID
err := pool.QueryRow(ctx, `
SELECT generated_at, report_json, migrated_at
FROM reconciliation_report_archive WHERE source_report_id = $1
`, sourceReportID).Scan(&ar.GeneratedAt, &ar.ReportJSON, &ar.MigratedAt)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ArchivedReport{}, ErrNotFound
}
return ArchivedReport{}, fmt.Errorf("reconcilereportmigration: report %q lesen: %w", sourceReportID, err)
}
return ar, nil
}