From c1380d89fbbd8f3b3b93bfba0cc23a3bed8e9abb Mon Sep 17 00:00:00 2001 From: jcobhams Date: Wed, 30 Dec 2020 22:33:22 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 3 + LICENSE.md | 7 + README.md | 93 +++++++++++ endpoints.go | 233 ++++++++++++++++++++++++++ go.mod | 9 + go.sum | 18 ++ gomono.go | 152 +++++++++++++++++ gomono_test.go | 443 +++++++++++++++++++++++++++++++++++++++++++++++++ responses.go | 121 ++++++++++++++ 9 files changed, 1079 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 endpoints.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gomono.go create mode 100644 gomono_test.go create mode 100644 responses.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13f9046 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +cover.out +.DS_Store \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..81719dd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2020 Joseph Cobhams + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c720972 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# GoMono +GoMono is a Golang API wrapper around Mono's REST API. + +It implements the `Authentication`, `Account`, `User` and `Misc` endpoints as documented on the [Mono's v1 API docs](https://docs.mono.co/reference) + +## Install +``` +$ go get github.com/jcobhams/gomono +``` + +## Usage + +```go +package main + +import ( + "github.com/jcobhams/gomono" + "net/http" +) + +func main() { + gm, err := gomono.New(gomono.NewDefaultConfig("YOUR_SECRET_KEY")) // If you'd like to use the default config + + //Or you could configure the HTTP Client how you'd like (timeouts, Transports etc + //gm, err := gomono.New(gomono.Config{ + // SecretKey: "YOUR_SECRET_KEY", + // HttpClient: &http.Client{ + // Transport: nil, + // CheckRedirect: nil, + // Jar: nil, + // Timeout: 0, + // }, + // ApiUrl: "https://api.withmono.com", + //}) + + if err != nil { + //do something with error + } + + //Exchange Token + id, err := gm.ExchangeToken("CODE") + + //Get Bank Account Details + infResponse, err := gm.Information(id) + + //Get Bank Statement - Period => in months (1-12) | output => json or pdf + stmtResponse, err := gm.Statement(id, "period", "output") + + // Query PDF Job Status - If the Statement call above had a pdf output, the response will contain a PDF struct with and ID + pdfStmtResponse, err := gm.PdfStatementJobStatus(id, stmtResponse.PDF.ID) + + // Get user transactions - tnxType => debit or credit | paginate => bool + tnxResponse, err := gm.Transactions(id, "start", "end", "narration", "tnxType", "paginate") + + // Get Credit Transactions + crdTnxResponse, err := gm.CreditTransactions(id) + + // Get Debit Transactions + dbtTnxResponse, err := gm.DebitTransactions(id) + + // Get Income Information + incResponse, err := gm.Income(id) + + // Get Identity Information + idyResponse, err := gm.Identity(id) + + // Get Institutions List + insResponse, err := gm.Institutions() + + // LookupBVN + bvnResponse, err := gm.LookupBVN("1234567890") + +} + +``` + +In all the examples, error handling has been ignore/suppressed. Please handle errors properly to avoid `nil pointer` panics. + +## Integration Testing +`Gomono` is an interface that can easily be mocked to ease testing. + +You could also use the explicit configuration option shown earlier to create your clients. + +That way you can set a test API Url or intercept HTTP calls using a fake http client - Whatever works best for you :) + +## What's Not Covered/Supported +1. Data Sync / Reauth Code + +## Run Tests +go test -race -v -coverprofile cover.out + +## View Coverage +go tool cover -html=cover.out \ No newline at end of file diff --git a/endpoints.go b/endpoints.go new file mode 100644 index 0000000..48597bc --- /dev/null +++ b/endpoints.go @@ -0,0 +1,233 @@ +// Copyright 2020 Joseph Cobhams. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE.md file. +// +package gomono + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" +) + +//Auth Endpoints + +//ExchangeToken - https://docs.mono.co/reference#authentication-endpoint +func (g *gomono) ExchangeToken(code string) (string, error) { + if code == "" { + return "", errors.New("gomono: Code cannot be blank") + } + + payload, err := g.preparePayload(map[string]string{"code": code}) + if err != nil { + return "", err + } + + respTarget := make(map[string]string) + + err = g.makeRequest("POST", fmt.Sprintf("%v/account/auth", g.apiUrl), payload, nil, &respTarget) + if err != nil { + return "", err + } + return respTarget["id"], nil +} + +//Account Endpoints + +//Information - https://docs.mono.co/reference#bank-account-details +func (g *gomono) Information(id string) (*InformationResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + var respTarget InformationResponse + err := g.makeRequest("POST", fmt.Sprintf("%v/accounts/%v", g.apiUrl, id), nil, nil, &respTarget) + if err != nil { + return nil, err + } + + return &respTarget, nil +} + +//Statement - https://docs.mono.co/reference#bank-statement +func (g *gomono) Statement(id, period, output string) (*StatementResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + output = strings.ToLower(output) + if output != "" && output != "pdf" && output != "json" { + return nil, errors.New("gomono: only json or pdf output supported") + } + + params := url.Values{} + if output != "" { + params.Add("output", output) + } + + if period != "" { + params.Add("period", period) + } + + endpoint := fmt.Sprintf("%v/accounts/%v/statement?%v", g.apiUrl, id, params.Encode()) + + var result StatementResponse + + switch output { + case "pdf": + var pdfRespTarget StatementResponsePdf + err := g.makeRequest("GET", endpoint, nil, nil, &pdfRespTarget) + if err != nil { + return nil, err + } + result.PDF = &pdfRespTarget + case "json": + var jsonRespTarget StatementResponseJson + err := g.makeRequest("POST", endpoint, nil, nil, &jsonRespTarget) + if err != nil { + return nil, err + } + result.JSON = &jsonRespTarget + } + + return &result, nil +} + +//PdfStatementJobStatus - https://docs.mono.co/reference#poll-statement-status +func (g *gomono) PdfStatementJobStatus(id, jobId string) (*StatementResponsePdf, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + if jobId == "" { + return nil, errors.New("gomono: JOBID is required") + } + + var respTarget StatementResponsePdf + err := g.makeRequest("GET", fmt.Sprintf("%v/accounts/%v/statement/jobs/%v?", g.apiUrl, id, jobId), nil, nil, &respTarget) + if err != nil { + return nil, err + } + + return &respTarget, nil +} + +// User Endpoints + +//Transactions - https://docs.mono.co/reference#poll-statement-status +func (g *gomono) Transactions(id, start, end, narration, tnxType string, paginate bool) (*TransactionsResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + params := url.Values{} + if start != "" { + params.Add("start", start) + } + + if end != "" { + params.Add("end", end) + } + + if narration != "" { + params.Add("narration", narration) + } + + if tnxType != "" { + params.Add("type", tnxType) + } + + params.Add("paginate", strconv.FormatBool(paginate)) + + var respTarget TransactionsResponse + err := g.makeRequest("GET", fmt.Sprintf("%v/accounts/%v/transactions?%v", g.apiUrl, id, params.Encode()), nil, nil, &respTarget) + if err != nil { + return nil, err + } + return &respTarget, nil +} + +//CreditTransactions - https://docs.mono.co/reference#credits +func (g *gomono) CreditTransactions(id string) (*TransactionByTypeResponse, error) { + return g.transactionByType(id, "credit") +} + +//DebitTransactions - https://docs.mono.co/reference#debits +func (g *gomono) DebitTransactions(id string) (*TransactionByTypeResponse, error) { + return g.transactionByType(id, "debit") +} + +func (g *gomono) transactionByType(id, tnxType string) (*TransactionByTypeResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + var respTarget TransactionByTypeResponse + err := g.makeRequest("GET", fmt.Sprintf("%v/accounts/%v/%v", g.apiUrl, id, tnxType), nil, nil, &respTarget) + if err != nil { + return nil, err + } + return &respTarget, nil +} + +//Income - https://docs.mono.co/reference#income +func (g *gomono) Income(id string) (*IncomeResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + var respTarget IncomeResponse + err := g.makeRequest("GET", fmt.Sprintf("%v/accounts/%v/income", g.apiUrl, id), nil, nil, &respTarget) + if err != nil { + return nil, err + } + return &respTarget, nil +} + +//Identity - https://docs.mono.co/reference#identity +func (g *gomono) Identity(id string) (*IdentityResponse, error) { + if id == "" { + return nil, errors.New("gomono: ID is required") + } + + var respTarget IdentityResponse + err := g.makeRequest("GET", fmt.Sprintf("%v/accounts/%v/identity", g.apiUrl, id), nil, nil, &respTarget) + if err != nil { + return nil, err + } + return &respTarget, nil +} + +//MISC Endpoints + +//Institutions - https://docs.mono.co/reference#list-institutions +func (g *gomono) Institutions() (*InstitutionsResponse, error) { + var respTarget []Institution + err := g.makeRequest("GET", fmt.Sprintf("%v/coverage", g.apiUrl), nil, nil, &respTarget) + if err != nil { + return nil, err + } + return &InstitutionsResponse{ + Institutions: respTarget, + }, nil +} + +func (g *gomono) LookupBVN(bvn string) (*IdentityResponse, error) { + if bvn == "" { + return nil, errors.New("gomono: BVN is required") + } + + payload, err := g.preparePayload(map[string]string{"bvn": bvn}) + if err != nil { + return nil, err + } + + var respTarget IdentityResponse + err = g.makeRequest("POST", fmt.Sprintf("%v/v1/lookup/bvn/identity", g.apiUrl), payload, nil, &respTarget) + if err != nil { + return nil, err + } + return &respTarget, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..008ac2b --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/jcobhams/gomono + +go 1.14 + +require ( + github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/testify v1.6.1 + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a613cac --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gomono.go b/gomono.go new file mode 100644 index 0000000..b664292 --- /dev/null +++ b/gomono.go @@ -0,0 +1,152 @@ +// Copyright 2020 Joseph Cobhams. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE.md file. +// +package gomono + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "reflect" + "time" +) + +type ( + Gomono interface { + ExchangeToken(code string) (string, error) + Information(id string) (*InformationResponse, error) + Statement(id, period, output string) (*StatementResponse, error) + PdfStatementJobStatus(id, jobId string) (*StatementResponsePdf, error) + Transactions(id, start, end, narration, tnxType string, paginate bool) (*TransactionsResponse, error) + CreditTransactions(id string) (*TransactionByTypeResponse, error) + DebitTransactions(id string) (*TransactionByTypeResponse, error) + Income(id string) (*IncomeResponse, error) + Identity(id string) (*IdentityResponse, error) + Institutions() (*InstitutionsResponse, error) + LookupBVN(bvn string) (*IdentityResponse, error) + } + + gomono struct { + secretKey string + client *http.Client + apiUrl string + } + + Error struct { + Code int + Body string + Endpoint string + } + + Config struct { + SecretKey string + HttpClient *http.Client + ApiUrl string + } + + header struct { + Key string + Value string + } +) + +func New(cfg Config) (Gomono, error) { + if err := validateConfig(&cfg); err != nil { + return nil, err + } + + g := &gomono{ + secretKey: cfg.SecretKey, + client: cfg.HttpClient, + apiUrl: cfg.ApiUrl, + } + + return g, nil +} + +func NewDefaultConfig(secretKey string) Config { + return Config{ + SecretKey: secretKey, + HttpClient: &http.Client{ + Timeout: 5 * time.Second, + }, + ApiUrl: "https://api.withmono.com", + } +} + +func validateConfig(cfg *Config) error { + if cfg.SecretKey == "" { + return errors.New("gomono: Missing Secret Key") + } + + if cfg.HttpClient == nil { + return errors.New("gomono: HTTP Client Cannot Be Nil") + } + + if cfg.ApiUrl == "" { + return errors.New("gomono: Missing API Url") + } + + return nil +} + +func (g *gomono) preparePayload(body interface{}) (io.Reader, error) { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + return bytes.NewReader(b), nil +} + +func (g *gomono) makeRequest(method, url string, body io.Reader, headers []header, responseTarget interface{}) error { + if reflect.TypeOf(responseTarget).Kind() != reflect.Ptr { + return errors.New("gomono: responseTarget must be a pointer to a struct for JSON unmarshalling") + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return err + } + + for _, h := range headers { + req.Header.Set(h.Key, h.Value) + } + req.Header.Set("mono-sec-key", g.secretKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Client-Lib", "GoMono | v1 | github.com/jcobhams/gomono") + + resp, err := g.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode == 200 { + err = json.Unmarshal(b, responseTarget) + if err != nil { + return err + } + return nil + } + + err = Error{ + Code: resp.StatusCode, + Body: string(b), + Endpoint: req.URL.String(), + } + return err +} + +func (e Error) Error() string { + return fmt.Sprintf("Request To %v Endpoint Failed With Status Code %v | Body: %v", e.Endpoint, e.Code, e.Body) +} diff --git a/gomono_test.go b/gomono_test.go new file mode 100644 index 0000000..5210687 --- /dev/null +++ b/gomono_test.go @@ -0,0 +1,443 @@ +// Copyright 2020 Joseph Cobhams. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE.md file. +// +package gomono + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +var ( + testAccountId = "5fc68b964bdcbe4eb164e852" + testJobId = "MvRh2vWwv5CGafudTivY" + testSecretKey = "TEST_SECRET_KEY" + testMonoConnectCode = "TEST_MONO_CONNECT_CODE" + mockServer *httptest.Server + client Gomono +) + +func TestMain(m *testing.M) { + + mockServer = testServer() + client, _ = New(Config{ + SecretKey: testSecretKey, + HttpClient: &http.Client{Timeout: 1 * time.Second}, + ApiUrl: mockServer.URL, + }) + mockServer.Close() + + os.Exit(m.Run()) +} + +func TestNew(t *testing.T) { + g, err := New(NewDefaultConfig("")) + assert.Nil(t, g) + assert.NotNil(t, err) + + g, err = New(NewDefaultConfig(testSecretKey)) + assert.NotNil(t, g) + assert.Nil(t, err) +} + +func TestGomono_ExchangeToken(t *testing.T) { + id, err := client.ExchangeToken("") + assert.Empty(t, id) + assert.NotNil(t, err) + + id, err = client.ExchangeToken(testMonoConnectCode) + assert.NotEmpty(t, id) + assert.Equal(t, testAccountId, id) + assert.Nil(t, err) +} + +func TestGomono_Information(t *testing.T) { + r, err := client.Information("") + assert.Nil(t, r) + assert.NotNil(t, err) + + r, err = client.Information(testAccountId) + assert.NotNil(t, r) + assert.Equal(t, testAccountId, r.Account.ID) + assert.Nil(t, err) +} + +func TestGomono_Statement(t *testing.T) { + r, err := client.Statement(testAccountId, "", "x") + assert.Nil(t, r) + assert.NotNil(t, err) + + r, err = client.Statement(testAccountId, "", "json") + assert.NotNil(t, r) + assert.Equal(t, 2, r.JSON.Meta.Count) + assert.Equal(t, 2, len(r.JSON.Data)) + assert.Nil(t, r.PDF) + assert.Nil(t, err) + + r, err = client.Statement(testAccountId, "", "pdf") + assert.NotNil(t, r) + assert.Equal(t, "BUILDING", r.PDF.Status) + assert.NotEmpty(t, r.PDF.ID) + assert.NotEmpty(t, r.PDF.Path) + assert.Nil(t, r.JSON) + assert.Nil(t, err) +} + +func TestGomono_PdfStatementJobStatus(t *testing.T) { + r, err := client.PdfStatementJobStatus(testAccountId, testJobId) + assert.NotNil(t, r) + assert.Equal(t, testJobId, r.ID) + assert.Equal(t, "COMPLETE", r.Status) + assert.Nil(t, err) +} + +func TestGomono_Transactions(t *testing.T) { + r, err := client.Transactions(testAccountId, "01-10-2020", "07-10-2020", "test", "debit", true) + assert.NotNil(t, r) + assert.Equal(t, 190, r.Paging.Total) + assert.Equal(t, 2, r.Paging.Page) + assert.Equal(t, 2, len(r.Data)) + assert.Nil(t, err) +} + +func TestGomono_TransactionByType(t *testing.T) { + r, err := client.CreditTransactions(testAccountId) + assert.NotNil(t, r) + assert.Equal(t, float64(2000000), r.Total) + assert.Equal(t, 2, len(r.History)) + assert.Nil(t, err) + + r, err = client.DebitTransactions(testAccountId) + assert.NotNil(t, r) + assert.Equal(t, float64(1000000), r.Total) + assert.Equal(t, 2, len(r.History)) + assert.Nil(t, err) +} + +func TestGomono_Income(t *testing.T) { + r, err := client.Income(testAccountId) + assert.NotNil(t, r) + assert.Equal(t, "INCOME", r.Type) + assert.Equal(t, float64(59700000), r.Amount) + assert.Nil(t, err) +} + +func TestGomono_Identity(t *testing.T) { + r, err := client.Identity(testAccountId) + assert.NotNil(t, r) + assert.Equal(t, "ABDULHAMID", r.FirstName) + assert.Equal(t, "HASSAN", r.LastName) + assert.Equal(t, "NO", r.WatchListed) + assert.Equal(t, "06-May-1996", r.DateOfBirth) + assert.Nil(t, err) +} + +func TestGomono_Institutions(t *testing.T) { + r, err := client.Institutions() + assert.NotNil(t, r) + assert.Equal(t, "GTBank", r.Institutions[0].Name) + assert.Equal(t, 4, len(r.Institutions)) + assert.Nil(t, err) +} + +func TestGomono_LookupBVN(t *testing.T) { + r, err := client.LookupBVN("1234567897418") + assert.NotNil(t, r) + assert.Equal(t, "ABDULHAMID", r.FirstName) + assert.Equal(t, "HASSAN", r.LastName) + assert.Equal(t, "NO", r.WatchListed) + assert.Equal(t, "06-May-1996", r.DateOfBirth) + assert.Nil(t, err) +} + +//StartServer initializes a test HTTP server useful for request mocking, Integration tests and Client configuration +func testServer() *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-type", "application/json") + + switch r.URL.Path { + case "/account/auth": + successBody := fmt.Sprintf(`{"id": "%v"}`, testAccountId) + w.WriteHeader(200) + fmt.Fprintf(w, successBody) + + case fmt.Sprintf("/accounts/%v", testAccountId): + body := fmt.Sprintf(`{ + "meta": {"data_status": "AVAILABLE"}, + "account": { + "_id": "%v", + "institution": { + "name": "Access Bank", + "bankCode": "044", + "type": "PERSONAL_BANKING" + }, + "name": "IDORENYIN OBONG OBONG", + "currency": "NGN", + "type": "Current", + "accountNumber": "0788164862", + "balance": 37836709, + "bvn": "6800" + } +}`, testAccountId) + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/statement", testAccountId): + body := "" + switch r.URL.Query().Get("output") { + case "json": + body = `{"meta": {"count": 2}, + "data": [ + { + "_id": "5fc686f98b97632dbef0f8db", + "type": "debit", + "date": "2020-12-01T00:00:00.000Z", + "narration": "VALUE ADDED TAX VAT ON NIP TRANSFER FOR Yusuf Money TO FCMB/OGUNGBEFUN OLADUNNI KHADIJAH ReF:", + "amount": 375, + "balance": 10517116 + }, + { + "_id": "5fc686f98b97632dbef0f8dc", + "type": "debit", + "date": "2020-12-01T00:00:00.000Z", + "narration": "COMMISSION NIP TRANSFER COMMISSION FOR Yusuf Money TO FCMB/OGUNGBEFUN OLADUNNI KHADIJAH ReF:", + "amount": 5000, + "balance": 10517491 + } +] +}` + case "pdf": + body = fmt.Sprintf(`{"id": "%v", "status": "BUILDING", "path": "https://api.withmono.com/statements/pvLhFR89Id2zrnPGJZcM.pdf"}`, testJobId) + } + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/statement/jobs/%v", testAccountId, testJobId): + body := fmt.Sprintf(`{"id": "%v", "status": "COMPLETE", "path": "https://api.withmono.com/statements/pvLhFR89Id2zrnPGJZcM.pdf"}`, testJobId) + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/transactions", testAccountId): + body := `{ + "paging": { + "total": 190, + "page": 2, + "previous": "https://api.withmono.com/accounts/:id/transactions?page=2", + "next": "https://api.withmono.com/accounts/:id/transactions?page=3" + }, + "data": [ + { + "_id": "5f171a540295e231abca1154", + "amount": 10000, + "date": "2020-07-21T00:00:00.000Z", + "narration": "TRANSFER from HASSAN ABDULHAMID TOMIWA to OGUNGBEFUN OLADUNNI KHADIJAH", + "type": "debit", + "category": "E-CHANNELS" + }, + { + "_id": "5d171a540295e231abca6654", + "amount": 20000, + "date": "2020-07-21T00:00:00.000Z", + "narration": "TRANSFER from HASSAN ABDULHAMID TOMIWA to UMAR ABDULLAHI", + "type": "debit", + "category": "E-CHANNELS" + } + ] +}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/credit", testAccountId): + body := `{"total": 2000000, + "history": [ + { + "amount": 1000000, + "period": "01-20" + }, + { + "amount": 1000000, + "period": "07-20" + } + ] +}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/debit", testAccountId): + body := `{"total": 1000000, + "history": [ + { + "amount": 1000000, + "period": "01-20" + }, + { + "amount": 1000000, + "period": "07-20" + } + ] +}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/income", testAccountId): + body := `{"type": "INCOME", "amount": 59700000, "employer": "Relentless Labs Inc", "confidence": 0.95}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case fmt.Sprintf("/accounts/%v/identity", testAccountId): + body := `{ + "firstName": "ABDULHAMID", + "middleName": "TOMIWA", + "lastName": "HASSAN", + "dateOfBirth": "06-May-1996", + "phoneNumber1": "0000000", + "phoneNumber2": "", + "registrationDate": "26-Mar-2018", + "email": "tomiwa.jr@gmail.com", + "gender": "Male", + "levelOfAccount": "Level 1 - Low Level Accounts", + "lgaOfOrigin": "Abeokuta South", + "lgaOfResidence": "Ikeja", + "maritalStatus": "Single", + "nin": "000000", + "nationality": "Nigeria", + "residentialAddress": "23, SHITTU ANIMASHA STR, PHASE2, GBAGADA", + "stateOfOrigin": "Ogun State", + "stateOfResidence": "Lagos State", + "title": "Mr", + "watchListed": "NO", + "bvn": "00000000", + "photoId": "/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAGQASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDA1JO45APTiooyA4Bzk+lMRpWiELk1Bqsm2IjP4Vbtj8mcfnWPrE3O3PNIOpkR8z1sRBhFjNY8ESSyfOM4ORWwmNg5pIpkZYMWUhht9RwfpVWUBckVoMMrVKVccdfrTYBbykNtBAPvVq6TfF0BxVKA4frzWkDuXHYigRglSjn61Mk20jd0ovEKOSaroQ/Q0ii8xjnUZXPoR1FVJLeQfcbd7Hqb0ak86MyGMMC4AJXuB/kUhjwOKORxTDncDn5R1FPznmgBe1RlVkKvlhtPr1p2TnHWjoDxTAjIwx+tSIMLz1qMkk07NAH//2Q==" +}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + case "/coverage": + successBody := `[ + { + "name": "GTBank", + "icon": "https://connect.withmono.com/build/img/guaranty-trust-bank.png", + "website": "https://www.gtbank.com", + "coverage": { + "personal": true, + "business": true, + "countries": [ + "NG" + ] + }, + "products": [ + "Auth", + "Accounts", + "Transactions", + "Balance", + "Income", + "Identity", + "Direct Debit" + ] + }, + { + "name": "Access Bank", + "icon": "https://connect.withmono.com/build/img/access-bank.png", + "website": "https://www.accessbankplc.com", + "coverage": { + "personal": true, + "business": true, + "countries": [ + "NG" + ] + }, + "products": [ + "Auth", + "Accounts", + "Transactions", + "Balance", + "Income", + "Identity", + "Direct Debit" + ] + }, + { + "name": "First Bank", + "icon": "https://connect.withmono.com/build/img/first-bank-of-nigeria.png", + "website": "https://www.firstbanknigeria.com", + "coverage": { + "personal": true, + "business": true, + "countries": [ + "NG" + ] + }, + "products": [ + "Auth", + "Accounts", + "Transactions", + "Balance", + "Income", + "Identity", + "Direct Debit" + ] + }, + { + "name": "Fidelity Bank", + "icon": "https://connect.withmono.com/build/img/fidelity-bank.png", + "website": "https://www.fidelitybank.ng", + "coverage": { + "personal": true, + "business": true, + "countries": [ + "NG" + ] + }, + "products": [ + "Auth", + "Accounts", + "Transactions", + "Balance", + "Income", + "Identity", + "Direct Debit" + ] + }]` + w.WriteHeader(200) + fmt.Fprintf(w, successBody) + + case "/v1/lookup/bvn/identity": + body := `{ + "firstName": "ABDULHAMID", + "middleName": "TOMIWA", + "lastName": "HASSAN", + "dateOfBirth": "06-May-1996", + "phoneNumber1": "0000000", + "phoneNumber2": "", + "registrationDate": "26-Mar-2018", + "email": "tomiwa.jr@gmail.com", + "gender": "Male", + "levelOfAccount": "Level 1 - Low Level Accounts", + "lgaOfOrigin": "Abeokuta South", + "lgaOfResidence": "Ikeja", + "maritalStatus": "Single", + "nin": "000000", + "nationality": "Nigeria", + "residentialAddress": "23, SHITTU ANIMASHA STR, PHASE2, GBAGADA", + "stateOfOrigin": "Ogun State", + "stateOfResidence": "Lagos State", + "title": "Mr", + "watchListed": "NO", + "bvn": "00000000", + "photoId": "/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAGQASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDA1JO45APTiooyA4Bzk+lMRpWiELk1Bqsm2IjP4Vbtj8mcfnWPrE3O3PNIOpkR8z1sRBhFjNY8ESSyfOM4ORWwmNg5pIpkZYMWUhht9RwfpVWUBckVoMMrVKVccdfrTYBbykNtBAPvVq6TfF0BxVKA4frzWkDuXHYigRglSjn61Mk20jd0ovEKOSaroQ/Q0ii8xjnUZXPoR1FVJLeQfcbd7Hqb0ak86MyGMMC4AJXuB/kUhjwOKORxTDncDn5R1FPznmgBe1RlVkKvlhtPr1p2TnHWjoDxTAjIwx+tSIMLz1qMkk07NAH//2Q==" +}` + w.WriteHeader(200) + fmt.Fprintf(w, body) + + default: + w.WriteHeader(500) + } + })) + return server +} diff --git a/responses.go b/responses.go new file mode 100644 index 0000000..e91d6c1 --- /dev/null +++ b/responses.go @@ -0,0 +1,121 @@ +// Copyright 2020 Joseph Cobhams. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE.md file. +// +package gomono + +type ( + InformationResponse struct { + Meta struct{ DataStatus string } `json:"meta"` + Account struct { + ID string `json:"_id"` + Name string `json:"name"` + Currency string `json:"currency"` + Type string `json:"type"` + AccountNumber string `json:"accountNumber"` + Balance float64 `json:"balance"` + BVN string `json:"bvn"` + Institution struct { + Name string `json:"name"` + BankCode string `json:"bankCode"` + Type string `json:"type"` + } `json:"institution"` + } + } + + StatementResponse struct { + JSON *StatementResponseJson + PDF *StatementResponsePdf + } + + StatementResponseJson struct { + Meta struct{ Count int } `json:"meta"` + Data []struct { + ID string `json:"_id"` + Type string `json:"type"` + Date string `json:"date"` + Narration string `json:"narration"` + Amount float64 `json:"amount"` + Balance float64 `json:"balance"` + } + } + + StatementResponsePdf struct { + ID string `json:"id"` + Status string `json:"status"` + Path string `json:"path"` + } + + TransactionsResponse struct { + Paging struct { + Total int `json:"total"` + Page int `json:"page"` + Previous string `json:"previous"` + Next string `json:"next"` + } + Data []struct { + ID string `json:"_id"` + Amount float64 `json:"amount"` + Date string `json:"date"` + Narration string `json:"narration"` + Type string `json:"type"` + Category string `json:"category"` + } + } + + TransactionByTypeResponse struct { + Total float64 `json:"total"` + History []struct { + Amount float64 `json:"amount"` + Period string `json:"period"` + } + } + + IncomeResponse struct { + Type string `json:"type"` + Amount float64 `json:"amount"` + Employer string `json:"employer"` + Confidence float64 `json:"confidence"` + } + + IdentityResponse struct { + FirstName string `json:"firstName"` + MiddleName string `json:"middleName"` + LastName string `json:"lastName"` + DateOfBirth string `json:"dateOfBirth"` + PhoneNumber1 string `json:"phoneNumber1"` + PhoneNumber2 string `json:"phoneNumber2"` + RegistrationDate string `json:"registrationDate"` + Email string `json:"email"` + Gender string `json:"gender"` + LevelOfAccount string `json:"levelOfAccount"` + LgaOfOrigin string `json:"lgaOfOrigin"` + LgaOfResidence string `json:"lgaOfResidence"` + MaritalStatus string `json:"maritalStatus"` + NIN string `json:"nin"` + Nationality string `json:"nationality"` + ResidentialAddress string `json:"residentialAddress"` + StateOfOrigin string `json:"stateOfOrigin"` + StateOfResidence string `json:"stateOfResidence"` + Title string `json:"title"` + WatchListed string `json:"watchListed"` + BVN string `json:"bvn"` + PhotoID string `json:"photo_id"` + } + + InstitutionsResponse struct { + Institutions []Institution + } + + Institution struct { + Name string `json:"name"` + Icon string `json:"icon"` + Website string `json:"website"` + Coverage struct { + Personal bool `json:"personal"` + Business bool `json:"business"` + Countries []string `json:"countries"` + } `json:"coverage"` + Products []string `json:"products"` + } +)