From c81b58f60c3640543fba311daa3118d6de34e151 Mon Sep 17 00:00:00 2001 From: Sidharthan Chandrasekaran Kamaraj Date: Sun, 7 May 2023 10:27:22 +0200 Subject: [PATCH] feature: add premium endpoint implementations (#8) * feature: update balance and transaction structs * code refactor * Update pull_request_template.md * refactor code * feat: add premium account transactions endpoint * Update README.md * format and lint issues fix * Update Taskfile.yml --- .github/pull_request_template.md | 3 - README.md | 6 + Taskfile.yml | 24 +- accounts.go | 104 +++++++ accounts_endpoints.go | 52 ++++ ...ints_test.go => accounts_endpoints_test.go | 77 ++--- .../agreements/agreements.go => agreements.go | 4 +- agreements_endpoints.go | 72 +++++ agreements_endpoints_test.go | 243 ++++++++++++++++ consts/api_info.go => api_info.go | 26 +- assets/cover-treemap.svg | 97 +++---- client.go | 63 +--- client_test.go | 41 +++ endpoints/accounts/accounts.go | 74 ----- endpoints/accounts/accounts_endpoints.go | 52 ---- endpoints/accounts/client.go | 13 - endpoints/agreements/agreements_endpoints.go | 74 ----- .../agreements/agreements_endpoints_test.go | 265 ----------------- endpoints/agreements/client.go | 13 - endpoints/institutions/client.go | 13 - .../institutions/institutions_endpoints.go | 39 --- .../institutions_endpoints_test.go | 47 --- endpoints/payments/client.go | 13 - endpoints/payments/payments.go | 44 --- endpoints/payments/payments_endpoints.go | 24 -- endpoints/premium/.gitkeep | 0 endpoints/requisitions/client.go | 13 - .../requisitions/requisitions_endpoints.go | 61 ---- .../requisitions_endpoints_test.go | 273 ------------------ endpoints/token/client.go | 13 - endpoints/token/token_endpoints.go | 38 --- consts/error.go => error.go | 2 +- go.mod | 5 +- go.sum | 25 -- consts/helpers.go => helpers.go | 16 +- http/http-client.go => http-client.go | 57 ++-- http/mocks/mock_client.go | 90 ------ .../institutions.go => institutions.go | 2 +- institutions_endpoints.go | 37 +++ institutions_endpoints_test.go | 76 +++++ payments.go | 37 +++ payments_endpoints.go | 23 ++ ...ints_test.go => payments_endpoints_test.go | 17 +- premium.go | 73 +++++ premium_endpoints.go | 15 + .../requisitions.go => requisitions.go | 4 +- requisitions_endpoints.go | 60 ++++ requisitions_endpoints_test.go | 255 ++++++++++++++++ endpoints/token/token.go => token.go | 2 +- token_endpoints.go | 36 +++ ...dpoints_test.go => token_endpoints_test.go | 37 +-- 51 files changed, 1294 insertions(+), 1456 deletions(-) create mode 100644 accounts.go create mode 100644 accounts_endpoints.go rename endpoints/accounts/accounts_endpoints_test.go => accounts_endpoints_test.go (53%) rename endpoints/agreements/agreements.go => agreements.go (95%) create mode 100644 agreements_endpoints.go create mode 100644 agreements_endpoints_test.go rename consts/api_info.go => api_info.go (58%) create mode 100644 client_test.go delete mode 100644 endpoints/accounts/accounts.go delete mode 100644 endpoints/accounts/accounts_endpoints.go delete mode 100644 endpoints/accounts/client.go delete mode 100644 endpoints/agreements/agreements_endpoints.go delete mode 100644 endpoints/agreements/agreements_endpoints_test.go delete mode 100644 endpoints/agreements/client.go delete mode 100644 endpoints/institutions/client.go delete mode 100644 endpoints/institutions/institutions_endpoints.go delete mode 100644 endpoints/institutions/institutions_endpoints_test.go delete mode 100644 endpoints/payments/client.go delete mode 100644 endpoints/payments/payments.go delete mode 100644 endpoints/payments/payments_endpoints.go delete mode 100644 endpoints/premium/.gitkeep delete mode 100644 endpoints/requisitions/client.go delete mode 100644 endpoints/requisitions/requisitions_endpoints.go delete mode 100644 endpoints/requisitions/requisitions_endpoints_test.go delete mode 100644 endpoints/token/client.go delete mode 100644 endpoints/token/token_endpoints.go rename consts/error.go => error.go (97%) rename consts/helpers.go => helpers.go (67%) rename http/http-client.go => http-client.go (58%) delete mode 100644 http/mocks/mock_client.go rename endpoints/institutions/institutions.go => institutions.go (96%) create mode 100644 institutions_endpoints.go create mode 100644 institutions_endpoints_test.go create mode 100644 payments.go create mode 100644 payments_endpoints.go rename endpoints/payments/payments_endpoints_test.go => payments_endpoints_test.go (50%) create mode 100644 premium.go create mode 100644 premium_endpoints.go rename endpoints/requisitions/requisitions.go => requisitions.go (96%) create mode 100644 requisitions_endpoints.go create mode 100644 requisitions_endpoints_test.go rename endpoints/token/token.go => token.go (92%) create mode 100644 token_endpoints.go rename endpoints/token/token_endpoints_test.go => token_endpoints_test.go (57%) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 40322ab..6bf8be9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,9 +8,6 @@ A brief summary of the changes made in the PR ### Check list - [ ] `task pr-check` command executed successfully after the changes -- [ ] Tested helm deploy is working fine in local machine with the changes -- [ ] Tested docker compose is working fine in local machine with the changes - [ ] Required changes are made in `Taskfile.yml` (if required) - [ ] Optimal test coverage is added ( > 70%) -- [ ] Check if the Traces are present in Jaeger - [ ] Ran manual e2e testing (if required) \ No newline at end of file diff --git a/README.md b/README.md index ea25d8f..fd37dbe 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,10 @@ Usage examples can be found in the respective package's `*_test.go` files. Please refer to [Nordigen API Documentation](https://nordigen.com/en/docs/account-information/integration/parameters-and-responses/) for more information on the endpoints. +# Pending Endpoints +- Payments - Since this needs some access to the API which is paid, I will not be implementing this since I can't test. If you would like to contribute, please feel free to open a PR. + +# Issues +Please report any issues or bugs to the [Issues](https://github.com/weportfolio/go-nordigen/issues) page. + ![pkg-coverage-img](./assets/cover-treemap.svg?raw=true "Unit Test Coverage Image") \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index 3e60d69..a804c7d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,5 +1,17 @@ version: 3 +env: + TAG_NAME: + sh: | + # check if the current branch is main + if [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ]; then + # if the current branch is main, then use the version as is + cat .version + else + # if the current branch is not main, then append the branch hash to the version + cat .version | git rev-parse --short HEAD + fi + tasks: dev-setup: desc: "Setup development environment" @@ -42,5 +54,13 @@ tasks: tag: desc: "Tagging the release" cmds: - - git tag -a $(cat .version) -m "Release v$(cat .version)" - - git push origin $(cat .version) \ No newline at end of file + - git tag -a {{.TAG_NAME}} -m "Release {{.TAG_NAME}}" + - git push origin {{.TAG_NAME}} + + pr-check: + desc: "PR Check" + cmds: + - task deps + - task fmt + - task lint + - task test \ No newline at end of file diff --git a/accounts.go b/accounts.go new file mode 100644 index 0000000..a5e9cc5 --- /dev/null +++ b/accounts.go @@ -0,0 +1,104 @@ +package nordigen + +import "time" + +type Account struct { + ID string `json:"id"` + Created string `json:"created"` + LastAccessed string `json:"last_accessed"` + IBAN string `json:"iban"` + InstitutionID string `json:"institution_id"` + Status string `json:"status"` + OwnerName string `json:"owner_name"` +} + +type Balance struct { + BalanceAmount Amount `json:"balanceAmount"` + BalanceType string `json:"balanceType"` + ReferenceDate string `json:"referenceDate"` + CreditLimitIncluded bool `json:"creditLimitIncluded"` + LastChangeDateTime time.Time `json:"lastChangeDateTime"` + LastCommittedTransaction string `json:"lastCommittedTransaction"` +} + +type Amount struct { + Amount string `json:"amount"` + Currency string `json:"currency"` +} + +type Balances struct { + Balances []Balance `json:"balances"` +} + +// AccountDetails is a struct that contains the details of an account +// Some fields might be empty, depending on the account type +type AccountDetails struct { + BBAN string `json:"bban"` + BIC string `json:"bic"` + Details string `json:"details"` + DisplayName string `json:"displayName"` + LinkedAccounts string `json:"linkedAccounts"` + MisISDN string `json:"misIsdn"` + OwnerAddressUnstructured string `json:"ownerAddressUnstructured"` + Status string `json:"status"` + Usage string `json:"usage"` + ResourceID string `json:"resourceId"` + IBAN string `json:"iban"` + Currency string `json:"currency"` + OwnerName string `json:"ownerName"` + Name string `json:"name"` + Product string `json:"product"` + CashAccountType string `json:"cashAccountType"` +} + +type Details struct { + Account AccountDetails `json:"account"` +} + +type TransactionParams struct { + DateFrom string `url:"date_from,omitempty" json:"date_from,omitempty"` + DateTo string `url:"date_to,omitempty" json:"date_to,omitempty"` +} + +type Transaction struct { + TransactionID string `json:"transactionId"` + BookingDate string `json:"bookingDate"` + ValueDate string `json:"valueDate"` + BookingDateTime time.Time `json:"bookingDateTime"` + ValueDateTime time.Time `json:"valueDateTime"` + TransactionAmount Amount `json:"transactionAmount"` + CreditorName string `json:"creditorName"` + CreditorAccount Account `json:"creditorAccount"` + DebtorName string `json:"debtorName"` + DebtorAccount Account `json:"debtorAccount"` + BankTransactionCode string `json:"bankTransactionCode"` + RemittanceInformationUnstructured string `json:"remittanceInformationUnstructured"` + RemittanceInformationUnstructuredArray []string `json:"remittanceInformationUnstructuredArray"` + ProprietaryBankTransactionCode string `json:"proprietaryBankTransactionCode"` + InternalTransactionID string `json:"internalTransactionId"` + + AdditionalInformation string `json:"additionalInformation"` + AdditionalInformationStructured string `json:"additionalInformationStructured"` + BalanceAfterTransaction Balance `json:"balanceAfterTransaction"` + CheckID string `json:"checkId"` + CreditorID string `json:"creditorId"` + // CurrencyExchange []string `json:"currencyExchange"` + DebtorAgent string `json:"debtorAgent"` + EndToEndID string `json:"endToEndId"` + EntryReference string `json:"entryReference"` + MandateID string `json:"mandateId"` + MerchantCategoryCode string `json:"merchantCategoryCode"` + RemittanceInformationStructured string `json:"remittanceInformationStructured"` + RemittanceInformationStructuredArray []string `json:"remittanceInformationStructuredArray"` + UltimateCollector string `json:"ultimateCreditor"` + UltimateDebtor string `json:"ultimateDebtor"` +} + +type Transactions struct { + Transactions TransactionList `json:"transactions"` +} + +type TransactionList struct { + Booked []Transaction `json:"booked"` + Pending []Transaction `json:"pending"` +} diff --git a/accounts_endpoints.go b/accounts_endpoints.go new file mode 100644 index 0000000..288c9ec --- /dev/null +++ b/accounts_endpoints.go @@ -0,0 +1,52 @@ +package nordigen + +import ( + "context" +) + +// GetAccount retrieves an account by ID +func (c Client) GetAccount(ctx context.Context, token string, accountID string) (*Account, error) { + var account Account + endpointURL := AccountsPath + accountID + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &account) + if err != nil { + return nil, err + } + return &account, nil +} + +// GetAccountBalances retrieves balances for an account by ID +func (c Client) GetAccountBalances(ctx context.Context, token string, accountID string) (*Balances, error) { + var balances Balances + endpointURL := AccountsPath + accountID + "/balances" + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &balances) + if err != nil { + return nil, err + } + + return &balances, nil +} + +// GetAccountDetails retrieves details for an account by ID +func (c Client) GetAccountDetails(ctx context.Context, token string, accountID string) (*Details, error) { + var details Details + endpointURL := AccountsPath + accountID + "/details" + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &details) + if err != nil { + return nil, err + } + + return &details, nil +} + +// GetAccountTransactions retrieves transactions for an account by ID +func (c Client) GetAccountTransactions(ctx context.Context, token string, accountID string) (*Transactions, error) { + var transactions Transactions + endpointURL := AccountsPath + accountID + "/transactions" + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &transactions) + if err != nil { + return nil, err + } + + return &transactions, nil +} diff --git a/endpoints/accounts/accounts_endpoints_test.go b/accounts_endpoints_test.go similarity index 53% rename from endpoints/accounts/accounts_endpoints_test.go rename to accounts_endpoints_test.go index 1068ea9..1875ca8 100644 --- a/endpoints/accounts/accounts_endpoints_test.go +++ b/accounts_endpoints_test.go @@ -1,12 +1,11 @@ -package accounts_test +package nordigen_test import ( "context" - "github.com/stretchr/testify/assert" - "github.com/weportfolio/go-nordigen" - "github.com/weportfolio/go-nordigen/consts" "os" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_GetAccount(t *testing.T) { @@ -15,18 +14,16 @@ func TestClient_GetAccount(t *testing.T) { t.Run("get an account by ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) testAccountID := os.Getenv("NORDIGEN_TEST_ACCOUNT_ID") - account, err := client.Accounts().GetAccount(context.Background(), token.Access, testAccountID) + account, err := client.GetAccount(context.Background(), token.Access, testAccountID) assert.NoError(t, err) assert.NotNil(t, account) }) @@ -34,39 +31,35 @@ func TestClient_GetAccount(t *testing.T) { t.Run("get an account by invalid ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - account, err := client.Accounts().GetAccount(context.Background(), token.Access, "invalid") + account, err := client.GetAccount(context.Background(), token.Access, "invalid") assert.Error(t, err) assert.Nil(t, account) }) } -func TestClient_GetBalances(t *testing.T) { +func TestClient_GetAccountBalances(t *testing.T) { t.Parallel() t.Run("get balances for an account by ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) testAccountID := os.Getenv("NORDIGEN_TEST_ACCOUNT_ID") - balances, err := client.Accounts().GetBalances(context.Background(), token.Access, testAccountID) + balances, err := client.GetAccountBalances(context.Background(), token.Access, testAccountID) assert.NoError(t, err) assert.NotNil(t, balances) }) @@ -74,39 +67,35 @@ func TestClient_GetBalances(t *testing.T) { t.Run("get balances for an account by invalid ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - balances, err := client.Accounts().GetBalances(context.Background(), token.Access, "invalid") + balances, err := client.GetAccountBalances(context.Background(), token.Access, "invalid") assert.Error(t, err) assert.Nil(t, balances) }) } -func TestClient_GetDetails(t *testing.T) { +func TestClient_GetAccountDetails(t *testing.T) { t.Parallel() t.Run("get details for an account by ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) testAccountID := os.Getenv("NORDIGEN_TEST_ACCOUNT_ID") - details, err := client.Accounts().GetDetails(context.Background(), token.Access, testAccountID) + details, err := client.GetAccountDetails(context.Background(), token.Access, testAccountID) assert.NoError(t, err) assert.NotNil(t, details) }) @@ -114,39 +103,35 @@ func TestClient_GetDetails(t *testing.T) { t.Run("get details for an account by invalid ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - details, err := client.Accounts().GetDetails(context.Background(), token.Access, "invalid") + details, err := client.GetAccountDetails(context.Background(), token.Access, "invalid") assert.Error(t, err) assert.Nil(t, details) }) } -func TestClient_GetTransactions(t *testing.T) { +func TestClient_GetAccountTransactions(t *testing.T) { t.Parallel() t.Run("get transactions for an account by ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) testAccountID := os.Getenv("NORDIGEN_TEST_ACCOUNT_ID") - transactions, err := client.Accounts().GetTransactions(context.Background(), token.Access, testAccountID) + transactions, err := client.GetAccountTransactions(context.Background(), token.Access, testAccountID) assert.NoError(t, err) assert.NotNil(t, transactions) }) @@ -154,16 +139,14 @@ func TestClient_GetTransactions(t *testing.T) { t.Run("get transactions for an account by invalid ID", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - transactions, err := client.Accounts().GetTransactions(context.Background(), token.Access, "invalid") + transactions, err := client.GetAccountTransactions(context.Background(), token.Access, "invalid") assert.Error(t, err) assert.Nil(t, transactions) }) diff --git a/endpoints/agreements/agreements.go b/agreements.go similarity index 95% rename from endpoints/agreements/agreements.go rename to agreements.go index 7edd267..ca31956 100644 --- a/endpoints/agreements/agreements.go +++ b/agreements.go @@ -1,4 +1,4 @@ -package agreements +package nordigen import "time" @@ -19,7 +19,7 @@ type Agreement struct { Accepted time.Time `json:"accepted"` } -type ListRequestParams struct { +type ListAgreementsParams struct { Limit int `url:"limit,omitempty" json:"limit,omitempty"` Offset int `url:"offset,omitempty" json:"offset,omitempty"` } diff --git a/agreements_endpoints.go b/agreements_endpoints.go new file mode 100644 index 0000000..8e83094 --- /dev/null +++ b/agreements_endpoints.go @@ -0,0 +1,72 @@ +package nordigen + +import ( + "context" + "strconv" +) + +// CreateAgreement creates a new agreement for the enduser +func (c Client) CreateAgreement(ctx context.Context, token string, agreementRequestBody AgreementRequestBody) (*Agreement, error) { + var agreement Agreement + err := c.HTTP.Post(ctx, AgreementsEndusersPath, RequestHeadersWithAuth(token), agreementRequestBody, &agreement) + if err != nil { + return nil, err + } + + return &agreement, nil +} + +// FetchAgreement retrieves an agreement for the enduser by agreementID +func (c Client) FetchAgreement(ctx context.Context, token string, agreementID string) (*Agreement, error) { + var agreement Agreement + err := c.HTTP.Get(ctx, AgreementsEndusersPath+agreementID, RequestHeadersWithAuth(token), &agreement) + if err != nil { + return nil, err + } + + return &agreement, nil +} + +// ListAgreements returns a list of agreements for the enduser +func (c Client) ListAgreements(ctx context.Context, token string, requestParams *ListAgreementsParams) (*Agreements, error) { + var agreements Agreements + + endpointURL := AgreementsEndusersPath + if requestParams != nil { + if requestParams.Limit != 0 { + endpointURL = endpointURL + "?" + strconv.Itoa(requestParams.Limit) + } + if requestParams.Offset != 0 { + endpointURL = endpointURL + "&" + strconv.Itoa(requestParams.Offset) + } + } + + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &agreements) + if err != nil { + return nil, err + } + + return &agreements, nil +} + +// DeleteAgreement deletes an agreement for the enduser by agreementID +func (c Client) DeleteAgreement(ctx context.Context, token string, agreementID string) error { + err := c.HTTP.Delete(ctx, AgreementsEndusersPath+agreementID, RequestHeadersWithAuth(token), nil) + if err != nil { + return err + } + + return nil +} + +// UpdateAgreement updates an agreement for the enduser by agreementID +func (c Client) UpdateAgreement(ctx context.Context, token string, agreementID string, updateRequestBody UpdateRequestBody) (*Agreement, error) { + var agreement Agreement + // TODO: Check if this is the correct way to update an agreement, The API doc wants to append a /accept to the end of the URL + err := c.HTTP.Put(ctx, AgreementsEndusersPath+agreementID, RequestHeadersWithAuth(token), updateRequestBody, &agreement) + if err != nil { + return nil, err + } + + return &agreement, nil +} diff --git a/agreements_endpoints_test.go b/agreements_endpoints_test.go new file mode 100644 index 0000000..ee5d4ac --- /dev/null +++ b/agreements_endpoints_test.go @@ -0,0 +1,243 @@ +package nordigen_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/weportfolio/go-nordigen" +) + +func TestClient_CreateAgreement(t *testing.T) { + t.Parallel() + + t.Run("create a new agreement", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + }) + + t.Run("create a new agreement with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), "invalid", agreementRequestBody) + assert.Error(t, err) + assert.Nil(t, agreement) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_FetchAgreement(t *testing.T) { + t.Parallel() + + t.Run("fetch an agreement", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + fetchedAgreement, err := client.FetchAgreement(context.Background(), token.Access, agreement.ID) + assert.NoError(t, err) + assert.NotNil(t, fetchedAgreement) + assert.Equal(t, agreement.ID, fetchedAgreement.ID) + }) + + t.Run("fetch an agreement with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), "invalid", agreementRequestBody) + assert.Error(t, err) + assert.Nil(t, agreement) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_ListAgreements(t *testing.T) { + t.Parallel() + + t.Run("list agreements", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + responseAgreements, err := client.ListAgreements(context.Background(), token.Access, nil) + assert.NoError(t, err) + assert.NotNil(t, responseAgreements) + }) + + t.Run("list agreements with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + responseAgreements, err := client.ListAgreements(context.Background(), "invalid", nil) + assert.Error(t, err) + assert.Nil(t, responseAgreements) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_DeleteAgreement(t *testing.T) { + t.Parallel() + + t.Run("delete an agreement", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + err = client.DeleteAgreement(context.Background(), token.Access, agreement.ID) + assert.NoError(t, err) + }) + + t.Run("delete an agreement with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + err := client.DeleteAgreement(context.Background(), "invalid", "invalid") + assert.Error(t, err) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_UpdateAgreement(t *testing.T) { + t.Parallel() + + t.Run("update an agreement", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + updateRequestBody := nordigen.UpdateRequestBody{ + UserAgent: "test", + IPAddress: "0.0.0.0", + } + + updatedAgreement, err := client.UpdateAgreement(context.Background(), token.Access, agreement.ID, updateRequestBody) + assert.NoError(t, err) + assert.NotNil(t, updatedAgreement) + assert.Equal(t, nordigen.TestInstitutionID, updatedAgreement.InstitutionID) + assert.Equal(t, agreement.ID, updatedAgreement.ID) + }) + + t.Run("update an agreement with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + updateRequestBody := nordigen.UpdateRequestBody{ + UserAgent: "test", + IPAddress: "", + } + + updatedAgreement, err := client.UpdateAgreement(context.Background(), "invalid", "invalid", updateRequestBody) + assert.Error(t, err) + assert.Nil(t, updatedAgreement) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} diff --git a/consts/api_info.go b/api_info.go similarity index 58% rename from consts/api_info.go rename to api_info.go index a9004d6..5db993f 100644 --- a/consts/api_info.go +++ b/api_info.go @@ -1,4 +1,4 @@ -package consts +package nordigen const ( NordigenBaseURL = "https://ob.nordigen.com/api" @@ -7,20 +7,15 @@ const ( ) const ( - AccountsPath = "/accounts/:id/" - AccountBalancesPath = "/accounts/:id/balances/" - AccountDetailsPath = "/accounts/:id/details/" - AccountTransactionsPath = "/accounts/:id/transactions/" + AccountsPath = "/accounts/" ) const ( - AccountsTransactionPremiumPath = "/accounts/premium/:id/transactions/" + AccountsTransactionPremiumPath = "/accounts/premium/" ) const ( - AgreementsEndusersPath = "/agreements/enduser/" - AgreementsEnduserPath = "/agreements/enduser/:id/" - AgreementsEnduserAcceptPath = "/agreements/enduser/:id/accept/" + AgreementsEndusersPath = "/agreements/enduser/" ) const ( @@ -28,18 +23,11 @@ const ( ) const ( - PaymentsPath = "/payments/" - PaymentPath = "/payments/:id/" - PaymentSubmitPath = "/payments/:id/submit/" - PaymentsAccountPath = "/payments/account/" - PaymentsCreditorsPath = "/payments/creditors/" - PaymentsCreditorPath = "/payments/creditors/:id/" - PaymentsFieldsInstitutionPath = "/payments/fields/:institution_id/" + PaymentsPath = "/payments/" ) const ( - RequisitionsPath = "/requisitions" - RequisitionPath = "/requisitions/:id/" + RequisitionsPath = "/requisitions/" ) const ( @@ -87,5 +75,5 @@ const ( ) const ( - TestInstitutionID = "REVOLUT_REVOLT21" + TestInstitutionID = "N26_NTSBDEB1" ) diff --git a/assets/cover-treemap.svg b/assets/cover-treemap.svg index 8132a64..ad14bae 100644 --- a/assets/cover-treemap.svg +++ b/assets/cover-treemap.svg @@ -7,153 +7,142 @@ > - + github.com/weportfolio/go-nordigen/endpoints + data-math="N">github.com/weportfolio/go-nordigen - + accounts + data-math="N">accounts_endpoints.go - + agreements + data-math="N">agreements_endpoints.go - + institutions + transform="translate(991.843604,505.958621) scale(0.001810)" + style="font-family: Open Sans, verdana, arial, sans-serif !important; font-size: 12px; fill: rgb(255, 255, 255, 65535); fill-opacity: 1; white-space: pre;" + data-math="N">client.go - + requisitions + data-math="N">error.go - + token + data-math="N">helpers.go - + accounts_endpoints.go + transform="translate(36.000000,57.200000) scale(1.000000)" + style="font-family: Open Sans, verdana, arial, sans-serif !important; font-size: 12px; fill: rgb(0, 0, 0, 65535); fill-opacity: 1; white-space: pre;" + data-math="N">http-client.go - - - - - - - + agreements_endpoints.go + data-math="N">institutions_endpoints.go - - - - - - - - - - - - - + institutions_endpoints.go + data-math="N">payments_endpoints.go - + +premium_endpoints.go + - + requisitions_endpoints.go @@ -161,18 +150,12 @@ - - - - - - - + token_endpoints.go diff --git a/client.go b/client.go index 1d01d4d..b374f24 100644 --- a/client.go +++ b/client.go @@ -1,62 +1,25 @@ package nordigen -import ( - "github.com/weportfolio/go-nordigen/endpoints/accounts" - "github.com/weportfolio/go-nordigen/endpoints/agreements" - "github.com/weportfolio/go-nordigen/endpoints/institutions" - "github.com/weportfolio/go-nordigen/endpoints/payments" - "github.com/weportfolio/go-nordigen/endpoints/requisitions" - "github.com/weportfolio/go-nordigen/endpoints/token" -) - // Client is the Nordigen client type Client struct { - accounts *accounts.Client - token *token.Client - institutions *institutions.Client - agreements *agreements.Client - requisitions *requisitions.Client - payments *payments.Client -} - -// Accounts returns the accounts client -func (c *Client) Accounts() *accounts.Client { - return c.accounts -} - -// Token returns the token client -func (c *Client) Token() *token.Client { - return c.token -} - -// Institutions returns the institutions client -func (c *Client) Institutions() *institutions.Client { - return c.institutions -} - -// Agreements returns the agreements client -func (c *Client) Agreements() *agreements.Client { - return c.agreements -} - -// Requisitions returns the requisitions client -func (c *Client) Requisitions() *requisitions.Client { - return c.requisitions + HTTP IHTTPClient + SecretID string + SecretKey string } -// Payments returns the payments client -func (c *Client) Payments() *payments.Client { - return c.payments +type Config struct { + BaseURL string + APIVersion string + SecretID string + SecretKey string + HTTP *Client } // New creates a new Nordigen client -func New(secretID, secretKey string) *Client { +func New(config *Config) *Client { return &Client{ - accounts: accounts.New(secretID, secretKey), - token: token.New(secretID, secretKey), - institutions: institutions.New(secretID, secretKey), - agreements: agreements.New(secretID, secretKey), - requisitions: requisitions.New(secretID, secretKey), - payments: payments.New(secretID, secretKey), + HTTP: NewHTTPClient(config.BaseURL, config.APIVersion), + SecretID: config.SecretID, + SecretKey: config.SecretKey, } } diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..516ca12 --- /dev/null +++ b/client_test.go @@ -0,0 +1,41 @@ +package nordigen_test + +import ( + "fmt" + "os" + "testing" + + "github.com/weportfolio/go-nordigen" +) + +func getTestClient(t *testing.T) *nordigen.Client { + t.Helper() + + secretID := os.Getenv("NORDIGEN_SECRET_ID") + secretKey := os.Getenv("NORDIGEN_SECRET_KEY") + if secretID == "" || secretKey == "" { + fmt.Println("NORDIGEN_SECRET_ID or NORDIGEN_SECRET_KEY is not set") + } + + return nordigen.New( + &nordigen.Config{ + BaseURL: nordigen.NordigenBaseURL, + APIVersion: nordigen.APIVersion, + SecretID: secretID, + SecretKey: secretKey, + }, + ) +} + +func getInvalidTestClient(t *testing.T) *nordigen.Client { + t.Helper() + + return nordigen.New( + &nordigen.Config{ + BaseURL: nordigen.NordigenBaseURL, + APIVersion: nordigen.APIVersion, + SecretID: "invalid", + SecretKey: "invalid", + }, + ) +} diff --git a/endpoints/accounts/accounts.go b/endpoints/accounts/accounts.go deleted file mode 100644 index a8acc6a..0000000 --- a/endpoints/accounts/accounts.go +++ /dev/null @@ -1,74 +0,0 @@ -package accounts - -import "time" - -type Account struct { - ID string `json:"id"` - Created string `json:"created"` - LastAccessed string `json:"last_accessed"` - IBAN string `json:"iban"` - InstitutionID string `json:"institution_id"` - Status string `json:"status"` - OwnerName string `json:"owner_name"` -} - -type Balance struct { - BalanceAmount Amount `json:"balanceAmount"` - BalanceType string `json:"balanceType"` - ReferenceDate string `json:"referenceDate"` -} - -type Amount struct { - Amount string `json:"amount"` - Currency string `json:"currency"` -} - -type Balances struct { - Balances []Balance `json:"balances"` -} - -type AccountDetails struct { - ResourceID string `json:"resourceId"` - IBAN string `json:"iban"` - Currency string `json:"currency"` - OwnerName string `json:"ownerName"` - Name string `json:"name"` - Product string `json:"product"` - CashAccountType string `json:"cashAccountType"` -} - -type Details struct { - Account AccountDetails `json:"account"` -} - -type TransactionParams struct { - DateFrom string `url:"date_from,omitempty" json:"date_from,omitempty"` - DateTo string `url:"date_to,omitempty" json:"date_to,omitempty"` -} - -type Transaction struct { - TransactionID string `json:"transactionId"` - BookingDate string `json:"bookingDate"` - ValueDate string `json:"valueDate"` - BookingDateTime time.Time `json:"bookingDateTime"` - ValueDateTime time.Time `json:"valueDateTime"` - TransactionAmount Amount `json:"transactionAmount"` - CreditorName string `json:"creditorName"` - CreditorAccount Account `json:"creditorAccount"` - DebtorName string `json:"debtorName"` - DebtorAccount Account `json:"debtorAccount"` - BankTransactionCode string `json:"bankTransactionCode"` - RemittanceInformationUnstructured string `json:"remittanceInformationUnstructured"` - RemittanceInformationUnstructuredArray []string `json:"remittanceInformationUnstructuredArray"` - ProprietaryBankTransactionCode string `json:"proprietaryBankTransactionCode"` - InternalTransactionID string `json:"internalTransactionId"` -} - -type Transactions struct { - Transactions TransactionList `json:"transactions"` -} - -type TransactionList struct { - Booked []Transaction `json:"booked"` - Pending []Transaction `json:"pending"` -} diff --git a/endpoints/accounts/accounts_endpoints.go b/endpoints/accounts/accounts_endpoints.go deleted file mode 100644 index b6e1286..0000000 --- a/endpoints/accounts/accounts_endpoints.go +++ /dev/null @@ -1,52 +0,0 @@ -package accounts - -import ( - "context" - "fmt" - "github.com/weportfolio/go-nordigen/consts" -) - -// GetAccount retrieves an account by ID -func (c Client) GetAccount(ctx context.Context, token string, accountID string) (*Account, error) { - var account Account - endpointURL := fmt.Sprintf("/accounts/%s", accountID) - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &account) - if err != nil { - return nil, err - } - return &account, nil -} - -// GetBalances retrieves balances for an account by ID -func (c Client) GetBalances(ctx context.Context, token string, accountID string) (*Balances, error) { - var balances Balances - endpointURL := fmt.Sprintf("/accounts/%s/balances", accountID) - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &balances) - if err != nil { - return nil, err - } - - return &balances, nil -} - -func (c Client) GetDetails(ctx context.Context, token string, accountID string) (*Details, error) { - var details Details - endpointURL := fmt.Sprintf("/accounts/%s/details", accountID) - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &details) - if err != nil { - return nil, err - } - - return &details, nil -} - -func (c Client) GetTransactions(ctx context.Context, token string, accountID string) (*Transactions, error) { - var transactions Transactions - endpointURL := fmt.Sprintf("/accounts/%s/transactions", accountID) - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &transactions) - if err != nil { - return nil, err - } - - return &transactions, nil -} diff --git a/endpoints/accounts/client.go b/endpoints/accounts/client.go deleted file mode 100644 index 2552843..0000000 --- a/endpoints/accounts/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package accounts - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP http.IClient -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/agreements/agreements_endpoints.go b/endpoints/agreements/agreements_endpoints.go deleted file mode 100644 index 3439d3b..0000000 --- a/endpoints/agreements/agreements_endpoints.go +++ /dev/null @@ -1,74 +0,0 @@ -package agreements - -import ( - "context" - "strconv" - - "github.com/weportfolio/go-nordigen/consts" -) - -// Post creates a new agreement for the enduser -func (c Client) Post(ctx context.Context, token string, agreementRequestBody AgreementRequestBody) (*Agreement, error) { - var agreement Agreement - err := c.HTTP.Post(ctx, consts.AgreementsEndusersPath, consts.RequestHeadersWithAuth(token), agreementRequestBody, &agreement) - if err != nil { - return nil, err - } - - return &agreement, nil -} - -// Fetch retrieves an agreement for the enduser by agreementID -func (c Client) Fetch(ctx context.Context, token string, agreementID string) (*Agreement, error) { - var agreement Agreement - err := c.HTTP.Get(ctx, consts.AgreementsEndusersPath+agreementID, consts.RequestHeadersWithAuth(token), &agreement) - if err != nil { - return nil, err - } - - return &agreement, nil -} - -// List returns a list of agreements for the enduser -func (c Client) List(ctx context.Context, token string, requestParams *ListRequestParams) (*Agreements, error) { - var agreements Agreements - - endpointURL := consts.AgreementsEndusersPath - if requestParams != nil { - if requestParams.Limit != 0 { - endpointURL = endpointURL + "?" + strconv.Itoa(requestParams.Limit) - } - if requestParams.Offset != 0 { - endpointURL = endpointURL + "&" + strconv.Itoa(requestParams.Offset) - } - } - - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &agreements) - if err != nil { - return nil, err - } - - return &agreements, nil -} - -// Delete deletes an agreement for the enduser by agreementID -func (c Client) Delete(ctx context.Context, token string, agreementID string) error { - err := c.HTTP.Delete(ctx, consts.AgreementsEndusersPath+agreementID, consts.RequestHeadersWithAuth(token), nil) - if err != nil { - return err - } - - return nil -} - -// Update updates an agreement for the enduser by agreementID -func (c Client) Update(ctx context.Context, token string, agreementID string, updateRequestBody UpdateRequestBody) (*Agreement, error) { - var agreement Agreement - // TODO: Check if this is the correct way to update an agreement, The API doc wants to append a /accept to the end of the URL - err := c.HTTP.Put(ctx, consts.AgreementsEndusersPath+agreementID, consts.RequestHeadersWithAuth(token), updateRequestBody, &agreement) - if err != nil { - return nil, err - } - - return &agreement, nil -} diff --git a/endpoints/agreements/agreements_endpoints_test.go b/endpoints/agreements/agreements_endpoints_test.go deleted file mode 100644 index 825b2e6..0000000 --- a/endpoints/agreements/agreements_endpoints_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package agreements_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/weportfolio/go-nordigen" - "github.com/weportfolio/go-nordigen/consts" - "github.com/weportfolio/go-nordigen/endpoints/agreements" -) - -func TestClient_Post(t *testing.T) { - t.Parallel() - - t.Run("create a new agreement", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - }) - - t.Run("create a new agreement with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), "invalid", agreementRequestBody) - assert.Error(t, err) - assert.Nil(t, agreement) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_Fetch(t *testing.T) { - t.Parallel() - - t.Run("fetch an agreement", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - fetchedAgreement, err := client.Agreements().Fetch(context.Background(), token.Access, agreement.ID) - assert.NoError(t, err) - assert.NotNil(t, fetchedAgreement) - assert.Equal(t, agreement.ID, fetchedAgreement.ID) - }) - - t.Run("fetch an agreement with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), "invalid", agreementRequestBody) - assert.Error(t, err) - assert.Nil(t, agreement) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_List(t *testing.T) { - t.Parallel() - - t.Run("list agreements", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - responseAgreements, err := client.Agreements().List(context.Background(), token.Access, nil) - assert.NoError(t, err) - assert.NotNil(t, responseAgreements) - }) - - t.Run("list agreements with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - responseAgreements, err := client.Agreements().List(context.Background(), "invalid", nil) - assert.Error(t, err) - assert.Nil(t, responseAgreements) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_Delete(t *testing.T) { - t.Parallel() - - t.Run("delete an agreement", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - err = client.Agreements().Delete(context.Background(), token.Access, agreement.ID) - assert.NoError(t, err) - }) - - t.Run("delete an agreement with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - err := client.Agreements().Delete(context.Background(), "invalid", "invalid") - assert.Error(t, err) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_Update(t *testing.T) { - t.Parallel() - - t.Run("update an agreement", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - updateRequestBody := agreements.UpdateRequestBody{ - UserAgent: "test", - IPAddress: "0.0.0.0", - } - - updatedAgreement, err := client.Agreements().Update(context.Background(), token.Access, agreement.ID, updateRequestBody) - assert.NoError(t, err) - assert.NotNil(t, updatedAgreement) - assert.Equal(t, consts.TestInstitutionID, updatedAgreement.InstitutionID) - assert.Equal(t, agreement.ID, updatedAgreement.ID) - }) - - t.Run("update an agreement with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - updateRequestBody := agreements.UpdateRequestBody{ - UserAgent: "test", - IPAddress: "", - } - - updatedAgreement, err := client.Agreements().Update(context.Background(), "invalid", "invalid", updateRequestBody) - assert.Error(t, err) - assert.Nil(t, updatedAgreement) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} diff --git a/endpoints/agreements/client.go b/endpoints/agreements/client.go deleted file mode 100644 index 97d8e38..0000000 --- a/endpoints/agreements/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package agreements - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP http.IClient -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/institutions/client.go b/endpoints/institutions/client.go deleted file mode 100644 index ef43e69..0000000 --- a/endpoints/institutions/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package institutions - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP *http.Client -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/institutions/institutions_endpoints.go b/endpoints/institutions/institutions_endpoints.go deleted file mode 100644 index c3ba22b..0000000 --- a/endpoints/institutions/institutions_endpoints.go +++ /dev/null @@ -1,39 +0,0 @@ -package institutions - -import ( - "context" - - "github.com/weportfolio/go-nordigen/consts" -) - -// List returns a list of institutions -func (c *Client) List(ctx context.Context, token string, country string, paymentsEnabled bool) ([]Institution, error) { - endpointURL := consts.InstitutionsPath + "?country=" + country + "&payments_enabled=" + boolToString(paymentsEnabled) - var institutions []Institution - - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &institutions) - if err != nil { - return nil, err - } - - return institutions, nil -} - -// Get returns an institution -func (c *Client) Get(ctx context.Context, token, id string) (*Institution, error) { - var institution Institution - err := c.HTTP.Get(ctx, consts.InstitutionsPath+"/"+id, consts.RequestHeadersWithAuth(token), &institution) - if err != nil { - return nil, err - } - - return &institution, nil -} - -// boolToString converts a bool to a string -func boolToString(b bool) string { - if b { - return "true" - } - return "false" -} diff --git a/endpoints/institutions/institutions_endpoints_test.go b/endpoints/institutions/institutions_endpoints_test.go deleted file mode 100644 index 1600845..0000000 --- a/endpoints/institutions/institutions_endpoints_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package institutions_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/weportfolio/go-nordigen" - "github.com/weportfolio/go-nordigen/consts" -) - -func TestClient_List(t *testing.T) { - t.Parallel() - - t.Run("list institutions", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - institutions, err := client.Institutions().List(context.Background(), token.Access, consts.NetherlandsInstitution, true) - assert.NoError(t, err) - assert.NotNil(t, institutions) - }) - - t.Run("list institutions with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - institutions, err := client.Institutions().List(context.Background(), "invalid", consts.NetherlandsInstitution, true) - assert.Error(t, err) - assert.Nil(t, institutions) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} diff --git a/endpoints/payments/client.go b/endpoints/payments/client.go deleted file mode 100644 index 281d033..0000000 --- a/endpoints/payments/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package payments - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP http.IClient -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/payments/payments.go b/endpoints/payments/payments.go deleted file mode 100644 index d0eb223..0000000 --- a/endpoints/payments/payments.go +++ /dev/null @@ -1,44 +0,0 @@ -package payments - -import "github.com/weportfolio/go-nordigen/endpoints/accounts" - -type ListParams struct { - Limit int `url:"limit,omitempty"` - Offset int `url:"offset,omitempty"` -} - -type Payment struct { - PaymentID string `json:"payment_id"` - PaymentStatus string `json:"payment_status"` - PaymentProduct string `json:"payment_product"` - PaymentType string `json:"payment_type"` - Redirect string `json:"redirect"` - Description string `json:"description"` - CustomPaymentID string `json:"custom_payment_id"` - CreditorAccount string `json:"creditor_account"` - CreditorObject PaymentAccount `json:"creditor_object"` - DebtorObject PaymentAccount `json:"debtor_object"` - InstructedAmount accounts.Amount `json:"instructed_amount"` -} - -type Payments struct { - Code int `json:"code"` - Next string `json:"next"` - Previous string `json:"previous"` - Results []Payment `json:"results"` -} - -type PaymentAccount struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Account string `json:"account"` - Currency string `json:"currency"` - AddressCountry string `json:"address_country"` - InstitutionID string `json:"institution_id"` - Agent string `json:"agent"` - AgentName string `json:"agent_name"` - AddressStreet string `json:"address_street"` - PostCode string `json:"post_code"` - TypeNumber string `json:"type_number"` -} diff --git a/endpoints/payments/payments_endpoints.go b/endpoints/payments/payments_endpoints.go deleted file mode 100644 index 531f48d..0000000 --- a/endpoints/payments/payments_endpoints.go +++ /dev/null @@ -1,24 +0,0 @@ -package payments - -import ( - "context" - "github.com/weportfolio/go-nordigen/consts" - "strconv" -) - -// List returns a list of payments -func (c Client) List(ctx context.Context, token string, limit, offset int) (*Payments, error) { - var response Payments - - params := map[string]string{ - "limit": strconv.Itoa(limit), - "offset": strconv.Itoa(offset), - } - - err := c.HTTP.Get(ctx, consts.BuildQueryURL(consts.PaymentsPath, params), consts.RequestHeadersWithAuth(token), &response) - if err != nil { - return nil, err - } - - return &response, nil -} diff --git a/endpoints/premium/.gitkeep b/endpoints/premium/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/endpoints/requisitions/client.go b/endpoints/requisitions/client.go deleted file mode 100644 index c528332..0000000 --- a/endpoints/requisitions/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package requisitions - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP *http.Client -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/requisitions/requisitions_endpoints.go b/endpoints/requisitions/requisitions_endpoints.go deleted file mode 100644 index 08b433b..0000000 --- a/endpoints/requisitions/requisitions_endpoints.go +++ /dev/null @@ -1,61 +0,0 @@ -package requisitions - -import ( - "context" - "strconv" - - "github.com/weportfolio/go-nordigen/consts" -) - -// Post creates a new requisition -func (c Client) Post(ctx context.Context, token string, requisitionRequestBody *RequisitionRequestBody) (*Requisition, error) { - var requisition Requisition - err := c.HTTP.Post(ctx, consts.RequisitionsPath+"/", consts.RequestHeadersWithAuth(token), requisitionRequestBody, &requisition) - if err != nil { - return nil, err - } - - return &requisition, nil -} - -func (c Client) List(ctx context.Context, token string, requestParams *ListRequestParams) (*Requisitions, error) { - var requisitions Requisitions - - endpointURL := consts.RequisitionsPath + "/" - if requestParams != nil { - if requestParams.Limit != 0 { - endpointURL = endpointURL + "?" + strconv.Itoa(requestParams.Limit) - } - if requestParams.Offset != 0 { - endpointURL = endpointURL + "&" + strconv.Itoa(requestParams.Offset) - } - } - - err := c.HTTP.Get(ctx, endpointURL, consts.RequestHeadersWithAuth(token), &requisitions) - if err != nil { - return nil, err - } - - return &requisitions, nil -} - -// Fetch retrieves a requisition by requisitionID -func (c Client) Fetch(ctx context.Context, token string, requisitionID string) (*Requisition, error) { - var requisition Requisition - err := c.HTTP.Get(ctx, consts.RequisitionsPath+"/"+requisitionID, consts.RequestHeadersWithAuth(token), &requisition) - if err != nil { - return nil, err - } - - return &requisition, nil -} - -// Delete deletes a requisition by requisitionID -func (c Client) Delete(ctx context.Context, token string, requisitionID string) error { - err := c.HTTP.Delete(ctx, consts.RequisitionsPath+"/"+requisitionID, consts.RequestHeadersWithAuth(token), nil) - if err != nil { - return err - } - - return nil -} diff --git a/endpoints/requisitions/requisitions_endpoints_test.go b/endpoints/requisitions/requisitions_endpoints_test.go deleted file mode 100644 index 2be72d0..0000000 --- a/endpoints/requisitions/requisitions_endpoints_test.go +++ /dev/null @@ -1,273 +0,0 @@ -package requisitions_test - -import ( - "context" - "github.com/stretchr/testify/assert" - "github.com/weportfolio/go-nordigen" - "github.com/weportfolio/go-nordigen/consts" - "github.com/weportfolio/go-nordigen/endpoints/agreements" - "github.com/weportfolio/go-nordigen/endpoints/requisitions" - "math/rand" - "strconv" - "testing" -) - -func TestClient_Post(t *testing.T) { - t.Parallel() - - t.Run("create new requisition", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - requisitionRequestBody := &requisitions.RequisitionRequestBody{ - Redirect: "https://example.com", - InstitutionID: consts.TestInstitutionID, - Agreement: agreement.ID, - Reference: strconv.Itoa(rand.Intn(1000000000000000000)), - UserLanguage: consts.LangEN, - AccountSelection: false, - RedirectImmediate: false, - } - - requisition, err := client.Requisitions().Post(context.Background(), token.Access, requisitionRequestBody) - assert.NoError(t, err) - assert.NotNil(t, requisition) - assert.Equal(t, consts.TestInstitutionID, requisition.InstitutionID) - }) - - t.Run("create new requisition with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - requisitionRequestBody := &requisitions.RequisitionRequestBody{ - Redirect: "https://example.com", - InstitutionID: consts.TestInstitutionID, - Agreement: "invalid", - Reference: "12345", - UserLanguage: consts.LangEN, - AccountSelection: false, - RedirectImmediate: false, - } - - requisition, err := client.Requisitions().Post(context.Background(), "invalid", requisitionRequestBody) - assert.Error(t, err) - assert.Nil(t, requisition) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_List(t *testing.T) { - t.Parallel() - - t.Run("list requisitions", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - requisitionRequestBody := &requisitions.RequisitionRequestBody{ - Redirect: "https://example.com", - InstitutionID: consts.TestInstitutionID, - Agreement: agreement.ID, - Reference: strconv.Itoa(rand.Intn(1000000000000000000)), - UserLanguage: consts.LangEN, - AccountSelection: false, - RedirectImmediate: false, - } - - requisition, err := client.Requisitions().Post(context.Background(), token.Access, requisitionRequestBody) - assert.NoError(t, err) - assert.NotNil(t, requisition) - assert.Equal(t, consts.TestInstitutionID, requisition.InstitutionID) - - responseRequisitions, err := client.Requisitions().List(context.Background(), token.Access, nil) - assert.NoError(t, err) - assert.NotNil(t, responseRequisitions) - }) - - t.Run("list requisitions with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - responseRequisitions, err := client.Requisitions().List(context.Background(), "invalid", nil) - assert.Error(t, err) - assert.Nil(t, responseRequisitions) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_Fetch(t *testing.T) { - t.Parallel() - - t.Run("fetch requisition", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - requisitionRequestBody := &requisitions.RequisitionRequestBody{ - Redirect: "https://example.com", - InstitutionID: consts.TestInstitutionID, - Agreement: agreement.ID, - Reference: strconv.Itoa(rand.Intn(1000000000000000000)), - UserLanguage: consts.LangEN, - AccountSelection: false, - RedirectImmediate: false, - } - - requisition, err := client.Requisitions().Post(context.Background(), token.Access, requisitionRequestBody) - assert.NoError(t, err) - assert.NotNil(t, requisition) - assert.Equal(t, consts.TestInstitutionID, requisition.InstitutionID) - - responseRequisition, err := client.Requisitions().Fetch(context.Background(), token.Access, requisition.ID) - assert.NoError(t, err) - assert.NotNil(t, responseRequisition) - }) - - t.Run("fetch requisition with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - responseRequisition, err := client.Requisitions().Fetch(context.Background(), "invalid", "invalid") - assert.Error(t, err) - assert.Nil(t, responseRequisition) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} - -func TestClient_Delete(t *testing.T) { - t.Parallel() - - t.Run("delete requisition", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - token, err := client.Token().New(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, token) - - agreementRequestBody := agreements.AgreementRequestBody{ - InstitutionID: consts.TestInstitutionID, - MaxHistoricalDays: "180", - AccessValidForDays: "2", - AccessScope: []string{"balances", "details", "transactions"}, - } - - agreement, err := client.Agreements().Post(context.Background(), token.Access, agreementRequestBody) - assert.NoError(t, err) - assert.NotNil(t, agreement) - assert.Equal(t, consts.TestInstitutionID, agreement.InstitutionID) - - requisitionRequestBody := &requisitions.RequisitionRequestBody{ - Redirect: "https://example.com", - InstitutionID: consts.TestInstitutionID, - Agreement: agreement.ID, - Reference: strconv.Itoa(rand.Intn(1000000000000000000)), - UserLanguage: consts.LangEN, - AccountSelection: false, - RedirectImmediate: false, - } - - requisition, err := client.Requisitions().Post(context.Background(), token.Access, requisitionRequestBody) - assert.NoError(t, err) - assert.NotNil(t, requisition) - assert.Equal(t, consts.TestInstitutionID, requisition.InstitutionID) - - err = client.Requisitions().Delete(context.Background(), token.Access, requisition.ID) - assert.NoError(t, err) - }) - - t.Run("delete requisition with invalid token", func(t *testing.T) { - t.Parallel() - - client := nordigen.New( - consts.GetSecrets(t), - ) - assert.NotNil(t, client) - - err := client.Requisitions().Delete(context.Background(), "invalid", "invalid") - assert.Error(t, err) - - checkErr := consts.ExtractError(err) - assert.Equal(t, 401, checkErr.StatusCode) - }) -} diff --git a/endpoints/token/client.go b/endpoints/token/client.go deleted file mode 100644 index ddd6872..0000000 --- a/endpoints/token/client.go +++ /dev/null @@ -1,13 +0,0 @@ -package token - -import "github.com/weportfolio/go-nordigen/http" - -type Client struct { - HTTP *http.Client -} - -func New(secretID, secretKey string) *Client { - return &Client{ - HTTP: http.New(secretID, secretKey), - } -} diff --git a/endpoints/token/token_endpoints.go b/endpoints/token/token_endpoints.go deleted file mode 100644 index 9a34951..0000000 --- a/endpoints/token/token_endpoints.go +++ /dev/null @@ -1,38 +0,0 @@ -package token - -import ( - "context" - - "github.com/weportfolio/go-nordigen/consts" -) - -// New creates a new token -func (c Client) New(ctx context.Context) (*Token, error) { - var token Token - accessCreds := map[string]string{ - "secret_id": c.HTTP.APISecretID, - "secret_key": c.HTTP.APISecretKey, - } - - err := c.HTTP.Post(ctx, consts.TokenNewPath, consts.RequestHeaders(), accessCreds, &token) - if err != nil { - return nil, err - } - - return &token, nil -} - -// Refresh refreshes a token -func (c Client) Refresh(ctx context.Context, refreshToken string) (*Token, error) { - var token Token - refreshCreds := map[string]string{ - "refresh": refreshToken, - } - - err := c.HTTP.Post(ctx, consts.TokenRefreshPath, consts.RequestHeaders(), refreshCreds, &token) - if err != nil { - return nil, err - } - - return &token, nil -} diff --git a/consts/error.go b/error.go similarity index 97% rename from consts/error.go rename to error.go index 881c407..6363244 100644 --- a/consts/error.go +++ b/error.go @@ -1,4 +1,4 @@ -package consts +package nordigen import ( "encoding/json" diff --git a/go.mod b/go.mod index f2a4134..c7f95f9 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,7 @@ module github.com/weportfolio/go-nordigen go 1.20 -require ( - github.com/golang/mock v1.6.0 - github.com/stretchr/testify v1.8.2 -) +require github.com/stretchr/testify v1.8.2 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 2d9ed42..6a56e69 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ 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/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 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= @@ -12,29 +10,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= diff --git a/consts/helpers.go b/helpers.go similarity index 67% rename from consts/helpers.go rename to helpers.go index 59f8ab0..3f16425 100644 --- a/consts/helpers.go +++ b/helpers.go @@ -1,23 +1,9 @@ -package consts +package nordigen import ( "fmt" - "os" - "testing" ) -func GetSecrets(t *testing.T) (string, string) { - t.Helper() - - secretID := os.Getenv("NORDIGEN_SECRET_ID") - secretKey := os.Getenv("NORDIGEN_SECRET_KEY") - if secretID == "" || secretKey == "" { - fmt.Println("NORDIGEN_SECRET_ID or NORDIGEN_SECRET_KEY is not set") - } - - return secretID, secretKey -} - func RequestHeadersWithAuth(token string) map[string]string { authHeaders := map[string]string{ "Authorization": fmt.Sprintf("Bearer %s", token), diff --git a/http/http-client.go b/http-client.go similarity index 58% rename from http/http-client.go rename to http-client.go index 3a4066e..3961874 100644 --- a/http/http-client.go +++ b/http-client.go @@ -1,4 +1,4 @@ -package http +package nordigen import ( "bytes" @@ -6,61 +6,50 @@ import ( "encoding/json" "fmt" "net/http" - - "github.com/weportfolio/go-nordigen/consts" -) - -const ( - NordigenBaseURL = "https://ob.nordigen.com/api" - APIVersion = "v2" ) //go:generate mockgen -source=http-client.go -destination=mocks/mock_client.go -package=mocks -build_flags=-mod=mod -type IClient interface { +type IHTTPClient interface { Get(ctx context.Context, path string, headers map[string]string, response interface{}) error Post(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error Put(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error Delete(ctx context.Context, path string, headers map[string]string, response interface{}) error } -type Client struct { - BaseURL string - APIVersion string - APISecretID string - APISecretKey string +type HTTPClient struct { + BaseURL string + APIVersion string } -func New(secretID, secretKey string) *Client { - return &Client{ - BaseURL: NordigenBaseURL, - APIVersion: APIVersion, - APISecretID: secretID, - APISecretKey: secretKey, +func NewHTTPClient(baseURL, apiVersion string) IHTTPClient { + return &HTTPClient{ + BaseURL: baseURL, + APIVersion: apiVersion, } } // Get is a wrapper around request that performs a GET request -func (c *Client) Get(ctx context.Context, path string, headers map[string]string, response interface{}) error { - return c.request(ctx, http.MethodGet, path, headers, nil, response) +func (h *HTTPClient) Get(ctx context.Context, path string, headers map[string]string, response interface{}) error { + return h.request(ctx, http.MethodGet, path, headers, nil, response) } // Post is a wrapper around request that performs a POST request -func (c *Client) Post(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error { - return c.request(ctx, http.MethodPost, path, headers, body, response) +func (h *HTTPClient) Post(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error { + return h.request(ctx, http.MethodPost, path, headers, body, response) } // Put is a wrapper around request that performs a PUT request -func (c *Client) Put(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error { - return c.request(ctx, http.MethodPut, path, headers, body, response) +func (h *HTTPClient) Put(ctx context.Context, path string, headers map[string]string, body interface{}, response interface{}) error { + return h.request(ctx, http.MethodPut, path, headers, body, response) } // Delete is a wrapper around request that performs a DELETE request -func (c *Client) Delete(ctx context.Context, path string, headers map[string]string, response interface{}) error { - return c.request(ctx, http.MethodDelete, path, headers, nil, response) +func (h *HTTPClient) Delete(ctx context.Context, path string, headers map[string]string, response interface{}) error { + return h.request(ctx, http.MethodDelete, path, headers, nil, response) } -// request is a wrapper around http.Client.Do that performs a request and decodes the response into the response interface -func (c *Client) request(ctx context.Context, method, path string, headers map[string]string, body interface{}, response interface{}) error { +// request is a wrapper around http.HTTPClient.Do that performs a request and decodes the response into the response interface +func (h *HTTPClient) request(ctx context.Context, method, path string, headers map[string]string, body interface{}, response interface{}) error { var bytesBody []byte var err error if body != nil { @@ -70,7 +59,7 @@ func (c *Client) request(ctx context.Context, method, path string, headers map[s } } - req, err := c.newRequest(ctx, method, path, headers, bytesBody) + req, err := h.newRequest(ctx, method, path, headers, bytesBody) if err != nil { return fmt.Errorf("failed to create request: %w", err) } @@ -82,7 +71,7 @@ func (c *Client) request(ctx context.Context, method, path string, headers map[s } if resp.StatusCode > 300 { - return consts.NewError(resp) + return NewError(resp) } if response != nil { @@ -93,8 +82,8 @@ func (c *Client) request(ctx context.Context, method, path string, headers map[s } // newRequest creates a new http.Request with the given method, path, headers and body -func (c *Client) newRequest(ctx context.Context, method, path string, headers map[string]string, body []byte) (*http.Request, error) { - url := c.BaseURL + "/" + c.APIVersion + path +func (h *HTTPClient) newRequest(ctx context.Context, method, path string, headers map[string]string, body []byte) (*http.Request, error) { + url := h.BaseURL + "/" + h.APIVersion + path var req *http.Request var err error diff --git a/http/mocks/mock_client.go b/http/mocks/mock_client.go deleted file mode 100644 index c5326ee..0000000 --- a/http/mocks/mock_client.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: http-client.go - -// Package mocks is a generated GoMock package. -package mocks - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockIClient is a mock of IClient interface. -type MockIClient struct { - ctrl *gomock.Controller - recorder *MockIClientMockRecorder -} - -// MockIClientMockRecorder is the mock recorder for MockIClient. -type MockIClientMockRecorder struct { - mock *MockIClient -} - -// NewMockIClient creates a new mock instance. -func NewMockIClient(ctrl *gomock.Controller) *MockIClient { - mock := &MockIClient{ctrl: ctrl} - mock.recorder = &MockIClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIClient) EXPECT() *MockIClientMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockIClient) Delete(path string, params map[string]string, response interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", path, params, response) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockIClientMockRecorder) Delete(path, params, response interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockIClient)(nil).Delete), path, params, response) -} - -// Get mocks base method. -func (m *MockIClient) Get(path string, params map[string]string, response interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", path, params, response) - ret0, _ := ret[0].(error) - return ret0 -} - -// Get indicates an expected call of Get. -func (mr *MockIClientMockRecorder) Get(path, params, response interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockIClient)(nil).Get), path, params, response) -} - -// Post mocks base method. -func (m *MockIClient) Post(path string, params map[string]string, body []byte, response interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Post", path, params, body, response) - ret0, _ := ret[0].(error) - return ret0 -} - -// Post indicates an expected call of Post. -func (mr *MockIClientMockRecorder) Post(path, params, body, response interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockIClient)(nil).Post), path, params, body, response) -} - -// Put mocks base method. -func (m *MockIClient) Put(path string, params map[string]string, body []byte, response interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Put", path, params, body, response) - ret0, _ := ret[0].(error) - return ret0 -} - -// Put indicates an expected call of Put. -func (mr *MockIClientMockRecorder) Put(path, params, body, response interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockIClient)(nil).Put), path, params, body, response) -} diff --git a/endpoints/institutions/institutions.go b/institutions.go similarity index 96% rename from endpoints/institutions/institutions.go rename to institutions.go index 150f322..7fc6944 100644 --- a/endpoints/institutions/institutions.go +++ b/institutions.go @@ -1,4 +1,4 @@ -package institutions +package nordigen type Institution struct { ID string `json:"id"` diff --git a/institutions_endpoints.go b/institutions_endpoints.go new file mode 100644 index 0000000..ddbcbdf --- /dev/null +++ b/institutions_endpoints.go @@ -0,0 +1,37 @@ +package nordigen + +import ( + "context" +) + +// ListInstitutions returns a list of institutions +func (c Client) ListInstitutions(ctx context.Context, token string, country string, paymentsEnabled bool) ([]Institution, error) { + endpointURL := InstitutionsPath + "?country=" + country + "&payments_enabled=" + boolToString(paymentsEnabled) + var institutions []Institution + + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &institutions) + if err != nil { + return nil, err + } + + return institutions, nil +} + +// FetchInstitution returns an institution +func (c Client) FetchInstitution(ctx context.Context, token, id string) (*Institution, error) { + var institution Institution + err := c.HTTP.Get(ctx, InstitutionsPath+id, RequestHeadersWithAuth(token), &institution) + if err != nil { + return nil, err + } + + return &institution, nil +} + +// boolToString converts a bool to a string +func boolToString(b bool) string { + if b { + return "true" + } + return "false" +} diff --git a/institutions_endpoints_test.go b/institutions_endpoints_test.go new file mode 100644 index 0000000..f9a6a50 --- /dev/null +++ b/institutions_endpoints_test.go @@ -0,0 +1,76 @@ +package nordigen_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/weportfolio/go-nordigen" +) + +func TestClient_ListInstitutions(t *testing.T) { + t.Parallel() + + t.Run("list institutions", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + institutions, err := client.ListInstitutions(context.Background(), token.Access, nordigen.NetherlandsInstitution, true) + assert.NoError(t, err) + assert.NotNil(t, institutions) + }) + + t.Run("list institutions with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + institutions, err := client.ListInstitutions(context.Background(), "invalid", nordigen.NetherlandsInstitution, true) + assert.Error(t, err) + assert.Nil(t, institutions) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_FetchInstitution(t *testing.T) { + t.Parallel() + + t.Run("fetch institution", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + institution, err := client.FetchInstitution(context.Background(), token.Access, nordigen.TestInstitutionID) + assert.NoError(t, err) + assert.NotNil(t, institution) + assert.Equal(t, nordigen.TestInstitutionID, institution.ID) + }) + + t.Run("fetch institution with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + institution, err := client.FetchInstitution(context.Background(), "invalid", nordigen.TestInstitutionID) + assert.Error(t, err) + assert.Nil(t, institution) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} diff --git a/payments.go b/payments.go new file mode 100644 index 0000000..573f3cd --- /dev/null +++ b/payments.go @@ -0,0 +1,37 @@ +package nordigen + +type Payment struct { + PaymentID string `json:"payment_id"` + PaymentStatus string `json:"payment_status"` + PaymentProduct string `json:"payment_product"` + PaymentType string `json:"payment_type"` + Redirect string `json:"redirect"` + Description string `json:"description"` + CustomPaymentID string `json:"custom_payment_id"` + CreditorAccount string `json:"creditor_account"` + CreditorObject PaymentAccount `json:"creditor_object"` + DebtorObject PaymentAccount `json:"debtor_object"` + InstructedAmount Amount `json:"instructed_amount"` +} + +type Payments struct { + Code int `json:"code"` + Next string `json:"next"` + Previous string `json:"previous"` + Results []Payment `json:"results"` +} + +type PaymentAccount struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Account string `json:"account"` + Currency string `json:"currency"` + AddressCountry string `json:"address_country"` + InstitutionID string `json:"institution_id"` + Agent string `json:"agent"` + AgentName string `json:"agent_name"` + AddressStreet string `json:"address_street"` + PostCode string `json:"post_code"` + TypeNumber string `json:"type_number"` +} diff --git a/payments_endpoints.go b/payments_endpoints.go new file mode 100644 index 0000000..9d3da2b --- /dev/null +++ b/payments_endpoints.go @@ -0,0 +1,23 @@ +package nordigen + +import ( + "context" + "strconv" +) + +// ListPayments returns a list of payments +func (c Client) ListPayments(ctx context.Context, token string, limit, offset int) (*Payments, error) { + var response Payments + + params := map[string]string{ + "limit": strconv.Itoa(limit), + "offset": strconv.Itoa(offset), + } + + err := c.HTTP.Get(ctx, BuildQueryURL(PaymentsPath, params), RequestHeadersWithAuth(token), &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/endpoints/payments/payments_endpoints_test.go b/payments_endpoints_test.go similarity index 50% rename from endpoints/payments/payments_endpoints_test.go rename to payments_endpoints_test.go index 683b4c7..6af1a57 100644 --- a/endpoints/payments/payments_endpoints_test.go +++ b/payments_endpoints_test.go @@ -1,30 +1,27 @@ -package payments_test +package nordigen_test import ( "context" - "github.com/stretchr/testify/assert" - "github.com/weportfolio/go-nordigen" - "github.com/weportfolio/go-nordigen/consts" "testing" + + "github.com/stretchr/testify/assert" ) -func TestClient_List(t *testing.T) { +func TestClient_ListPayments(t *testing.T) { t.Parallel() // Payments are not enabled for the account used in the tests t.Run("list payments", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - payments, err := client.Payments().List(context.Background(), token.Access, 10, 0) + payments, err := client.ListPayments(context.Background(), token.Access, 10, 0) assert.Error(t, err) assert.Nil(t, payments) }) diff --git a/premium.go b/premium.go new file mode 100644 index 0000000..d5ccf13 --- /dev/null +++ b/premium.go @@ -0,0 +1,73 @@ +package nordigen + +import "time" + +type PremiumTransactions struct { + Transactions PremiumTransactionList `json:"transactions"` +} + +type PremiumTransactionList struct { + Booked []PremiumTransaction `json:"booked"` + Pending []PremiumTransaction `json:"pending"` +} + +type PremiumTransaction struct { + TransactionID string `json:"transactionId"` + BookingDate string `json:"bookingDate"` + ValueDate string `json:"valueDate"` + BookingDateTime time.Time `json:"bookingDateTime"` + ValueDateTime time.Time `json:"valueDateTime"` + TransactionAmount Amount `json:"transactionAmount"` + CreditorName string `json:"creditorName"` + CreditorAccount Account `json:"creditorAccount"` + DebtorName string `json:"debtorName"` + DebtorAccount Account `json:"debtorAccount"` + BankTransactionCode string `json:"bankTransactionCode"` + RemittanceInformationUnstructured string `json:"remittanceInformationUnstructured"` + RemittanceInformationUnstructuredArray []string `json:"remittanceInformationUnstructuredArray"` + ProprietaryBankTransactionCode string `json:"proprietaryBankTransactionCode"` + InternalTransactionID string `json:"internalTransactionId"` + + AdditionalInformation string `json:"additionalInformation"` + AdditionalInformationStructured string `json:"additionalInformationStructured"` + BalanceAfterTransaction Balance `json:"balanceAfterTransaction"` + CheckID string `json:"checkId"` + CreditorID string `json:"creditorId"` + // CurrencyExchange []string `json:"currencyExchange"` + DebtorAgent string `json:"debtorAgent"` + EndToEndID string `json:"endToEndId"` + EntryReference string `json:"entryReference"` + MandateID string `json:"mandateId"` + MerchantCategoryCode string `json:"merchantCategoryCode"` + RemittanceInformationStructured string `json:"remittanceInformationStructured"` + RemittanceInformationStructuredArray []string `json:"remittanceInformationStructuredArray"` + UltimateCollector string `json:"ultimateCreditor"` + UltimateDebtor string `json:"ultimateDebtor"` + Enrichment Enrichment `json:"enrichment"` +} + +type Enrichment struct { + DisplayName string `json:"displayName"` + BranchDisplayName string `json:"branchDisplayName"` + Location Location `json:"location"` + URLs URLs `json:"urls"` + TransactionType string `json:"transactionType"` + PurposeCategory []string `json:"purposeCategory"` + PurposeCategoryID string `json:"purposeCategoryID"` +} + +type Location struct { + Address string `json:"address"` + City string `json:"city"` + Region string `json:"region"` + PostalCode string `json:"postalCode"` + Country string `json:"country"` + Latitude float64 `json:"lat"` + Longitude float64 `json:"lon"` +} + +type URLs struct { + Website string `json:"website"` + Favicon string `json:"favicon"` + Logo string `json:"logo"` +} diff --git a/premium_endpoints.go b/premium_endpoints.go new file mode 100644 index 0000000..5a6948a --- /dev/null +++ b/premium_endpoints.go @@ -0,0 +1,15 @@ +package nordigen + +import "context" + +// ListPremiumTransactions retrieves transactions for an account by ID +func (c Client) ListPremiumTransactions(ctx context.Context, token string, accountID string) (*PremiumTransactions, error) { + var transactions PremiumTransactions + endpointURL := AccountsTransactionPremiumPath + accountID + "/transactions" + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &transactions) + if err != nil { + return nil, err + } + + return &transactions, nil +} diff --git a/endpoints/requisitions/requisitions.go b/requisitions.go similarity index 96% rename from endpoints/requisitions/requisitions.go rename to requisitions.go index adb661d..a2d3e02 100644 --- a/endpoints/requisitions/requisitions.go +++ b/requisitions.go @@ -1,8 +1,8 @@ -package requisitions +package nordigen import "time" -type ListRequestParams struct { +type ListRequisitionsParams struct { Limit int `url:"limit,omitempty" json:"limit,omitempty"` Offset int `url:"offset,omitempty" json:"offset,omitempty"` } diff --git a/requisitions_endpoints.go b/requisitions_endpoints.go new file mode 100644 index 0000000..22920a3 --- /dev/null +++ b/requisitions_endpoints.go @@ -0,0 +1,60 @@ +package nordigen + +import ( + "context" + "strconv" +) + +// CreateRequisition creates a new requisition +func (c Client) CreateRequisition(ctx context.Context, token string, requisitionRequestBody *RequisitionRequestBody) (*Requisition, error) { + var requisition Requisition + err := c.HTTP.Post(ctx, RequisitionsPath, RequestHeadersWithAuth(token), requisitionRequestBody, &requisition) + if err != nil { + return nil, err + } + + return &requisition, nil +} + +// ListRequisition lists all requisitions +func (c Client) ListRequisitions(ctx context.Context, token string, requestParams *ListRequisitionsParams) (*Requisitions, error) { + var requisitions Requisitions + + endpointURL := RequisitionsPath + if requestParams != nil { + if requestParams.Limit != 0 { + endpointURL = endpointURL + "?" + strconv.Itoa(requestParams.Limit) + } + if requestParams.Offset != 0 { + endpointURL = endpointURL + "&" + strconv.Itoa(requestParams.Offset) + } + } + + err := c.HTTP.Get(ctx, endpointURL, RequestHeadersWithAuth(token), &requisitions) + if err != nil { + return nil, err + } + + return &requisitions, nil +} + +// FetchRequisition retrieves a requisition by requisitionID +func (c Client) FetchRequisition(ctx context.Context, token string, requisitionID string) (*Requisition, error) { + var requisition Requisition + err := c.HTTP.Get(ctx, RequisitionsPath+requisitionID, RequestHeadersWithAuth(token), &requisition) + if err != nil { + return nil, err + } + + return &requisition, nil +} + +// DeleteRequisition deletes a requisition by requisitionID +func (c Client) DeleteRequisition(ctx context.Context, token string, requisitionID string) error { + err := c.HTTP.Delete(ctx, RequisitionsPath+requisitionID, RequestHeadersWithAuth(token), nil) + if err != nil { + return err + } + + return nil +} diff --git a/requisitions_endpoints_test.go b/requisitions_endpoints_test.go new file mode 100644 index 0000000..c1a868b --- /dev/null +++ b/requisitions_endpoints_test.go @@ -0,0 +1,255 @@ +package nordigen_test + +import ( + "context" + "math/rand" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/weportfolio/go-nordigen" +) + +func TestClient_CreateRequisition(t *testing.T) { + t.Parallel() + + t.Run("create new requisition", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + requisitionRequestBody := &nordigen.RequisitionRequestBody{ + Redirect: "https://example.com", + InstitutionID: nordigen.TestInstitutionID, + Agreement: agreement.ID, + Reference: strconv.Itoa(rand.Intn(1000000000000000000)), + UserLanguage: nordigen.LangEN, + AccountSelection: false, + RedirectImmediate: false, + } + + requisition, err := client.CreateRequisition(context.Background(), token.Access, requisitionRequestBody) + assert.NoError(t, err) + assert.NotNil(t, requisition) + assert.Equal(t, nordigen.TestInstitutionID, requisition.InstitutionID) + }) + + t.Run("create new requisition with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + requisitionRequestBody := &nordigen.RequisitionRequestBody{ + Redirect: "https://example.com", + InstitutionID: nordigen.TestInstitutionID, + Agreement: "invalid", + Reference: "12345", + UserLanguage: nordigen.LangEN, + AccountSelection: false, + RedirectImmediate: false, + } + + requisition, err := client.CreateRequisition(context.Background(), "invalid", requisitionRequestBody) + assert.Error(t, err) + assert.Nil(t, requisition) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_ListRequisitions(t *testing.T) { + t.Parallel() + + t.Run("list requisitions", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + requisitionRequestBody := &nordigen.RequisitionRequestBody{ + Redirect: "https://example.com", + InstitutionID: nordigen.TestInstitutionID, + Agreement: agreement.ID, + Reference: strconv.Itoa(rand.Intn(1000000000000000000)), + UserLanguage: nordigen.LangEN, + AccountSelection: false, + RedirectImmediate: false, + } + + requisition, err := client.CreateRequisition(context.Background(), token.Access, requisitionRequestBody) + assert.NoError(t, err) + assert.NotNil(t, requisition) + assert.Equal(t, nordigen.TestInstitutionID, requisition.InstitutionID) + + responseRequisitions, err := client.ListRequisitions(context.Background(), token.Access, nil) + assert.NoError(t, err) + assert.NotNil(t, responseRequisitions) + }) + + t.Run("list requisitions with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + responseRequisitions, err := client.ListRequisitions(context.Background(), "invalid", nil) + assert.Error(t, err) + assert.Nil(t, responseRequisitions) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_FetchRequisition(t *testing.T) { + t.Parallel() + + t.Run("fetch requisition", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + requisitionRequestBody := &nordigen.RequisitionRequestBody{ + Redirect: "https://example.com", + InstitutionID: nordigen.TestInstitutionID, + Agreement: agreement.ID, + Reference: strconv.Itoa(rand.Intn(1000000000000000000)), + UserLanguage: nordigen.LangEN, + AccountSelection: false, + RedirectImmediate: false, + } + + requisition, err := client.CreateRequisition(context.Background(), token.Access, requisitionRequestBody) + assert.NoError(t, err) + assert.NotNil(t, requisition) + assert.Equal(t, nordigen.TestInstitutionID, requisition.InstitutionID) + + responseRequisition, err := client.FetchRequisition(context.Background(), token.Access, requisition.ID) + assert.NoError(t, err) + assert.NotNil(t, responseRequisition) + }) + + t.Run("fetch requisition with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + responseRequisition, err := client.FetchRequisition(context.Background(), "invalid", "invalid") + assert.Error(t, err) + assert.Nil(t, responseRequisition) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} + +func TestClient_DeleteRequisition(t *testing.T) { + t.Parallel() + + t.Run("delete requisition", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + token, err := client.NewToken(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, token) + + agreementRequestBody := nordigen.AgreementRequestBody{ + InstitutionID: nordigen.TestInstitutionID, + MaxHistoricalDays: "180", + AccessValidForDays: "2", + AccessScope: []string{"balances", "details", "transactions"}, + } + + agreement, err := client.CreateAgreement(context.Background(), token.Access, agreementRequestBody) + assert.NoError(t, err) + assert.NotNil(t, agreement) + assert.Equal(t, nordigen.TestInstitutionID, agreement.InstitutionID) + + requisitionRequestBody := &nordigen.RequisitionRequestBody{ + Redirect: "https://example.com", + InstitutionID: nordigen.TestInstitutionID, + Agreement: agreement.ID, + Reference: strconv.Itoa(rand.Intn(1000000000000000000)), + UserLanguage: nordigen.LangEN, + AccountSelection: false, + RedirectImmediate: false, + } + + requisition, err := client.CreateRequisition(context.Background(), token.Access, requisitionRequestBody) + assert.NoError(t, err) + assert.NotNil(t, requisition) + assert.Equal(t, nordigen.TestInstitutionID, requisition.InstitutionID) + + err = client.DeleteRequisition(context.Background(), token.Access, requisition.ID) + assert.NoError(t, err) + }) + + t.Run("delete requisition with invalid token", func(t *testing.T) { + t.Parallel() + + client := getTestClient(t) + assert.NotNil(t, client) + + err := client.DeleteRequisition(context.Background(), "invalid", "invalid") + assert.Error(t, err) + + checkErr := nordigen.ExtractError(err) + assert.Equal(t, 401, checkErr.StatusCode) + }) +} diff --git a/endpoints/token/token.go b/token.go similarity index 92% rename from endpoints/token/token.go rename to token.go index dde174b..bd84858 100644 --- a/endpoints/token/token.go +++ b/token.go @@ -1,4 +1,4 @@ -package token +package nordigen type Token struct { Access string `json:"access"` diff --git a/token_endpoints.go b/token_endpoints.go new file mode 100644 index 0000000..34d8184 --- /dev/null +++ b/token_endpoints.go @@ -0,0 +1,36 @@ +package nordigen + +import ( + "context" +) + +// NewToken creates a new token +func (c Client) NewToken(ctx context.Context) (*Token, error) { + var token Token + accessCreds := map[string]string{ + "secret_id": c.SecretID, + "secret_key": c.SecretKey, + } + + err := c.HTTP.Post(ctx, TokenNewPath, RequestHeaders(), accessCreds, &token) + if err != nil { + return nil, err + } + + return &token, nil +} + +// RefreshToken refreshes a token +func (c Client) RefreshToken(ctx context.Context, refreshToken string) (*Token, error) { + var token Token + refreshCreds := map[string]string{ + "refresh": refreshToken, + } + + err := c.HTTP.Post(ctx, TokenRefreshPath, RequestHeaders(), refreshCreds, &token) + if err != nil { + return nil, err + } + + return &token, nil +} diff --git a/endpoints/token/token_endpoints_test.go b/token_endpoints_test.go similarity index 57% rename from endpoints/token/token_endpoints_test.go rename to token_endpoints_test.go index 807ba92..89d1fbd 100644 --- a/endpoints/token/token_endpoints_test.go +++ b/token_endpoints_test.go @@ -1,28 +1,24 @@ -package token_test +package nordigen_test import ( "context" "net/http" "testing" - "github.com/weportfolio/go-nordigen/consts" - "github.com/stretchr/testify/assert" "github.com/weportfolio/go-nordigen" ) -func TestClient_New(t *testing.T) { +func TestClient_NewToken(t *testing.T) { t.Parallel() t.Run("create a new client token", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) }) @@ -30,17 +26,14 @@ func TestClient_New(t *testing.T) { t.Run("create a new client token with invalid secret id", func(t *testing.T) { t.Parallel() - client := nordigen.New( - "invalid", - "invalid", - ) - assert.NotNil(t, client) + invalidClient := getInvalidTestClient(t) + assert.NotNil(t, invalidClient) - token, err := client.Token().New(context.Background()) + token, err := invalidClient.NewToken(context.Background()) assert.Error(t, err) assert.Nil(t, token) - checkErr := consts.ExtractError(err) + checkErr := nordigen.ExtractError(err) assert.Equal(t, http.StatusUnauthorized, checkErr.StatusCode) }) } @@ -51,16 +44,14 @@ func TestClient_Refresh(t *testing.T) { t.Run("refresh a client token", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - token, err := client.Token().New(context.Background()) + token, err := client.NewToken(context.Background()) assert.NoError(t, err) assert.NotNil(t, token) - refreshedToken, err := client.Token().Refresh(context.Background(), token.Refresh) + refreshedToken, err := client.RefreshToken(context.Background(), token.Refresh) assert.NoError(t, err) assert.NotNil(t, refreshedToken) }) @@ -68,12 +59,10 @@ func TestClient_Refresh(t *testing.T) { t.Run("refresh a client token with invalid refresh token", func(t *testing.T) { t.Parallel() - client := nordigen.New( - consts.GetSecrets(t), - ) + client := getTestClient(t) assert.NotNil(t, client) - refreshedToken, err := client.Token().Refresh(context.Background(), "invalid") + refreshedToken, err := client.RefreshToken(context.Background(), "invalid") assert.Error(t, err) assert.Nil(t, refreshedToken) })