Compare commits

...

5 Commits

Author SHA1 Message Date
Bel LaPointe
9cf8cb0736 rec ok with negatives/positives
Some checks failed
cicd / ci (push) Failing after 21s
2025-05-23 21:59:19 -06:00
Bel LaPointe
4997264f4c fix cache test 2025-05-23 21:56:36 -06:00
Bel LaPointe
f69a850bd8 got a silly ui that can yield a test token 2025-05-23 21:38:07 -06:00
Bel LaPointe
5a3d5e5610 stub teller.Init 2025-05-23 21:12:27 -06:00
Bel LaPointe
c38e8529af ready to get a real token 2025-05-23 21:09:26 -06:00
11 changed files with 198 additions and 13 deletions

View File

@@ -248,7 +248,7 @@ func Main() {
inDay := func(date string, transaction bank.Transaction) 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
})
}

View File

@@ -1,14 +1,32 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"gogs.inhome.blapointe.com/ana-ledger/cmd/cli"
"gogs.inhome.blapointe.com/ana-ledger/cmd/http"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
func main() {
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":
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
http.Main()

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path"
"time"
@@ -22,7 +23,11 @@ func New(client bank.Agg) Client {
}
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
}
@@ -31,13 +36,17 @@ func (c Client) Accounts(ctx context.Context) ([]bank.Account, error) {
return nil, err
}
toCache("accounts", result)
toCache(k, result)
return result, nil
}
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
}
@@ -46,7 +55,7 @@ func (c Client) Transactions(ctx context.Context, a bank.Account) ([]bank.Transa
return nil, err
}
toCache(path.Join("accounts", a.Account), result)
toCache(k, result)
return result, nil
}
@@ -56,15 +65,25 @@ var (
)
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)
if err != nil {
return
return err
}
p := path.Join(d, k)
os.MkdirAll(path.Dir(p), os.ModePerm)
if err := os.WriteFile(p, b, os.ModePerm); err != nil {
os.Remove(p)
return err
}
return nil
}
func fromCache(k string, ptr interface{}) error {

View File

@@ -12,12 +12,11 @@ import (
)
func Test(t *testing.T) {
c, err := teller.New()
tellerC, err := teller.New()
if err != nil {
t.Fatal(err)
}
client := cache.New(c)
client := cache.New(tellerC)
ctx := context.Background()
@@ -25,7 +24,7 @@ func Test(t *testing.T) {
i := i
client := client
t.Run(strconv.Itoa(i), func(t *testing.T) {
accounts, err := c.Accounts(ctx)
accounts, err := client.Accounts(ctx)
if err != nil {
t.Fatal(err)
}
@@ -33,7 +32,7 @@ func Test(t *testing.T) {
for _, account := range accounts {
account := account
t.Run(account.Account, func(t *testing.T) {
transactions, err := c.Transactions(ctx, account)
transactions, err := client.Transactions(ctx, account)
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1 @@
app_pdvv33dtmta4fema66000

74
src/bank/teller/init.go Normal file
View 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
View 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>

View File

@@ -5,7 +5,10 @@ import (
"crypto/tls"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/bank"
@@ -22,6 +25,8 @@ var (
certificate []byte
//go:embed private_key.pem
privateKey []byte
//go:embed token.txt
Token string
)
func New() (Client, error) {
@@ -54,7 +59,7 @@ func (c Client) get(ctx context.Context, url string, ptr interface{}) error {
if err != nil {
return err
}
req.SetBasicAuth("test_token_bfu2cyvq3il6o", "") // TODO
req.SetBasicAuth(strings.TrimSpace(Token), "")
req = req.WithContext(ctx)
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()
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
}

View File

@@ -10,6 +10,8 @@ import (
)
func Test(t *testing.T) {
teller.Token = "test_token_bfu2cyvq3il6o"
c, err := teller.New()
if err != nil {
t.Fatal(err)

View File

@@ -8,9 +8,13 @@ import (
"net/http"
"testing"
"time"
"gogs.inhome.blapointe.com/ana-ledger/src/bank/teller"
)
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
cert, err := tls.LoadX509KeyPair("./certificate.pem", "./private_key.pem")
if err != nil {

View File

@@ -0,0 +1 @@
token_2utqstwpn3pxwgvyno56hqdehq