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
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>
124 lines
3.9 KiB
Go
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
|
|
}
|