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" ) 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 [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 { return fmt.Errorf("loading configuration: %w", err) } pool, err := database.Open(ctx, cfg.DatabaseURL) if err != nil { return fmt.Errorf("connecting to database: %w", err) } defer pool.Close() repository := eventcatalog.NewRepository(pool) switch command.action { case "import": if err := repository.ImportDraft(ctx, input); err != nil { return fmt.Errorf("importing calendar catalog: %w", err) } logger.Info("calendar draft imported", "version", input.Version, "events", len(input.Events)) case "approve": if err := repository.Approve(ctx, command.version, command.actor); err != nil { return fmt.Errorf("approving calendar catalog: %w", err) } logger.Info("calendar approved", "version", command.version, "reviewer", command.actor) case "activate": if err := repository.Activate(ctx, command.version, command.actor); err != nil { return fmt.Errorf("activating calendar catalog: %w", err) } created, err := scheduleInsights(ctx, pool) if err != nil { return err } 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 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 }