feat: add idempotent calendar bootstrap

Bootstrap independently reviewed catalogs through audited import, approval, and initial activation transitions, then resume insight scheduling safely.
This commit is contained in:
2026-08-23 02:24:16 +08:00
parent 72238e7e87
commit 5673b60ba6
7 changed files with 671 additions and 49 deletions

View File

@@ -3,90 +3,185 @@ package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"strings"
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/config"
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/database"
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/eventcatalog"
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/insight"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
action := flag.String("action", "", "import, approve, or activate")
filePath := flag.String("file", "", "reviewed JSON catalog for import")
version := flag.String("version", "", "catalog version for approve or activate")
actor := flag.String("actor", "", "audited reviewer or operator identity")
currentJalaliYear := flag.Int("current-jalali-year", 0, "current Jalali year used for coverage validation")
flag.Parse()
type options struct {
action string
filePath string
version string
actor string
reviewer string
currentJalaliYear int
}
func main() {
os.Exit(realMain(os.Args[1:]))
}
func realMain(arguments []string) int {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := run(ctx, arguments, logger); err != nil {
if errors.Is(err, flag.ErrHelp) {
fmt.Fprint(os.Stdout, calendarUsage)
return 0
}
logger.Error("calendar command failed", "error", err)
return 1
}
return 0
}
const calendarUsage = `Usage: calendar -action <import|approve|activate|bootstrap> [options]
-file PATH reviewed JSON catalog for import or bootstrap
-current-jalali-year YEAR current Jalali year for coverage validation
-version VERSION catalog version for approve or activate
-reviewer ID independent reviewer identity for bootstrap
-actor ID approval or activation operator identity
`
func run(ctx context.Context, arguments []string, logger *slog.Logger) error {
command, err := parseOptions(arguments)
if err != nil {
return err
}
var input eventcatalog.Import
if command.action == "import" || command.action == "bootstrap" {
input, err = readImport(command.filePath)
if err != nil {
return err
}
if err := eventcatalog.ValidateImport(input, command.currentJalaliYear); err != nil {
return fmt.Errorf("validating calendar catalog: %w", err)
}
}
cfg, err := config.Load()
if err != nil {
fail(logger, "configuration failed", err)
return fmt.Errorf("loading configuration: %w", err)
}
pool, err := database.Open(ctx, cfg.DatabaseURL)
if err != nil {
fail(logger, "database connection failed", err)
return fmt.Errorf("connecting to database: %w", err)
}
defer pool.Close()
repository := eventcatalog.NewRepository(pool)
switch *action {
switch command.action {
case "import":
if *filePath == "" || *currentJalaliYear == 0 {
fail(logger, "invalid import arguments", fmt.Errorf("file and current-jalali-year are required"))
}
file, err := os.Open(*filePath)
if err != nil {
fail(logger, "opening calendar catalog failed", err)
}
defer file.Close()
var input eventcatalog.Import
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil {
fail(logger, "decoding calendar catalog failed", err)
}
if err := eventcatalog.ValidateImport(input, *currentJalaliYear); err != nil {
fail(logger, "calendar validation failed", err)
}
if err := repository.ImportDraft(ctx, input); err != nil {
fail(logger, "calendar import failed", err)
return fmt.Errorf("importing calendar catalog: %w", err)
}
logger.Info("calendar draft imported", "version", input.Version, "events", len(input.Events))
case "approve":
if *version == "" || *actor == "" {
fail(logger, "invalid approval arguments", fmt.Errorf("version and actor are required"))
if err := repository.Approve(ctx, command.version, command.actor); err != nil {
return fmt.Errorf("approving calendar catalog: %w", err)
}
if err := repository.Approve(ctx, *version, *actor); err != nil {
fail(logger, "calendar approval failed", err)
}
logger.Info("calendar approved", "version", *version, "reviewer", *actor)
logger.Info("calendar approved", "version", command.version, "reviewer", command.actor)
case "activate":
if *version == "" || *actor == "" {
fail(logger, "invalid activation arguments", fmt.Errorf("version and actor are required"))
if err := repository.Activate(ctx, command.version, command.actor); err != nil {
return fmt.Errorf("activating calendar catalog: %w", err)
}
if err := repository.Activate(ctx, *version, *actor); err != nil {
fail(logger, "calendar activation failed", err)
}
service := insight.NewService(insight.NewRepository(pool), insight.NewPostgreSQLBuilder(pool))
created, err := service.EnqueueAll(ctx, "CALENDAR")
created, err := scheduleInsights(ctx, pool)
if err != nil {
fail(logger, "calendar refresh scheduling failed", err)
return err
}
logger.Info("calendar activated", "version", *version, "actor", *actor, "created_jobs", created)
default:
fail(logger, "invalid action", fmt.Errorf("action must be import, approve, or activate"))
logger.Info("calendar activated", "version", command.version, "actor", command.actor, "created_jobs", created)
case "bootstrap":
result, err := eventcatalog.BootstrapCatalog(ctx, repository, input, command.currentJalaliYear, command.reviewer, command.actor, func(ctx context.Context) (int, error) {
return scheduleInsights(ctx, pool)
})
if err != nil {
return fmt.Errorf("bootstrapping calendar catalog: %w", err)
}
logger.Info("calendar bootstrap complete", "version", result.Version, "status", result.Status, "transitions", result.Transitions, "no_op", result.NoOp, "created_jobs", result.CreatedJobs)
}
return nil
}
func fail(logger *slog.Logger, message string, err error) {
logger.Error(message, "error", err)
os.Exit(1)
func parseOptions(arguments []string) (options, error) {
var result options
flags := flag.NewFlagSet("calendar", flag.ContinueOnError)
flags.SetOutput(io.Discard)
flags.StringVar(&result.action, "action", "", "import, approve, activate, or bootstrap")
flags.StringVar(&result.filePath, "file", "", "reviewed JSON catalog for import or bootstrap")
flags.StringVar(&result.version, "version", "", "catalog version for approve or activate")
flags.StringVar(&result.actor, "actor", "", "audited reviewer or activation operator identity")
flags.StringVar(&result.reviewer, "reviewer", "", "independent reviewer identity for bootstrap")
flags.IntVar(&result.currentJalaliYear, "current-jalali-year", 0, "current Jalali year used for coverage validation")
if err := flags.Parse(arguments); err != nil {
return options{}, fmt.Errorf("parsing calendar arguments: %w", err)
}
if flags.NArg() != 0 {
return options{}, errors.New("calendar: positional arguments are not supported")
}
result.action = strings.TrimSpace(result.action)
result.filePath = strings.TrimSpace(result.filePath)
result.version = strings.TrimSpace(result.version)
result.actor = strings.TrimSpace(result.actor)
result.reviewer = strings.TrimSpace(result.reviewer)
switch result.action {
case "import":
if result.filePath == "" || result.currentJalaliYear == 0 {
return options{}, errors.New("calendar: file and current-jalali-year are required for import")
}
case "approve", "activate":
if result.version == "" || result.actor == "" {
return options{}, fmt.Errorf("calendar: version and actor are required for %s", result.action)
}
case "bootstrap":
if result.filePath == "" || result.currentJalaliYear == 0 || result.reviewer == "" || result.actor == "" {
return options{}, errors.New("calendar: file, current-jalali-year, reviewer, and actor are required for bootstrap")
}
default:
return options{}, errors.New("calendar: action must be import, approve, activate, or bootstrap")
}
return result, nil
}
func readImport(path string) (eventcatalog.Import, error) {
file, err := os.Open(path)
if err != nil {
return eventcatalog.Import{}, fmt.Errorf("opening calendar catalog: %w", err)
}
defer file.Close()
var input eventcatalog.Import
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil {
return eventcatalog.Import{}, fmt.Errorf("decoding calendar catalog: %w", err)
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
if err == nil {
return eventcatalog.Import{}, errors.New("decoding calendar catalog: trailing JSON document")
}
return eventcatalog.Import{}, fmt.Errorf("decoding calendar catalog trailing data: %w", err)
}
return input, nil
}
func scheduleInsights(ctx context.Context, pool *pgxpool.Pool) (int, error) {
service := insight.NewService(insight.NewRepository(pool), insight.NewPostgreSQLBuilder(pool))
created, err := service.EnqueueAll(ctx, "CALENDAR")
if err != nil {
return 0, fmt.Errorf("enqueueing calendar insights: %w", err)
}
return created, nil
}

72
cmd/calendar/main_test.go Normal file
View File

@@ -0,0 +1,72 @@
package main
import (
"errors"
"flag"
"os"
"path/filepath"
"testing"
)
func TestParseOptionsSupportsHelp(t *testing.T) {
t.Parallel()
if _, err := parseOptions([]string{"--help"}); !errors.Is(err, flag.ErrHelp) {
t.Fatalf("error=%v, want flag.ErrHelp", err)
}
}
func TestParseOptionsRequiresBootstrapInputs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
}{
{name: "file", args: []string{"-action", "bootstrap", "-current-jalali-year", "1405", "-reviewer", "reviewer", "-actor", "operator"}},
{name: "current year", args: []string{"-action", "bootstrap", "-file", "/calendar/reviewed.json", "-reviewer", "reviewer", "-actor", "operator"}},
{name: "reviewer", args: []string{"-action", "bootstrap", "-file", "/calendar/reviewed.json", "-current-jalali-year", "1405", "-actor", "operator"}},
{name: "operator", args: []string{"-action", "bootstrap", "-file", "/calendar/reviewed.json", "-current-jalali-year", "1405", "-reviewer", "reviewer"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
if _, err := parseOptions(test.args); err == nil {
t.Fatal("expected missing bootstrap input error")
}
})
}
options, err := parseOptions([]string{"-action", "bootstrap", "-file", "/calendar/reviewed.json", "-current-jalali-year", "1405", "-reviewer", "reviewer", "-actor", "operator"})
if err != nil {
t.Fatal(err)
}
if options.action != "bootstrap" || options.currentJalaliYear != 1405 || options.reviewer != "reviewer" || options.actor != "operator" {
t.Fatalf("options=%+v", options)
}
}
func TestReadImportRejectsUnknownFieldsAndTrailingDocuments(t *testing.T) {
t.Parallel()
directory := t.TempDir()
tests := []struct {
name string
json string
}{
{name: "unknown field", json: `{"version":"test","unexpected":true}`},
{name: "trailing document", json: `{"version":"test"} {}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
path := filepath.Join(directory, test.name+".json")
if err := os.WriteFile(path, []byte(test.json), 0o600); err != nil {
t.Fatal(err)
}
if _, err := readImport(path); err == nil {
t.Fatal("expected strict JSON error")
}
})
}
}