All checks were successful
Deploy to K8s / deploy (push) Successful in 24s
Add second Fio account (CZ0820100000002502035405 / 2502035405/2010). Both accounts are fetched on every sync run and combined before dedup, so the payments sheet accumulates transactions from either account. QR codes now default to the new account. Go: - config.go: hardcoded Accounts/LoadedAccount slice replaces scalar BankAccount + FioAPIToken; Config.BankAccount renamed QRAccount; per-account tokens via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD - banksync.SyncToSheets: accepts []fio.Client, loops to combine txns - cmd/fuj/main.go: buildFioClients helper; both sync call sites updated - html_handler + build_adults/juniors: use Config.QRAccount - New TestSyncToSheets_MultiAccount covers cross-account dedup Python: - config.py: ACCOUNTS list + LOADED_ACCOUNTS (tokens from env) - fio_utils.py: fetch_transactions_for (per-account) + fetch_transactions_all (loops all accounts) - sync_fio_to_sheets.py: uses fetch_transactions_all Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
192 lines
6.0 KiB
Go
192 lines
6.0 KiB
Go
package banksync
|
|
|
|
import (
|
|
"context"
|
|
"fuj-management/go/internal/domain/synch"
|
|
"fuj-management/go/internal/io/fio"
|
|
"fuj-management/go/internal/io/sheets"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var testFioTxns = []fio.Transaction{
|
|
{Date: "2026-04-10", Amount: 750, Sender: "Jana Novakova", Message: "duben 2026", VS: "123", BankID: "111"},
|
|
{Date: "2026-04-11", Amount: 500, Sender: "Petr Prach", Message: "april", VS: "456", BankID: "222"},
|
|
}
|
|
|
|
func TestSyncToSheets_EmptySheet(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 2 {
|
|
t.Errorf("want 2 appended, got %d", n)
|
|
}
|
|
if len(sh.Appended) != 1 {
|
|
t.Fatalf("want 1 AppendValues call, got %d", len(sh.Appended))
|
|
}
|
|
rows := sh.Appended[0].Rows
|
|
if len(rows) != 2 {
|
|
t.Errorf("want 2 rows in append call, got %d", len(rows))
|
|
}
|
|
// Sync ID should be in column 10 (index 10)
|
|
if syncID, ok := rows[0][10].(string); !ok || len(syncID) != 64 {
|
|
t.Errorf("expected 64-char hex sync ID, got %v", rows[0][10])
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_Dedup(t *testing.T) {
|
|
// Seed the sheet with an existing sync ID matching testFioTxns[0]
|
|
firstID := syncIDFor(testFioTxns[0])
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"},
|
|
{"2026-04-10", 750.0, "", "", "", "", "Jana Novakova", "123", "duben 2026", "111", firstID},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 new row (one deduped), got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_NoNewTxns(t *testing.T) {
|
|
first := syncIDFor(testFioTxns[0])
|
|
second := syncIDFor(testFioTxns[1])
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"},
|
|
{"2026-04-10", 750.0, "", "", "", "", "Jana Novakova", "123", "duben 2026", "111", first},
|
|
{"2026-04-11", 500.0, "", "", "", "", "Petr Prach", "456", "april", "222", second},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 0 {
|
|
t.Errorf("want 0 new rows, got %d", n)
|
|
}
|
|
if len(sh.Appended) != 0 {
|
|
t.Error("expected no AppendValues call when all deduped")
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_MissingHeader(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Wrong", "Headers"},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 row appended after header fix, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_Sort(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
_, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{Days: 30, Sort: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// SortByDateColumn should have been called on the fake — check via a spy fake
|
|
}
|
|
|
|
func TestSyncToSheets_ExplicitDateWindow(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
from := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
|
to := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh, SyncOpts{From: from, To: to})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 row, got %d", n)
|
|
}
|
|
}
|
|
|
|
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", []fio.Client{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")
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_MultiAccount(t *testing.T) {
|
|
txnsA := []fio.Transaction{
|
|
{Date: "2026-04-10", Amount: 700, Sender: "Alice", Message: "april", VS: "1", BankID: "A1"},
|
|
}
|
|
txnsB := []fio.Transaction{
|
|
{Date: "2026-04-11", Amount: 500, Sender: "Bob", Message: "duben", VS: "2", BankID: "B1"},
|
|
}
|
|
// One transaction that duplicates the first one from account A (same sync_id).
|
|
dupID := syncIDFor(txnsA[0])
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"},
|
|
{"2026-04-10", 700.0, "", "", "", "", "Alice", "1", "april", "A1", dupID},
|
|
},
|
|
}}
|
|
fakeA := &fio.Fake{Transactions: txnsA}
|
|
fakeB := &fio.Fake{Transactions: txnsB}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fakeA, fakeB}, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 new row (B1 from account B; A1 is duplicate), got %d", n)
|
|
}
|
|
if len(sh.Appended) != 1 || len(sh.Appended[0].Rows) != 1 {
|
|
t.Fatalf("want exactly 1 row appended, got %v", sh.Appended)
|
|
}
|
|
row := sh.Appended[0].Rows[0]
|
|
if row[6] != "Bob" {
|
|
t.Errorf("expected Bob's row, got sender=%v", row[6])
|
|
}
|
|
}
|
|
|
|
// syncIDFor mirrors what SyncToSheets computes for a given fio.Transaction.
|
|
func syncIDFor(tx fio.Transaction) string {
|
|
currency := tx.Currency
|
|
if currency == "" {
|
|
currency = "CZK"
|
|
}
|
|
return synch.GenerateSyncID(synch.Transaction{
|
|
Date: tx.Date, Amount: tx.Amount, Currency: currency,
|
|
Sender: tx.Sender, VS: tx.VS, Message: tx.Message, BankID: tx.BankID,
|
|
})
|
|
}
|