Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
cbrand committed Jan 5, 2024
0 parents commit 487fb5e
Show file tree
Hide file tree
Showing 16 changed files with 1,086 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go

### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

# Go workspace file
go.work

# End of https://www.toptal.com/developers/gitignore/api/go

.env
.envrc
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go

### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

# Go workspace file
go.work

# End of https://www.toptal.com/developers/gitignore/api/go

.env
.envrc

vodafone-billing-downloader

66 changes: 66 additions & 0 deletions fetcher/json_getter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package fetcher

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httputil"
)

type BearerToken interface {
AuthenticateAPI(request *http.Request) *http.Request
}

var (
ErrJsonRequestFailed = errors.New("JSON request failed")
)

func GetJson(url string, bearerToken BearerToken, target interface{}) error {
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
request = setJsonRequestHeaders(request)
request = bearerToken.AuthenticateAPI(request)
return jsonFromRequest(request, target)
}

func setJsonRequestHeaders(request *http.Request) *http.Request {
request.Header.Set("Content-Type", "application/json")
return request
}

func jsonFromRequest(request *http.Request, target interface{}) error {
response, err := http.DefaultClient.Do(request)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK {
requestBytes, err := httputil.DumpRequest(request, true)
if err != nil {
panic(err)
}
fmt.Println(string(requestBytes))
fmt.Println("")

responseBytes, err := httputil.DumpResponse(response, true)
if err != nil {
panic(err)
}
fmt.Println(string(responseBytes))
return ErrJsonRequestFailed
}

return getJsonFromResponse(response, target)
}

func getJsonFromResponse(response *http.Response, target interface{}) error {
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return err
}
return json.Unmarshal(data, target)
}
28 changes: 28 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module github.com/cbrand/vodafone-billing-downloader

go 1.21.0

require (
github.com/fatih/color v1.16.0
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.27.1
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jedib0t/go-pretty/v6 v6.5.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rodaine/table v1.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/progressbar/v3 v3.14.1 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
64 changes: 64 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jedib0t/go-pretty/v6 v6.5.0 h1:FI0L5PktzbafnZKuPae/D3150x3XfYbFe2hxMT+TbpA=
github.com/jedib0t/go-pretty/v6 v6.5.0/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4=
github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI=
github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E=
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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
159 changes: 159 additions & 0 deletions invoice/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package invoice

import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"

"github.com/cbrand/vodafone-billing-downloader/fetcher"
)

const (
INVOICE_URL_TEMPLATE = "https://api.vodafone.de/meinvodafone/v2/customer/urn:vf-de:cable:can:%s/invoice"
INVOICE_DOCUMENT_URL_TEMPLATE = "https://api.vodafone.de/meinvodafone/v2/customer/%s/invoiceDocument/%s"
)

type ContractID string

func invoiceURLFor(contractID ContractID) string {
return fmt.Sprintf(INVOICE_URL_TEMPLATE, contractID)
}

type ContractIDFetcher interface {
GetAllContractIDs() []string
}

type InvoiceOverview struct {
Data map[ContractID]*InvoiceList
}

func (invoiceOverview *InvoiceOverview) GetNumInvoices() int {
numInvoices := 0
for _, invoiceList := range invoiceOverview.Data {
numInvoices += invoiceList.GetNumInvoices()
}
return numInvoices
}

func (invoiceOverview *InvoiceOverview) GetNumDocuments() int {
numDocuments := 0
for _, invoiceList := range invoiceOverview.Data {
numDocuments += invoiceList.GetNumDocuments()
}
return numDocuments
}

func List(contractIDFetcher ContractIDFetcher, bearerToken fetcher.BearerToken) (*InvoiceOverview, error) {
invoices := make(map[ContractID]*InvoiceList)
for _, contractID := range contractIDFetcher.GetAllContractIDs() {
invoiceList, err := ListFor(ContractID(contractID), bearerToken)
if err != nil {
return nil, err
}
invoices[ContractID(contractID)] = invoiceList
}

return &InvoiceOverview{Data: invoices}, nil
}

func ListFor(contractID ContractID, bearerToken fetcher.BearerToken) (*InvoiceList, error) {
invoiceList := &InvoiceList{}
err := fetcher.GetJson(invoiceURLFor(contractID), bearerToken, invoiceList)
if err != nil {
return nil, err
}
invoiceList.PropagateCustomerID()
return invoiceList, nil
}

type InvoiceList struct {
CustomerID string `json:"customerId"`
Invoices []*Invoice `json:"invoices"`
}

func (invoiceList *InvoiceList) GetNumInvoices() int {
return len(invoiceList.Invoices)
}

func (invoiceList *InvoiceList) GetNumDocuments() int {
numDocuments := 0
for _, invoice := range invoiceList.Invoices {
numDocuments += invoice.GetNumDocuments()
}
return numDocuments
}

func (invoiceList *InvoiceList) PropagateCustomerID() {
for _, invoice := range invoiceList.Invoices {
invoice.SetCustomerID(invoiceList.CustomerID)
}
}

type Invoice struct {
Number string `json:"number"`
Date string `json:"date"`
Amount float64 `json:"amount"` // Vodafone API has this defined as a float in json. This is a bad idea.
DueDate string `json:"dueDate"`
From string `json:"from"`
About string `json:"about"`
Documents []*InvoiceDocument `json:"documents"`
CustomerID string `json:"-"`
}

func (invoice *Invoice) GetNumDocuments() int {
return len(invoice.Documents)
}

func (invoice *Invoice) SetCustomerID(customerID string) {
invoice.CustomerID = customerID
for _, document := range invoice.Documents {
document.SetCustomerID(customerID)
}
}

type InvoiceDocument struct {
DocumentID string `json:"documentId"`
Category string `json:"category"`
Icon string `json:"icon"`
SubType string `json:"subType"`
CustomerID string `json:"-"`
}

func (invoiceDocument *InvoiceDocument) SetCustomerID(customerID string) {
invoiceDocument.CustomerID = customerID
}

func (invoiceDocument *InvoiceDocument) DownloadURL() string {
return fmt.Sprintf(INVOICE_DOCUMENT_URL_TEMPLATE, invoiceDocument.CustomerID, invoiceDocument.DocumentID)
}

func (invoiceDocument *InvoiceDocument) Download(bearerToken fetcher.BearerToken) (*DocumentData, error) {
documentData := &DocumentData{}
invoiceDownloadURL := invoiceDocument.DownloadURL()
err := fetcher.GetJson(invoiceDownloadURL, bearerToken, documentData)
if err != nil {
return nil, err
}
return documentData, nil
}

type DocumentData struct {
CustomerID string `json:"customerId"`
DocumentID string `json:"documentId"`
MimeType string `json:"mime"`
Data string `json:"data"`
}

func (documentData *DocumentData) Bytes() ([]byte, error) {
return base64.StdEncoding.DecodeString(documentData.Data)
}

// Checksum returns an md5checksum the same as paperless would calculate for
// cross referencing.
func (documentData *DocumentData) Checksum() string {
data, _ := documentData.Bytes()
hash := md5.New()
hash.Write(data)
return hex.EncodeToString(hash.Sum(nil))
}
Loading

0 comments on commit 487fb5e

Please sign in to comment.