feat(go): add --print-fio-table flag to fuj sync --dry-run
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
Prints an aligned tabwriter table of every Fio transaction in the look-back window, with a STATUS column showing NEW (would be appended) or DUP (already in sheet). Only fires when --dry-run is also set, so it can't affect real syncs. Refactors Sync ID computation into a single pre-pass shared by both the table printer and the row builder. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
29
docs/plans/2026-05-07-1321-fuj-sync-print-fio-table.md
Normal file
29
docs/plans/2026-05-07-1321-fuj-sync-print-fio-table.md
Normal file
@@ -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)
|
||||||
@@ -135,8 +135,9 @@ func syncCmd(args []string) {
|
|||||||
toStr := fs.String("to", "", "end date YYYY-MM-DD")
|
toStr := fs.String("to", "", "end date YYYY-MM-DD")
|
||||||
sort := fs.Bool("sort", true, "sort sheet by date after appending")
|
sort := fs.Bool("sort", true, "sort sheet by date after appending")
|
||||||
dryRun := fs.Bool("dry-run", false, "print planned writes without modifying the sheet")
|
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() {
|
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()
|
fs.PrintDefaults()
|
||||||
}
|
}
|
||||||
if err := fs.Parse(args); err != nil {
|
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)
|
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 != "" {
|
if *fromStr != "" && *toStr != "" {
|
||||||
opts.From, err = time.Parse("2006-01-02", *fromStr)
|
opts.From, err = time.Parse("2006-01-02", *fromStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
32
go/internal/services/banksync/fio_table.go
Normal file
32
go/internal/services/banksync/fio_table.go
Normal file
@@ -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]) + "…"
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"fuj-management/go/internal/domain/synch"
|
"fuj-management/go/internal/domain/synch"
|
||||||
"fuj-management/go/internal/io/fio"
|
"fuj-management/go/internal/io/fio"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,7 @@ type SyncOpts struct {
|
|||||||
From, To time.Time // explicit window (overrides Days)
|
From, To time.Time // explicit window (overrides Days)
|
||||||
Sort bool // sort the sheet by Date after appending
|
Sort bool // sort the sheet by Date after appending
|
||||||
DryRun bool // print planned writes without modifying the sheet
|
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.
|
// 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))
|
from.Format("2006-01-02"), to.Format("2006-01-02"), len(txns))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Append new rows.
|
// 4a. Compute Sync IDs for every fetched txn (shared by table-print and row-build).
|
||||||
var newRows [][]any
|
syncIDs := make([]string, len(txns))
|
||||||
for _, tx := range txns {
|
for i, tx := range txns {
|
||||||
currency := tx.Currency
|
currency := tx.Currency
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
currency = "CZK"
|
currency = "CZK"
|
||||||
}
|
}
|
||||||
id := synch.GenerateSyncID(synch.Transaction{
|
syncIDs[i] = synch.GenerateSyncID(synch.Transaction{
|
||||||
Date: tx.Date,
|
Date: tx.Date,
|
||||||
Amount: tx.Amount,
|
Amount: tx.Amount,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
@@ -108,13 +110,23 @@ func SyncToSheets(
|
|||||||
Message: tx.Message,
|
Message: tx.Message,
|
||||||
BankID: tx.BankID,
|
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
|
continue
|
||||||
}
|
}
|
||||||
newRows = append(newRows, []any{
|
newRows = append(newRows, []any{
|
||||||
tx.Date, tx.Amount,
|
tx.Date, tx.Amount,
|
||||||
"", "", "", "", // manual fix, Person, Purpose, Inferred 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],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user