Files
fuj-management/go/internal/io/fio/api.go
T
kacerrandClaude Opus 5.5 022facc957
Deploy to K8s / deploy (push) Successful in 20s
refactor: move Python implementation to legacy/, make Go primary
Go (go/) becomes the primary implementation. The original Python
version moves to legacy/ and is kept runnable; new features are still
built in both (see the Dual implementation rule in CLAUDE.md).

- git mv app.py, scripts/, templates/, tests/, pyproject.toml, uv.lock
  and build/ into legacy/
- Makefile: Python targets point at legacy/, venv in legacy/.venv,
  CREDENTIALS_PATH exported for the root .secret/ file
- CI and make image build the Python image with legacy/ as context;
  the Makefile is no longer copied into the image
- legacy/.dockerignore moved to the context root so it takes effect
- CLAUDE.md, docs/README.md, Go comments, fixture README, capture
  script and VS Code launch config updated to the new layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:33:57 +02:00

137 lines
3.6 KiB
Go

package fio
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
)
// httpDoer is the subset of *http.Client used by both Fio impls.
type httpDoer interface {
Do(*http.Request) (*http.Response, error)
}
// apiClient fetches transactions from the Fio REST API (JSON).
// Ports legacy/scripts/fio_utils.py fetch_transactions_api.
type apiClient struct {
token string
hc httpDoer
}
func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) ([]Transaction, error) {
const layout = "2006-01-02"
url := fmt.Sprintf("https://fioapi.fio.cz/v1/rest/periods/%s/%s/%s/transactions.json",
c.token, from.Format(layout), to.Format(layout))
slog.Debug("fio api: GET",
"url", strings.Replace(url, c.token, "****", 1),
"from", from.Format(layout), "to", to.Format(layout))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
slog.Debug("fio api: response", "status", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fio api: HTTP %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
txns, err := parseAPIResponse(body)
slog.Debug("fio api: parsed", "body_bytes", len(body), "parsed_count", len(txns))
return txns, err
}
// fioAPIResponse is the top-level envelope from the Fio JSON API.
type fioAPIResponse struct {
AccountStatement struct {
TransactionList struct {
Transaction []map[string]json.RawMessage `json:"transaction"`
} `json:"transactionList"`
} `json:"accountStatement"`
}
func parseAPIResponse(body []byte) ([]Transaction, error) {
var resp fioAPIResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("fio api: parse JSON: %w", err)
}
var txns []Transaction
for _, raw := range resp.AccountStatement.TransactionList.Transaction {
amount := colFloat(raw, "column1")
if amount <= 0 {
continue // skip outgoing
}
dateRaw := colString(raw, "column0")
dateStr := ""
if len(dateRaw) >= 10 {
dateStr = dateRaw[:10]
}
txns = append(txns, Transaction{
Date: dateStr,
Amount: amount,
Sender: colString(raw, "column10"),
Message: colString(raw, "column16"),
VS: colString(raw, "column5"),
KS: colString(raw, "column4"),
SS: colString(raw, "column6"),
UserID: colString(raw, "column7"),
SenderAccount: colString(raw, "column2"),
BankID: colString(raw, "column22"),
Currency: colStringOr(raw, "column14", "CZK"),
})
}
return txns, nil
}
// colString extracts {"value":…} as a string from a column map.
func colString(m map[string]json.RawMessage, col string) string {
raw, ok := m[col]
if !ok {
return ""
}
var cell struct {
Value *string `json:"value"`
}
if json.Unmarshal(raw, &cell) != nil || cell.Value == nil {
return ""
}
return *cell.Value
}
// colStringOr is colString with a fallback value.
func colStringOr(m map[string]json.RawMessage, col, fallback string) string {
if v := colString(m, col); v != "" {
return v
}
return fallback
}
// colFloat extracts {"value":…} as a float64 from a column map.
// Returns 0 on any error (null column, non-numeric value).
func colFloat(m map[string]json.RawMessage, col string) float64 {
raw, ok := m[col]
if !ok {
return 0
}
var cell struct {
Value *float64 `json:"value"`
}
if json.Unmarshal(raw, &cell) != nil || cell.Value == nil {
return 0
}
return *cell.Value
}