feat(go): add --dry-run to fuj sync
All checks were successful
Deploy to K8s / deploy (push) Successful in 18s

Mirror fuj infer's read-only mode: SyncOpts.DryRun skips WriteHeader,
AppendValues, and SortByDateColumn, printing one "Dry run: would …"
line per planned operation instead. ID-dedup still runs so the output
reflects exactly what the next real sync would write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:33:55 +02:00
parent 6465e2a221
commit 36a28a40d2
5 changed files with 86 additions and 5 deletions

View File

@@ -134,8 +134,9 @@ func syncCmd(args []string) {
fromStr := fs.String("from", "", "start date YYYY-MM-DD")
toStr := fs.String("to", "", "end date YYYY-MM-DD")
sort := fs.Bool("sort", true, "sort sheet by date after appending")
dryRun := fs.Bool("dry-run", false, "print planned writes without modifying the sheet")
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: fuj sync [--days N] [--from YYYY-MM-DD --to YYYY-MM-DD] [--sort]")
fmt.Fprintln(os.Stderr, "usage: fuj sync [--days N] [--from YYYY-MM-DD --to YYYY-MM-DD] [--sort] [--dry-run]")
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
@@ -153,7 +154,7 @@ func syncCmd(args []string) {
}
fioCli := fio.New(cfg.FioAPIToken, config.IBANAccountNum(cfg.BankAccount), nil)
opts := banksync.SyncOpts{Days: *days, Sort: *sort}
opts := banksync.SyncOpts{Days: *days, Sort: *sort, DryRun: *dryRun}
if *fromStr != "" && *toStr != "" {
opts.From, err = time.Parse("2006-01-02", *fromStr)
if err != nil {
@@ -172,7 +173,11 @@ func syncCmd(args []string) {
fmt.Fprintf(os.Stderr, "fuj sync: %v\n", err)
os.Exit(1)
}
fmt.Printf("Synced %d new transaction(s).\n", n)
if *dryRun {
fmt.Printf("Dry run: would sync %d new transaction(s).\n", n)
} else {
fmt.Printf("Synced %d new transaction(s).\n", n)
}
}
func inferCmd(args []string) {

View File

@@ -30,6 +30,7 @@ type SyncOpts struct {
Days int // look-back window when From/To are zero
From, To time.Time // explicit window (overrides Days)
Sort bool // sort the sheet by Date after appending
DryRun bool // print planned writes without modifying the sheet
}
// SyncToSheets fetches Fio transactions and appends new ones to the payments sheet.
@@ -52,8 +53,12 @@ func SyncToSheets(
if len(rows) > 0 {
header := rows[0]
if !headerMatches(header) {
if err := sh.WriteHeader(ctx, spreadsheetID, columnLabels); err != nil {
return 0, fmt.Errorf("sync: write header: %w", err)
if opts.DryRun {
fmt.Println("Dry run: would write header row")
} else {
if err := sh.WriteHeader(ctx, spreadsheetID, columnLabels); err != nil {
return 0, fmt.Errorf("sync: write header: %w", err)
}
}
} else {
for _, row := range rows[1:] {
@@ -113,6 +118,17 @@ func SyncToSheets(
return 0, nil
}
if opts.DryRun {
for _, row := range newRows {
fmt.Printf("Dry run: would append date=%v amount=%v sender=%v vs=%v message=%v\n",
row[0], row[1], row[6], row[7], row[8])
}
if opts.Sort {
fmt.Println("Dry run: would sort by date")
}
return len(newRows), nil
}
if err := sh.AppendValues(ctx, spreadsheetID, "A2", newRows); err != nil {
return 0, fmt.Errorf("sync: append: %w", err)
}

View File

@@ -127,6 +127,23 @@ func TestSyncToSheets_ExplicitDateWindow(t *testing.T) {
}
}
func TestSyncToSheets_DryRun(t *testing.T) {
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
fioFake := &fio.Fake{Transactions: testFioTxns}
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh,
SyncOpts{Days: 30, Sort: true, DryRun: true})
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Errorf("want 2 planned, got %d", n)
}
if len(sh.Appended) != 0 {
t.Error("dry-run must not call AppendValues")
}
}
// syncIDFor mirrors what SyncToSheets computes for a given fio.Transaction.
func syncIDFor(tx fio.Transaction) string {
currency := tx.Currency