- 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>
55 lines
1.9 KiB
Go
55 lines
1.9 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"basicstack.de/proofedmesh/services/attest-reconciler/internal/attest"
|
|
)
|
|
|
|
// Postgres is the production PendingRateStore. Skeleton for now; wiring a
|
|
// real pgxpool.Pool lives alongside the admin-api Postgres swap in a later
|
|
// change so both consumers share one connection-config convention.
|
|
type Postgres struct {
|
|
// pool *pgxpool.Pool
|
|
}
|
|
|
|
var errPGUnimplemented = errors.New("postgres pending-row store is a skeleton; use a fake PendingRateStore for tests")
|
|
|
|
func NewPostgres() (*Postgres, error) {
|
|
return &Postgres{}, nil
|
|
}
|
|
|
|
func (p *Postgres) ListPending(ctx context.Context, limit int) ([]PendingRow, error) {
|
|
// Query shape (ADR-0004 schema):
|
|
// SELECT rate_id, country, city, currency,
|
|
// full_day, partial_day_min8h, arrival_day, departure_day,
|
|
// valid_from, valid_to, source_regulation,
|
|
// created_by, created_at,
|
|
// (valid_to IS NOT NULL AND valid_to < CURRENT_DATE) AS retired
|
|
// FROM per_diem_rates
|
|
// WHERE rate_change_attestation_ref IS NULL
|
|
// ORDER BY created_at ASC
|
|
// LIMIT $1
|
|
// Kind is "retire" when the row has a valid_to set at pickup time; "create"
|
|
// otherwise. See store.go for why that heuristic is safe under TLog
|
|
// idempotency.
|
|
return nil, errPGUnimplemented
|
|
}
|
|
|
|
func (p *Postgres) AttachAttestation(ctx context.Context, rateID string, attestationRef string) error {
|
|
// UPDATE per_diem_rates
|
|
// SET rate_change_attestation_ref = $2
|
|
// WHERE rate_id = $1 AND rate_change_attestation_ref IS NULL
|
|
// RETURNING rate_id;
|
|
// A zero-row result means someone else attested the row concurrently; the
|
|
// reconciler treats that as a successful no-op (see reconciler.go).
|
|
return errPGUnimplemented
|
|
}
|
|
|
|
// Compile-time assertions the real type implements the interface.
|
|
var _ PendingRateStore = (*Postgres)(nil)
|
|
|
|
// Reference imports kept so a future pgx wiring can reach the attest types
|
|
// without re-adding them.
|
|
var _ = attest.Rate{}
|