Files
zarinpal-analytics-backend/internal/eventcatalog/bootstrap.go
Amirhossein Khalili 5673b60ba6 feat: add idempotent calendar bootstrap
Bootstrap independently reviewed catalogs through audited import, approval, and initial activation transitions, then resume insight scheduling safely.
2026-08-23 02:24:16 +08:00

141 lines
4.8 KiB
Go

package eventcatalog
import (
"context"
"errors"
"fmt"
"strings"
)
var (
ErrCatalogNotFound = errors.New("event catalog: version not found")
ErrCatalogConflict = errors.New("event catalog: bootstrap conflict")
)
type CatalogStatus string
const (
CatalogStatusDraft CatalogStatus = "DRAFT"
CatalogStatusApproved CatalogStatus = "APPROVED"
CatalogStatusActive CatalogStatus = "ACTIVE"
CatalogStatusRetired CatalogStatus = "RETIRED"
)
type CatalogState struct {
Version string
Status CatalogStatus
CoverageStartJalaliYear int
CoverageEndJalaliYear int
SourceURI string
SourceVersion string
SourceChecksum string
ImportedBy string
}
type BootstrapResult struct {
Version string
Status CatalogStatus
Transitions []string
NoOp bool
CreatedJobs int
}
type bootstrapStore interface {
CatalogState(context.Context, string) (CatalogState, error)
ActiveCatalogState(context.Context) (CatalogState, error)
ImportDraft(context.Context, Import) error
Approve(context.Context, string, string) error
ActivateInitial(context.Context, string, string) error
}
type BootstrapScheduler func(context.Context) (int, error)
func BootstrapCatalog(ctx context.Context, store bootstrapStore, input Import, currentJalaliYear int, reviewer, operator string, schedule BootstrapScheduler) (BootstrapResult, error) {
reviewer = strings.TrimSpace(reviewer)
operator = strings.TrimSpace(operator)
if err := ValidateImport(input, currentJalaliYear); err != nil {
return BootstrapResult{}, err
}
if err := ValidateReviewableChecksum(input.SourceChecksum); err != nil {
return BootstrapResult{}, err
}
if err := ValidateApproval(input.ImportedBy, reviewer); err != nil {
return BootstrapResult{}, err
}
if operator == "" {
return BootstrapResult{}, errors.New("event catalog: activation operator is required")
}
if schedule == nil {
return BootstrapResult{}, errors.New("event catalog: bootstrap scheduler is required")
}
active, err := store.ActiveCatalogState(ctx)
if err != nil && !errors.Is(err, ErrCatalogNotFound) {
return BootstrapResult{}, fmt.Errorf("loading active calendar catalog: %w", err)
}
if active.Version != "" && active.Version != input.Version {
return BootstrapResult{}, fmt.Errorf("%w: active version %q differs from requested version %q", ErrCatalogConflict, active.Version, input.Version)
}
imported := false
state, err := store.CatalogState(ctx, input.Version)
if errors.Is(err, ErrCatalogNotFound) {
if err := store.ImportDraft(ctx, input); err != nil {
return BootstrapResult{}, fmt.Errorf("importing calendar draft: %w", err)
}
state = stateFromImport(input, CatalogStatusDraft)
imported = true
} else if err != nil {
return BootstrapResult{}, fmt.Errorf("loading calendar catalog version: %w", err)
} else if !state.matches(input) {
return BootstrapResult{}, fmt.Errorf("%w: version %q metadata does not match the reviewed file", ErrCatalogConflict, input.Version)
}
result := BootstrapResult{Version: input.Version}
if imported {
result.Transitions = append(result.Transitions, "IMPORTED")
}
if state.Status == CatalogStatusDraft && active.Version == "" {
if err := store.Approve(ctx, input.Version, reviewer); err != nil {
return BootstrapResult{}, fmt.Errorf("approving calendar catalog: %w", err)
}
result.Transitions = append(result.Transitions, "APPROVED")
state.Status = CatalogStatusApproved
}
if state.Status == CatalogStatusApproved && active.Version == "" {
if err := store.ActivateInitial(ctx, input.Version, operator); err != nil {
return BootstrapResult{}, fmt.Errorf("activating calendar catalog: %w", err)
}
result.Transitions = append(result.Transitions, "ACTIVATED")
state.Status = CatalogStatusActive
}
if state.Status != CatalogStatusActive {
return BootstrapResult{}, fmt.Errorf("%w: version %q has status %q", ErrCatalogConflict, input.Version, state.Status)
}
created, err := schedule(ctx)
if err != nil {
return BootstrapResult{}, fmt.Errorf("scheduling calendar refresh: %w", err)
}
result.Status = CatalogStatusActive
result.CreatedJobs = created
result.NoOp = len(result.Transitions) == 0 && created == 0
return result, nil
}
func stateFromImport(input Import, status CatalogStatus) CatalogState {
return CatalogState{
Version: input.Version,
Status: status,
CoverageStartJalaliYear: input.CoverageStartJalaliYear,
CoverageEndJalaliYear: input.CoverageEndJalaliYear,
SourceURI: input.SourceURI,
SourceVersion: input.SourceVersion,
SourceChecksum: input.SourceChecksum,
ImportedBy: input.ImportedBy,
}
}
func (state CatalogState) matches(input Import) bool {
return state == stateFromImport(input, state.Status)
}