- services/attest-reconciler/ as its own go.mod (admin-api internal/ is not importable; shared code gets extracted when a 3rd consumer appears). - Scheduler accepts "@every <dur>" or a bare Go duration; context-cancellation stops the loop cleanly. - Store client defines PendingRateStore + Postgres skeleton for SELECT ... WHERE rate_change_attestation_ref IS NULL. - Orchestrator + HTTPSigner + tlog.HTTPSubmitter mirror admin-api byte-for-byte so the TLog idempotency key collides on resubmits (kein Doppelsignieren). - Prometheus text-format /metrics endpoint, dependency-free: attest_reconciler_dirty_rows_total, _reconciliation_attempts_total, _reconciliation_failures_total, _idempotent_noop_total. - Tests with fake store + fake signer + fake submitter cover: happy path, already-attested rows are NOT re-signed, TLog idempotency across ticks, TLog-down failure path, pre-cancelled ctx. - Top-level README reflects the new service; CI auto-discovers services/*/go.mod so no workflow change needed. Co-Authored-By: Paperclip <noreply@paperclip.ing>
40 lines
1.7 KiB
Go
40 lines
1.7 KiB
Go
// Package store contains the pending-rows reader. The reconciler never writes
|
|
// to per_diem_rates directly; it hands the attestation ref back through
|
|
// AttachAttestation, which goes through the same immutability-safe update
|
|
// path admin-api uses.
|
|
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"basicstack.de/proofedmesh/services/attest-reconciler/internal/attest"
|
|
)
|
|
|
|
// ErrNotFound is returned by AttachAttestation when the rate_id is gone.
|
|
var ErrNotFound = errors.New("per_diem_rate: not found")
|
|
|
|
// PendingRateStore is implemented by the Postgres reader in prod and by a
|
|
// fake in tests. ListPending returns every row where
|
|
// rate_change_attestation_ref IS NULL — these are what the reconciler re-signs.
|
|
//
|
|
// Kind is reconstructed from (valid_to, now): a rate with a valid_to that was
|
|
// present at creation time is a "create" (valid_to-at-creation stays with the
|
|
// row), a rate whose valid_to was set later via Retire is a "retire". Because
|
|
// the Transparency Log is idempotent on (envelope, content_hash), a wrong
|
|
// Kind guess cannot produce a duplicate log entry — it would just land under
|
|
// a different content_hash the first time and converge from then on.
|
|
type PendingRateStore interface {
|
|
ListPending(ctx context.Context, limit int) ([]PendingRow, error)
|
|
AttachAttestation(ctx context.Context, rateID string, attestationRef string) error
|
|
}
|
|
|
|
// PendingRow is the minimal projection of per_diem_rates needed to rebuild a
|
|
// RateChangeEvent. The reconciler intentionally does NOT read the admin
|
|
// principal fields — it attests under its own service identity (see
|
|
// PerformedBy in the main command) because the original admin session is long
|
|
// gone by the time a row is picked up.
|
|
type PendingRow struct {
|
|
Rate attest.Rate
|
|
Kind attest.ChangeKind
|
|
}
|