// Command replay-outbox publishes one explicitly bounded wallet owner-outbox // batch. It is intentionally separate from the long-running server worker: // batch-size there controls one poll, while --limit here caps the whole process. package main import ( "context" "errors" "flag" "fmt" "os" "strings" "time" "hyapp/pkg/outboxreplay" "hyapp/services/wallet-service/internal/app" "hyapp/services/wallet-service/internal/config" ) type repeatedFlag []string func (values *repeatedFlag) String() string { return strings.Join(*values, ",") } func (values *repeatedFlag) Set(value string) error { value = strings.TrimSpace(value) if value == "" { return errors.New("event-type cannot be empty") } *values = append(*values, value) return nil } func main() { configPath := flag.String("config", "services/wallet-service/configs/config.yaml", "path to wallet-service config") appCode := flag.String("app-code", "", "exact App partition to replay") limit := flag.Int("limit", 0, "process-wide maximum rows, 1..500") execute := flag.Bool("execute", false, "publish the bounded batch; default is a no-write dry-run") confirmation := flag.String("confirm", "", "required execution confirmation") timeout := flag.Duration("timeout", 2*time.Minute, "total command timeout, at most 30m") var eventTypes repeatedFlag flag.Var(&eventTypes, "event-type", "exact wallet event type to include; repeat for multiple types") flag.Parse() scope, err := outboxreplay.NewScope(*appCode, *limit, 500) if err != nil { fatal(err) } if *timeout <= 0 || *timeout > 30*time.Minute { fatal(errors.New("timeout must be within (0,30m]")) } if *execute && strings.TrimSpace(*confirmation) != outboxreplay.ExecuteConfirmation { fatal(fmt.Errorf("--execute requires --confirm=%s", outboxreplay.ExecuteConfirmation)) } cfg, err := config.Load(strings.TrimSpace(*configPath)) if err != nil { fatal(err) } normalizedTypes, err := app.ValidateWalletReplayEventTypes(cfg, eventTypes) if err != nil { fatal(err) } if !*execute { fmt.Printf("mode=dry-run app_code=%s limit=%d event_types=%s no_writes=true\n", scope.AppCode, scope.Limit, strings.Join(normalizedTypes, ",")) return } ctx, cancel := context.WithTimeout(context.Background(), *timeout) defer cancel() result, err := app.ReplayWalletOutboxOnce(ctx, cfg, scope, normalizedTypes) if err != nil { fatal(fmt.Errorf("wallet replay stopped after %d rows: %w", result.Processed, err)) } fmt.Printf("mode=execute app_code=%s limit=%d processed=%d drained=%t event_types=%s\n", scope.AppCode, scope.Limit, result.Processed, result.Drained, strings.Join(normalizedTypes, ",")) } func fatal(err error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) }