diff --git a/docs/plans/2026-05-07-1321-fuj-sync-print-fio-table.md b/docs/plans/2026-05-07-1321-fuj-sync-print-fio-table.md new file mode 100644 index 0000000..5f4dd53 --- /dev/null +++ b/docs/plans/2026-05-07-1321-fuj-sync-print-fio-table.md @@ -0,0 +1,29 @@ +# Add `--print-fio-table` debug flag to `fuj sync` + +## Context + +The Go port of `fuj sync --dry-run` currently prints only the **new** +transactions — i.e. rows that will be appended to the payments sheet after +deduping against existing Sync IDs (see [sync.go:125-129](../../go/internal/services/banksync/sync.go#L125-L129)). +When debugging Fio sync issues ("why isn't transaction X showing up?", +"is the dedup working?"), there's no way to see what Fio actually +returned versus what got filtered as a duplicate. + +This change adds a `--print-fio-table` flag that, **only when combined +with `--dry-run`**, prints an aligned table of every Fio transaction in +the window with each row marked `NEW` (would be appended) or `DUP` +(already in sheet, skipped). The flag is silently ignored without +`--dry-run`, so it can't accidentally fire during a real sync. + +## Decisions + +- Flag name: `--print-fio-table` (specific, not generic `--verbose`). +- Columns: `DATE | AMOUNT | SENDER | VS | MESSAGE | BANKID | STATUS`, + with MESSAGE truncated and STATUS = `NEW` / `DUP`. +- Scope: only effective when `--dry-run` is also set. + +## Files modified + +- [go/cmd/fuj/main.go](../../go/cmd/fuj/main.go) — new flag + SyncOpts field +- [go/internal/services/banksync/sync.go](../../go/internal/services/banksync/sync.go) — SyncOpts struct + refactored step 4 +- [go/internal/services/banksync/debug.go](../../go/internal/services/banksync/debug.go) — printFioTable helper (new) diff --git a/go/cmd/fuj/main.go b/go/cmd/fuj/main.go index ebe8d0d..a55d3e0 100644 --- a/go/cmd/fuj/main.go +++ b/go/cmd/fuj/main.go @@ -135,8 +135,9 @@ func syncCmd(args []string) { 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") + printFioTable := fs.Bool("print-fio-table", false, "with --dry-run: print aligned table of every Fio transaction with NEW/DUP status") fs.Usage = func() { - fmt.Fprintln(os.Stderr, "usage: fuj sync [--days N] [--from YYYY-MM-DD --to YYYY-MM-DD] [--sort] [--dry-run]") + fmt.Fprintln(os.Stderr, "usage: fuj sync [--days N] [--from YYYY-MM-DD --to YYYY-MM-DD] [--sort] [--dry-run] [--print-fio-table]") fs.PrintDefaults() } if err := fs.Parse(args); err != nil { @@ -154,7 +155,7 @@ func syncCmd(args []string) { } fioCli := fio.New(cfg.FioAPIToken, config.IBANAccountNum(cfg.BankAccount), nil) - opts := banksync.SyncOpts{Days: *days, Sort: *sort, DryRun: *dryRun} + opts := banksync.SyncOpts{Days: *days, Sort: *sort, DryRun: *dryRun, PrintFioTable: *printFioTable} if *fromStr != "" && *toStr != "" { opts.From, err = time.Parse("2006-01-02", *fromStr) if err != nil { diff --git a/go/internal/services/banksync/fio_table.go b/go/internal/services/banksync/fio_table.go new file mode 100644 index 0000000..0fbb2e6 --- /dev/null +++ b/go/internal/services/banksync/fio_table.go @@ -0,0 +1,32 @@ +package banksync + +import ( + "fmt" + "io" + "text/tabwriter" + + "fuj-management/go/internal/io/fio" +) + +func printFioTable(w io.Writer, txns []fio.Transaction, syncIDs []string, existing map[string]bool) { + tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) + fmt.Fprintln(tw, "DATE\tAMOUNT\tSENDER\tVS\tMESSAGE\tBANKID\tSTATUS") + for i, tx := range txns { + status := "NEW" + if existing[syncIDs[i]] { + status = "DUP" + } + fmt.Fprintf(tw, "%s\t%.2f\t%s\t%s\t%s\t%s\t%s\n", + tx.Date, tx.Amount, tx.Sender, tx.VS, + truncRunes(tx.Message, 40), tx.BankID, status) + } + _ = tw.Flush() +} + +func truncRunes(s string, n int) string { + rs := []rune(s) + if len(rs) <= n { + return s + } + return string(rs[:n-1]) + "…" +} diff --git a/go/internal/services/banksync/sync.go b/go/internal/services/banksync/sync.go index a036c2e..95559d0 100644 --- a/go/internal/services/banksync/sync.go +++ b/go/internal/services/banksync/sync.go @@ -6,6 +6,7 @@ import ( "fmt" "fuj-management/go/internal/domain/synch" "fuj-management/go/internal/io/fio" + "os" "strings" "time" ) @@ -27,10 +28,11 @@ type sheetsWriter interface { // SyncOpts controls the date window and sort behaviour. 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 + 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 + PrintFioTable bool // with DryRun: print every fetched Fio txn with NEW/DUP status } // SyncToSheets fetches Fio transactions and appends new ones to the payments sheet. @@ -92,14 +94,14 @@ func SyncToSheets( from.Format("2006-01-02"), to.Format("2006-01-02"), len(txns)) } - // 4. Append new rows. - var newRows [][]any - for _, tx := range txns { + // 4a. Compute Sync IDs for every fetched txn (shared by table-print and row-build). + syncIDs := make([]string, len(txns)) + for i, tx := range txns { currency := tx.Currency if currency == "" { currency = "CZK" } - id := synch.GenerateSyncID(synch.Transaction{ + syncIDs[i] = synch.GenerateSyncID(synch.Transaction{ Date: tx.Date, Amount: tx.Amount, Currency: currency, @@ -108,13 +110,23 @@ func SyncToSheets( Message: tx.Message, BankID: tx.BankID, }) - if existingIDs[id] { + } + + // 4b. Optional debug table (dry-run only). + if opts.DryRun && opts.PrintFioTable { + printFioTable(os.Stdout, txns, syncIDs, existingIDs) + } + + // 4c. Build new rows. + var newRows [][]any + for i, tx := range txns { + if existingIDs[syncIDs[i]] { continue } newRows = append(newRows, []any{ tx.Date, tx.Amount, "", "", "", "", // manual fix, Person, Purpose, Inferred Amount - tx.Sender, tx.VS, tx.Message, tx.BankID, id, + tx.Sender, tx.VS, tx.Message, tx.BankID, syncIDs[i], }) }