Skip to content

Commit

Permalink
Complete rewrite (#7)
Browse files Browse the repository at this point in the history
- Adds an API client
- CSV formatting has its own package and tests
- Supports pagination

Closes #5
  • Loading branch information
anothertobi committed May 24, 2023
1 parent 89326d2 commit f183fa4
Show file tree
Hide file tree
Showing 12 changed files with 419 additions and 127 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Little helper to get transactions from Viseca One and print them in CSV format.

1. ```
source .env
go run viseca-exporter.go "$CARDID" "$COOKIE" > data/export.csv
go run main.go "$CARDID" "$COOKIE" > data/export.csv
```


Expand All @@ -22,9 +22,17 @@ Little helper to get transactions from Viseca One and print them in CSV format.
### Env file
```
export CARDID=0000000AAAAA0000
export COOKIE=AL_SESS-S=AAAAAAAAAA...
export COOKIE=AAAAAAAAAA...
```

## API

### Known issues

The API returns `500 Internal Server Error` without any additional information when a request doesn't meet the API requirements.

Large page sizes (e.g. 1000) lead to an error.

### API Output

```json
Expand Down
46 changes: 46 additions & 0 deletions csv/transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package csv

import (
"fmt"
"strings"

"github.com/anothertobi/viseca-exporter/viseca"
)

// TransactionsString() returns a CSV representation of the transactions.
func TransactionsString(transactions viseca.Transactions) string {
var stringBuilder strings.Builder

stringBuilder.WriteString(`"TransactionID","Date","Merchant","Amount","PFMCategoryID","PFMCategoryName"`)
stringBuilder.WriteString("\n")

for _, transaction := range transactions.Transactions {
stringBuilder.WriteString(TransactionString(transaction))
stringBuilder.WriteString("\n")
}

return stringBuilder.String()
}

// TransactionString returns a CSV record.
func TransactionString(transaction viseca.Transaction) string {
innerRecord := strings.Join([]string{
transaction.TransactionID,
transaction.Date,
prettiestMerchantName(transaction),
fmt.Sprintf("%.2f", transaction.Amount),
transaction.PFMCategory.ID,
transaction.PFMCategory.Name,
}, `","`)

return fmt.Sprintf(`"%s"`, innerRecord)
}

// prettiestMerchantName extracts the prettiest merchant name from a transaction.
func prettiestMerchantName(transaction viseca.Transaction) string {
if transaction.PrettyName != "" {
return transaction.PrettyName
} else {
return transaction.MerchantName
}
}
59 changes: 59 additions & 0 deletions csv/transactions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package csv_test

import (
"testing"

"github.com/anothertobi/viseca-exporter/csv"
"github.com/anothertobi/viseca-exporter/viseca"
"github.com/stretchr/testify/assert"
)

var inputTransaction = viseca.Transaction{
TransactionID: "AUTH8c919db2-1c23-43f1-8862-61c31336d9b6",
CardID: "0000000AAAAA0000",
MaskedCardNumber: "XXXXXXXXXXXX0000",
CardName: "Mastercard",
Date: "2021-10-20T17:05:44",
ShowTimestamp: true,
Amount: 50.55,
Currency: "CHF",
OriginalAmount: 50.55,
OriginalCurrency: "CHF",
MerchantName: "Aldi Suisse 00",
PrettyName: "ALDI",
MerchantPlace: "",
IsOnline: false,
PFMCategory: viseca.PFMCategory{
ID: "cv_groceries",
Name: "Lebensmittel",
LightColor: "#E2FDD3",
MediumColor: "#A5D58B",
Color: "#51A127",
ImageURL: "https://api.one.viseca.ch/v1/media/categories/icon_with_background/ic_cat_tile_groceries_v2.png",
TransparentImageURL: "https://api.one.viseca.ch/v1/media/categories/icon_without_background/ic_cat_tile_groceries_v2.png",
},
StateType: "authorized",
Details: "Aldi Suisse 00",
Type: "merchant",
IsBilled: false,
Links: viseca.TransactionLinks{
Transactiondetails: "/v1/card/0000000AAAAA0000/transaction/AUTH8c919db2-1c23-43f1-8862-61c31336d9b6",
},
}

func TestTransactionString(t *testing.T) {
expected := `"AUTH8c919db2-1c23-43f1-8862-61c31336d9b6","2021-10-20T17:05:44","ALDI","50.55","cv_groceries","Lebensmittel"`

assert.Equal(t, expected, csv.TransactionString(inputTransaction))
}

func TestTransactionsString(t *testing.T) {
inputTransactions := viseca.Transactions{Transactions: []viseca.Transaction{inputTransaction}}
expected :=
`"TransactionID","Date","Merchant","Amount","PFMCategoryID","PFMCategoryName"` +
"\n" +
`"AUTH8c919db2-1c23-43f1-8862-61c31336d9b6","2021-10-20T17:05:44","ALDI","50.55","cv_groceries","Lebensmittel"` +
"\n"

assert.Equal(t, expected, csv.TransactionsString(inputTransactions))
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
module github.com/anothertobi/viseca-exporter

go 1.19

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
75 changes: 75 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"context"
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"os"
"strings"

"github.com/anothertobi/viseca-exporter/csv"
"github.com/anothertobi/viseca-exporter/viseca"
)

const sessionCookieName = "AL_SESS-S"

// arg0: cardID
// arg1: sessionCookie (e.g. `AL_SESS-S=...`)
func main() {
if len(os.Args) < 3 {
log.Fatal("card ID and session cookie args required")
}
visecaClient, err := initClient(os.Args[2])
if err != nil {
log.Fatalf("error initializing Viseca API client: %v", err)
}

transactions, err := ListAllTransactions(visecaClient, os.Args[1])
if err != nil {
log.Fatal(err)
}
fmt.Println(csv.TransactionsString(*transactions))
}

func initClient(sessionCookie string) (*viseca.Client, error) {
cookieJar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
httpClient := http.Client{
Jar: cookieJar,
}
visecaClient := viseca.NewClient(&httpClient)
cookie := &http.Cookie{
Name: sessionCookieName,
Value: extractSessionCookieValue(sessionCookie),
}
httpClient.Jar.SetCookies(visecaClient.BaseURL, []*http.Cookie{cookie})

return visecaClient, nil
}

func ListAllTransactions(visecaClient *viseca.Client, cardID string) (*viseca.Transactions, error) {
ctx := context.Background()
listOptions := viseca.NewDefaultListOptions()
transactions, err := visecaClient.ListTransactions(ctx, cardID, listOptions)
if err != nil {
return nil, err
}
for listOptions.Offset+listOptions.PageSize < transactions.TotalCount {
listOptions.Offset += listOptions.PageSize
transactionsPage, err := visecaClient.ListTransactions(ctx, cardID, listOptions)
if err != nil {
return nil, err
}
transactions.Transactions = append(transactions.Transactions, transactionsPage.Transactions...)
}

return transactions, nil
}

func extractSessionCookieValue(sessionCookie string) string {
return strings.TrimPrefix(sessionCookie, fmt.Sprintf("%s=", sessionCookieName))
}
125 changes: 0 additions & 125 deletions viseca-exporter.go

This file was deleted.

25 changes: 25 additions & 0 deletions viseca/card.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package viseca

import (
"context"
"fmt"
)

func (client *Client) ListTransactions(ctx context.Context, card string, listOptions ListOptions) (*Transactions, error) {
path := fmt.Sprintf("card/%s/transactions", card)

request, err := client.NewRequest(path, "GET", nil)
if err != nil {
return nil, err
}
addListOptions(request.URL, listOptions)

transactions := &Transactions{}

_, err = client.Do(ctx, request, transactions)
if err != nil {
return nil, err
}

return transactions, err
}
Loading

0 comments on commit f183fa4

Please sign in to comment.