Compare commits
5 Commits
7a946b7604
...
9cf8cb0736
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cf8cb0736 | ||
|
|
4997264f4c | ||
|
|
f69a850bd8 | ||
|
|
5a3d5e5610 | ||
|
|
c38e8529af |
@@ -248,7 +248,7 @@ func Main() {
|
|||||||
|
|
||||||
inDay := func(date string, transaction bank.Transaction) bool {
|
inDay := func(date string, transaction bank.Transaction) bool {
|
||||||
return slices.ContainsFunc(byDate[date], func(d ledger.Delta) bool {
|
return slices.ContainsFunc(byDate[date], func(d ledger.Delta) bool {
|
||||||
return d.Value == transaction.Amount
|
return d.Value == transaction.Amount || -1.0*d.Value == transaction.Amount
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
cmd/main.go
18
cmd/main.go
@@ -1,14 +1,32 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/cmd/cli"
|
"gogs.inhome.blapointe.com/ana-ledger/cmd/cli"
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/cmd/http"
|
"gogs.inhome.blapointe.com/ana-ledger/cmd/http"
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
case "tel":
|
||||||
|
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||||
|
defer can()
|
||||||
|
|
||||||
|
if c, err := teller.New(); err != nil {
|
||||||
|
} else if _, err := c.Accounts(ctx); err != nil {
|
||||||
|
} else {
|
||||||
|
log.Println("teller already init")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := teller.Init(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
case "http":
|
case "http":
|
||||||
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
|
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
|
||||||
http.Main()
|
http.Main()
|
||||||
|
|||||||
29
src/bank/cache/cache.go
vendored
29
src/bank/cache/cache.go
vendored
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
@@ -22,7 +23,11 @@ func New(client bank.Agg) Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
|
func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
|
||||||
if result := []bank.Account{}; fromCache("accounts", &result) == nil {
|
k := "accounts"
|
||||||
|
result := []bank.Account{}
|
||||||
|
if err := fromCache(k, &result); err != nil {
|
||||||
|
log.Printf("%q not in cache: %v", k, err)
|
||||||
|
} else {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +36,17 @@ func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
toCache("accounts", result)
|
toCache(k, result)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
|
func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transaction, error) {
|
||||||
if result := []bank.Transaction{}; fromCache(path.Join("accounts", a.Account), &result) == nil {
|
k := path.Join("accounts.d", a.Account)
|
||||||
|
result := []bank.Transaction{}
|
||||||
|
if err := fromCache(k, &result); err != nil {
|
||||||
|
log.Printf("%q not in cache: %v", k, err)
|
||||||
|
} else {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +55,7 @@ func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transa
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
toCache(path.Join("accounts", a.Account), result)
|
toCache(k, result)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -56,15 +65,25 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func toCache(k string, v interface{}) {
|
func toCache(k string, v interface{}) {
|
||||||
|
if err := _toCache(k, v); err != nil {
|
||||||
|
log.Printf("failed to cache %s: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _toCache(k string, v interface{}) error {
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := path.Join(d, k)
|
p := path.Join(d, k)
|
||||||
|
os.MkdirAll(path.Dir(p), os.ModePerm)
|
||||||
if err := os.WriteFile(p, b, os.ModePerm); err != nil {
|
if err := os.WriteFile(p, b, os.ModePerm); err != nil {
|
||||||
os.Remove(p)
|
os.Remove(p)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromCache(k string, ptr interface{}) error {
|
func fromCache(k string, ptr interface{}) error {
|
||||||
|
|||||||
9
src/bank/cache/cache_integration_test.go
vendored
9
src/bank/cache/cache_integration_test.go
vendored
@@ -12,12 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
func Test(t *testing.T) {
|
||||||
c, err := teller.New()
|
tellerC, err := teller.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
client := cache.New(tellerC)
|
||||||
client := cache.New(c)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ func Test(t *testing.T) {
|
|||||||
i := i
|
i := i
|
||||||
client := client
|
client := client
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
accounts, err := c.Accounts(ctx)
|
accounts, err := client.Accounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -33,7 +32,7 @@ func Test(t *testing.T) {
|
|||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
account := account
|
account := account
|
||||||
t.Run(account.Account, func(t *testing.T) {
|
t.Run(account.Account, func(t *testing.T) {
|
||||||
transactions, err := c.Transactions(ctx, account)
|
transactions, err := client.Transactions(ctx, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/bank/teller/application_id.txt
Normal file
1
src/bank/teller/application_id.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
app_pdvv33dtmta4fema66000
|
||||||
74
src/bank/teller/init.go
Normal file
74
src/bank/teller/init.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package teller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed application_id.txt
|
||||||
|
applicationId string
|
||||||
|
//go:embed init.html
|
||||||
|
initHTML string
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(ctx context.Context) error {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
if Token == "" {
|
||||||
|
} else if fmt.Println("Token already exists; are you sure [nY]?"); false {
|
||||||
|
} else if text, _ := reader.ReadString('\n'); !strings.Contains(text, "Y") {
|
||||||
|
return fmt.Errorf("token already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
environment := "development"
|
||||||
|
if sandbox := !slices.Contains(os.Args, "forreal"); sandbox {
|
||||||
|
environment = "sandbox"
|
||||||
|
}
|
||||||
|
fmt.Printf("environment=%q\n", environment)
|
||||||
|
|
||||||
|
newTokens := make(chan string)
|
||||||
|
defer close(newTokens)
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: ":20000",
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
newTokens <- string(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.New("initHTML").Parse(initHTML)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Execute(w, map[string]string{
|
||||||
|
"applicationId": applicationId,
|
||||||
|
"environment": environment,
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
go s.ListenAndServe()
|
||||||
|
|
||||||
|
fmt.Println("Open https://localhost:20000")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case newToken := <-newTokens:
|
||||||
|
return fmt.Errorf("not impl: %q => token.txt", newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
58
src/bank/teller/init.html
Normal file
58
src/bank/teller/init.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<button id="teller-connect">Connect to your bank</button>
|
||||||
|
|
||||||
|
<h3 id="log">
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<script src="https://cdn.teller.io/connect/connect.js"></script>
|
||||||
|
<script>
|
||||||
|
function logme(msg) {
|
||||||
|
document.getElementById("log").innerHTML += `<br>* ${msg}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function http(method, remote, callback, body) {
|
||||||
|
var xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange = function() {
|
||||||
|
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
|
||||||
|
callback(xmlhttp.responseText, xmlhttp.status)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xmlhttp.open(method, remote, true);
|
||||||
|
if (typeof body == "undefined") {
|
||||||
|
body = null
|
||||||
|
}
|
||||||
|
xmlhttp.send(body);
|
||||||
|
}
|
||||||
|
function callback(responseBody, responseStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var tellerConnect = TellerConnect.setup({
|
||||||
|
applicationId: "{{.applicationId}}",
|
||||||
|
environment: "{{.environment}}",
|
||||||
|
products: ["verify", "balance", "transactions"],
|
||||||
|
onInit: function() {
|
||||||
|
logme("Teller Connect has initialized")
|
||||||
|
},
|
||||||
|
onSuccess: function(enrollment) {
|
||||||
|
logme(`User enrolled successfully: ${enrollment.accessToken}`)
|
||||||
|
http("post", "/", callback, enrollment.accessToken)
|
||||||
|
},
|
||||||
|
onExit: function() {
|
||||||
|
logme("User closed Teller Connect")
|
||||||
|
},
|
||||||
|
onFailure: function(failure) {
|
||||||
|
logme(`Failed: type=${failure.type} code=${failure.code} message=${failure.message}`)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = document.getElementById("teller-connect");
|
||||||
|
el.addEventListener("click", function() {
|
||||||
|
tellerConnect.open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,7 +5,10 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
|
||||||
@@ -22,6 +25,8 @@ var (
|
|||||||
certificate []byte
|
certificate []byte
|
||||||
//go:embed private_key.pem
|
//go:embed private_key.pem
|
||||||
privateKey []byte
|
privateKey []byte
|
||||||
|
//go:embed token.txt
|
||||||
|
Token string
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() (Client, error) {
|
func New() (Client, error) {
|
||||||
@@ -54,7 +59,7 @@ func (c Client) get(ctx context.Context, url string, ptr interface{}) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.SetBasicAuth("test_token_bfu2cyvq3il6o", "") // TODO
|
req.SetBasicAuth(strings.TrimSpace(Token), "")
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := httpc.Do(req)
|
resp, err := httpc.Do(req)
|
||||||
@@ -63,5 +68,9 @@ func (c Client) get(ctx context.Context, url string, ptr interface{}) error {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
return json.NewDecoder(resp.Body).Decode(ptr)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if err := json.Unmarshal(b, &ptr); err != nil {
|
||||||
|
return fmt.Errorf("cannot unmarshal: %w: %s", err, b)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
func Test(t *testing.T) {
|
||||||
|
teller.Token = "test_token_bfu2cyvq3il6o"
|
||||||
|
|
||||||
c, err := teller.New()
|
c, err := teller.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegration(t *testing.T) {
|
func TestIntegration(t *testing.T) {
|
||||||
|
teller.Token = "test_token_bfu2cyvq3il6o"
|
||||||
|
|
||||||
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
|
//curl --cert certificate.pem --cert-key private_key.pem --auth test_token_bfu2cyvq3il6o: https://api.teller.io/accounts
|
||||||
cert, err := tls.LoadX509KeyPair("./certificate.pem", "./private_key.pem")
|
cert, err := tls.LoadX509KeyPair("./certificate.pem", "./private_key.pem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
1
src/bank/teller/token.txt
Normal file
1
src/bank/teller/token.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
token_2utqstwpn3pxwgvyno56hqdehq
|
||||||
Reference in New Issue
Block a user