From 9382a5bd01e8488f3e4a20c2d2663265ac1bbd77 Mon Sep 17 00:00:00 2001 From: Martin Seco Date: Fri, 25 Nov 2022 12:12:52 -0300 Subject: [PATCH] Release 1.0.0.beta.1 This commit represents an effort made by the Checkout team to build a brand new SDK for GO. It ships a new instantiation layer that makes easier a less confusing to set up the SDK. From the point of view of usability, the new version follows similar principles, although major interfaces are now split/duplicated to reference different data schemes which makes it fully compatible with `default` (NAS) and `Previous` (ABC) account structures. This separation is statically forced during instantiation. The SDK supports the following modules: * Tokens (default/previous) * Instruments (default/previous) * Customers (default/previous) * Disputes (default/previous) * Payments (default/previous) * Alternative Payments (default/previous) * Forex (default) * Sessions (default) * Accounts (default) * Events (previous) * Sources (previous) `README.md` was updated to match the new setup. Code of conduct and license were reviewed. --- .github/workflows/build-master.yml | 36 + .github/workflows/build-pull-request.yml | 35 + .github/workflows/build-release.yml | 48 + .gitignore | 19 +- .gitmessage | 10 - CHANGELOG.md | 2 - README.md | 270 +++--- VERSION | 1 - abc/checkout_api.go | 52 ++ abc/checkout_previous_sdk_builder.go | 48 + accounts/accounts.go | 54 ++ accounts/client.go | 175 ++++ accounts/client_test.go | 594 ++++++++++++ accounts/common.go | 142 +++ accounts/document_types.go | 12 + accounts/entities.go | 36 + accounts/files.go | 20 + accounts/instruments.go | 37 + accounts/onboarding_status.go | 12 + accounts/payouts.go | 170 ++++ apm/ideal/client.go | 49 + apm/ideal/client_test.go | 84 ++ apm/ideal/ideal.go | 45 + apm/klarna/client.go | 110 +++ apm/klarna/client_test.go | 415 +++++++++ apm/klarna/klarna.go | 80 ++ apm/sepa/client.go | 91 ++ apm/sepa/client_test.go | 170 ++++ apm/sepa/sepa.go | 35 + checkout.go | 373 +------- checkout_sdk_builder.go | 20 + checkout_test.go | 105 --- client/api.go | 74 -- client/api_test.go | 27 - client/client.go | 191 ++++ client/version.go | 3 + common/address.go | 11 - common/cardcategory.go | 11 - common/cardtype.go | 15 - common/common.go | 228 +++++ common/country.go | 259 ++++++ common/currency.go | 640 ++++--------- common/errorresponse.go | 19 - common/files.go | 131 +++ common/link.go | 7 - common/paymentaction.go | 23 - common/paymenttype.go | 17 - common/phone.go | 7 - common/regex.go | 18 - common/serializer.go | 43 + common/tokentype.go | 19 - common/utils.go | 38 + common/webhookcontenttype.go | 15 - configuration/configuration.go | 29 + configuration/default_keys_credentials.go | 31 + configuration/environment.go | 79 ++ configuration/key_patterns.go | 8 + configuration/oauth_keys_credentials.go | 124 +++ configuration/oauth_scopes.go | 43 + configuration/previous_keys_credentials.go | 31 + configuration/sdk_builder.go | 12 + configuration/sdk_credentials.go | 50 + configuration/static_keys_builder.go | 34 + customers/client.go | 83 +- customers/client_test.go | 478 ++++++++++ customers/customer.go | 25 - customers/customers.go | 28 + disputes/client.go | 204 ++-- disputes/client_test.go | 666 +++++++++++++ disputes/dispute.go | 120 --- disputes/disputes.go | 161 ++++ disputes/disputes_categories.go | 15 + disputes/disputes_relevant_evidence.go | 13 + disputes/disputes_resolved_reasons.go | 9 + disputes/disputes_status.go | 17 + errors/error.go | 47 + errors/error_handler.go | 33 + errors/error_handler_test.go | 81 ++ events/client.go | 134 +-- events/client_test.go | 128 +++ events/event.go | 82 -- events/events.go | 29 + files/client.go | 68 -- files/file.go | 105 --- forex/client.go | 34 + forex/client_test.go | 118 +++ forex/forex.go | 34 + go.mod | 3 +- go.sum | 18 +- httpclient/client.go | 409 -------- instruments/abc/client.go | 91 ++ instruments/abc/client_test.go | 487 ++++++++++ instruments/abc/create.go | 42 + instruments/abc/get.go | 29 + instruments/abc/instruments.go | 10 + instruments/abc/update.go | 29 + instruments/client.go | 80 -- instruments/instrument.go | 53 -- instruments/instruments.go | 21 + instruments/nas/client.go | 91 ++ instruments/nas/client_test.go | 565 +++++++++++ instruments/nas/create.go | 122 +++ instruments/nas/get.go | 87 ++ instruments/nas/instuments.go | 13 + instruments/nas/update.go | 114 +++ log.go | 104 --- log_test.go | 117 --- mocks/client_mock.go | 72 ++ mocks/credentials_mock.go | 21 + mocks/environment_mock.go | 31 + nas/checkout_api.go | 59 ++ nas/checkout_default_sdk_builder.go | 48 + nas/checkout_oauth_sdk_builder.go | 65 ++ nas/default_sdk_builder.go | 5 + params.go | 42 - payments/abc/client.go | 171 ++++ payments/abc/client_test.go | 981 ++++++++++++++++++++ payments/abc/destinations.go | 96 ++ payments/abc/payments.go | 159 ++++ payments/abc/source.go | 65 ++ payments/abc/sources/apm/apm.go | 286 ++++++ payments/abc/sources/sources.go | 91 ++ payments/actions.go | 67 -- payments/authorizations.go | 52 -- payments/authorizations_test.go | 78 -- payments/captures.go | 33 - payments/captures_test.go | 71 -- payments/client.go | 131 --- payments/nas/client.go | 171 ++++ payments/nas/client_test.go | 972 +++++++++++++++++++ payments/nas/destinations.go | 85 ++ payments/nas/payments.go | 257 +++++ payments/nas/senders.go | 105 +++ payments/nas/source.go | 76 ++ payments/nas/sources/apm/apm.go | 284 ++++++ payments/nas/sources/sources.go | 112 +++ payments/payer.go | 9 + payments/payment.go | 691 -------------- payments/payments.go | 445 +++++++++ payments/refunds.go | 16 - payments/threeDS.go | 63 -- payments/voids.go | 15 - reconciliation/client.go | 207 ----- reconciliation/reconciliation.go | 153 --- sessions/channels/channels.go | 89 ++ sessions/client.go | 137 +++ sessions/client_test.go | 604 ++++++++++++ sessions/completion/completion.go | 46 + sessions/session_secret_credentials.go | 29 + sessions/session_secret_credentials_test.go | 60 ++ sessions/sessions.go | 83 ++ sessions/sessions_requests.go | 90 ++ sessions/sessions_responses.go | 182 ++++ sessions/sources/session_sources.go | 108 +++ sources/client.go | 48 +- sources/client_test.go | 153 +++ sources/source.go | 83 -- sources/sources.go | 63 ++ test/accounts_test.go | 465 ++++++++++ test/apm_ideal_test.go | 66 ++ test/apm_klarna_test.go | 136 +++ test/checkout.jpeg | Bin 0 -> 4752 bytes test/checkout.pdf | Bin 0 -> 465986 bytes test/configuration_test.go | 75 ++ test/customers_previous_test.go | 189 ++++ test/customers_test.go | 189 ++++ test/disputes_previous_test.go | 491 ++++++++++ test/disputes_test.go | 491 ++++++++++ test/events_test.go | 18 + test/forex_test.go | 58 ++ test/instruments_previous_test.go | 116 +++ test/instruments_test.go | 126 +++ test/oauth_sdk_test.go | 127 +++ test/payments_actions_previous_test.go | 32 + test/payments_actions_test.go | 32 + test/payments_captures_previous_test.go | 63 ++ test/payments_captures_test.go | 76 ++ test/payments_payouts_previous_test.go | 51 + test/payments_refunds_previous_test.go | 56 ++ test/payments_refunds_test.go | 67 ++ test/payments_request_apm_previous_test.go | 915 ++++++++++++++++++ test/payments_request_apm_test.go | 574 ++++++++++++ test/payments_request_previous_test.go | 422 +++++++++ test/payments_request_test.go | 486 ++++++++++ test/payments_voids_previous_test.go | 49 + test/payments_voids_test.go | 62 ++ test/sandbox_test_fixture_test.go | 121 +++ test/sessions_test.go | 438 +++++++++ test/sources_test.go | 28 + test/tokens_previous_test.go | 42 + test/tokens_test.go | 39 + tokens/client.go | 67 +- tokens/client_test.go | 148 +++ tokens/token.go | 98 -- tokens/tokens.go | 73 ++ travis.yml | 40 - webhooks/client.go | 136 --- webhooks/webhook.go | 46 - 198 files changed, 20702 insertions(+), 4850 deletions(-) create mode 100644 .github/workflows/build-master.yml create mode 100644 .github/workflows/build-pull-request.yml create mode 100644 .github/workflows/build-release.yml delete mode 100644 .gitmessage delete mode 100644 CHANGELOG.md delete mode 100644 VERSION create mode 100644 abc/checkout_api.go create mode 100644 abc/checkout_previous_sdk_builder.go create mode 100644 accounts/accounts.go create mode 100644 accounts/client.go create mode 100644 accounts/client_test.go create mode 100644 accounts/common.go create mode 100644 accounts/document_types.go create mode 100644 accounts/entities.go create mode 100644 accounts/files.go create mode 100644 accounts/instruments.go create mode 100644 accounts/onboarding_status.go create mode 100644 accounts/payouts.go create mode 100644 apm/ideal/client.go create mode 100644 apm/ideal/client_test.go create mode 100644 apm/ideal/ideal.go create mode 100644 apm/klarna/client.go create mode 100644 apm/klarna/client_test.go create mode 100644 apm/klarna/klarna.go create mode 100644 apm/sepa/client.go create mode 100644 apm/sepa/client_test.go create mode 100644 apm/sepa/sepa.go create mode 100644 checkout_sdk_builder.go delete mode 100644 checkout_test.go delete mode 100644 client/api.go delete mode 100644 client/api_test.go create mode 100644 client/client.go create mode 100644 client/version.go delete mode 100644 common/address.go delete mode 100644 common/cardcategory.go delete mode 100644 common/cardtype.go create mode 100644 common/common.go create mode 100644 common/country.go delete mode 100644 common/errorresponse.go create mode 100644 common/files.go delete mode 100644 common/link.go delete mode 100644 common/paymentaction.go delete mode 100644 common/paymenttype.go delete mode 100644 common/phone.go delete mode 100644 common/regex.go create mode 100644 common/serializer.go delete mode 100644 common/tokentype.go create mode 100644 common/utils.go delete mode 100644 common/webhookcontenttype.go create mode 100644 configuration/configuration.go create mode 100644 configuration/default_keys_credentials.go create mode 100644 configuration/environment.go create mode 100644 configuration/key_patterns.go create mode 100644 configuration/oauth_keys_credentials.go create mode 100644 configuration/oauth_scopes.go create mode 100644 configuration/previous_keys_credentials.go create mode 100644 configuration/sdk_builder.go create mode 100644 configuration/sdk_credentials.go create mode 100644 configuration/static_keys_builder.go create mode 100644 customers/client_test.go delete mode 100644 customers/customer.go create mode 100644 customers/customers.go create mode 100644 disputes/client_test.go delete mode 100644 disputes/dispute.go create mode 100644 disputes/disputes.go create mode 100644 disputes/disputes_categories.go create mode 100644 disputes/disputes_relevant_evidence.go create mode 100644 disputes/disputes_resolved_reasons.go create mode 100644 disputes/disputes_status.go create mode 100644 errors/error.go create mode 100644 errors/error_handler.go create mode 100644 errors/error_handler_test.go create mode 100644 events/client_test.go delete mode 100644 events/event.go create mode 100644 events/events.go delete mode 100644 files/client.go delete mode 100644 files/file.go create mode 100644 forex/client.go create mode 100644 forex/client_test.go create mode 100644 forex/forex.go delete mode 100644 httpclient/client.go create mode 100644 instruments/abc/client.go create mode 100644 instruments/abc/client_test.go create mode 100644 instruments/abc/create.go create mode 100644 instruments/abc/get.go create mode 100644 instruments/abc/instruments.go create mode 100644 instruments/abc/update.go delete mode 100644 instruments/client.go delete mode 100644 instruments/instrument.go create mode 100644 instruments/instruments.go create mode 100644 instruments/nas/client.go create mode 100644 instruments/nas/client_test.go create mode 100644 instruments/nas/create.go create mode 100644 instruments/nas/get.go create mode 100644 instruments/nas/instuments.go create mode 100644 instruments/nas/update.go delete mode 100644 log.go delete mode 100644 log_test.go create mode 100644 mocks/client_mock.go create mode 100644 mocks/credentials_mock.go create mode 100644 mocks/environment_mock.go create mode 100644 nas/checkout_api.go create mode 100644 nas/checkout_default_sdk_builder.go create mode 100644 nas/checkout_oauth_sdk_builder.go create mode 100644 nas/default_sdk_builder.go delete mode 100644 params.go create mode 100644 payments/abc/client.go create mode 100644 payments/abc/client_test.go create mode 100644 payments/abc/destinations.go create mode 100644 payments/abc/payments.go create mode 100644 payments/abc/source.go create mode 100644 payments/abc/sources/apm/apm.go create mode 100644 payments/abc/sources/sources.go delete mode 100644 payments/actions.go delete mode 100644 payments/authorizations.go delete mode 100644 payments/authorizations_test.go delete mode 100644 payments/captures.go delete mode 100644 payments/captures_test.go delete mode 100644 payments/client.go create mode 100644 payments/nas/client.go create mode 100644 payments/nas/client_test.go create mode 100644 payments/nas/destinations.go create mode 100644 payments/nas/payments.go create mode 100644 payments/nas/senders.go create mode 100644 payments/nas/source.go create mode 100644 payments/nas/sources/apm/apm.go create mode 100644 payments/nas/sources/sources.go create mode 100644 payments/payer.go delete mode 100644 payments/payment.go create mode 100644 payments/payments.go delete mode 100644 payments/refunds.go delete mode 100644 payments/threeDS.go delete mode 100644 payments/voids.go delete mode 100644 reconciliation/client.go delete mode 100644 reconciliation/reconciliation.go create mode 100644 sessions/channels/channels.go create mode 100644 sessions/client.go create mode 100644 sessions/client_test.go create mode 100644 sessions/completion/completion.go create mode 100644 sessions/session_secret_credentials.go create mode 100644 sessions/session_secret_credentials_test.go create mode 100644 sessions/sessions.go create mode 100644 sessions/sessions_requests.go create mode 100644 sessions/sessions_responses.go create mode 100644 sessions/sources/session_sources.go create mode 100644 sources/client_test.go delete mode 100644 sources/source.go create mode 100644 sources/sources.go create mode 100644 test/accounts_test.go create mode 100644 test/apm_ideal_test.go create mode 100644 test/apm_klarna_test.go create mode 100644 test/checkout.jpeg create mode 100644 test/checkout.pdf create mode 100644 test/configuration_test.go create mode 100644 test/customers_previous_test.go create mode 100644 test/customers_test.go create mode 100644 test/disputes_previous_test.go create mode 100644 test/disputes_test.go create mode 100644 test/events_test.go create mode 100644 test/forex_test.go create mode 100644 test/instruments_previous_test.go create mode 100644 test/instruments_test.go create mode 100644 test/oauth_sdk_test.go create mode 100644 test/payments_actions_previous_test.go create mode 100644 test/payments_actions_test.go create mode 100644 test/payments_captures_previous_test.go create mode 100644 test/payments_captures_test.go create mode 100644 test/payments_payouts_previous_test.go create mode 100644 test/payments_refunds_previous_test.go create mode 100644 test/payments_refunds_test.go create mode 100644 test/payments_request_apm_previous_test.go create mode 100644 test/payments_request_apm_test.go create mode 100644 test/payments_request_previous_test.go create mode 100644 test/payments_request_test.go create mode 100644 test/payments_voids_previous_test.go create mode 100644 test/payments_voids_test.go create mode 100644 test/sandbox_test_fixture_test.go create mode 100644 test/sessions_test.go create mode 100644 test/sources_test.go create mode 100644 test/tokens_previous_test.go create mode 100644 test/tokens_test.go create mode 100644 tokens/client_test.go delete mode 100644 tokens/token.go create mode 100644 tokens/tokens.go delete mode 100644 travis.yml delete mode 100644 webhooks/client.go delete mode 100644 webhooks/webhook.go diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml new file mode 100644 index 0000000..ad92961 --- /dev/null +++ b/.github/workflows/build-master.yml @@ -0,0 +1,36 @@ +name: build-master +on: + push: + branches: + - beta +jobs: + build: + if: "!contains(github.event.commits[0].message, 'Release')" + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + fail-fast: false + matrix: + go: + - "1.14" + - "1.16" + - "1.18" + steps: + - uses: actions/checkout@v2 + - id: setup-go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + - id: build-and-test + env: + CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} + CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} + CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET }} + run: + go build && go test -v ./... + diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml new file mode 100644 index 0000000..440fe5c --- /dev/null +++ b/.github/workflows/build-pull-request.yml @@ -0,0 +1,35 @@ +name: build-pull-request +on: + pull_request: + branches: + - beta + - "feature/**" +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + fail-fast: false + matrix: + go: + - "1.14" + - "1.16" + - "1.18" + steps: + - uses: actions/checkout@v2 + - id: setup-go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + - id: build-and-test + env: + CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} + CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} + CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET }} + run: + go build && go test -v ./... diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..4c5d957 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,48 @@ +name: build-release +on: + push: + branches: + - beta + paths: + - client/version.go +jobs: + build: + if: github.ref == 'refs/heads/beta' + runs-on: ubuntu-latest + strategy: + matrix: + go: + - "1.14" + steps: + - uses: actions/checkout@v2 + - id: setup-go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + - id: build-and-test + env: + CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} + CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} + CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID }} + CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET }} + run: + go build && go test -v ./... + - id: read-version + run: echo "CURRENT_VERSION=v$( grep "VERSION" client/version.go | awk '{ print $4 }' | tr -d "\"")" >> $GITHUB_ENV + - id: print-version + run: echo "Releasing $CURRENT_VERSION" + - id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.CURRENT_VERSION }} + release_name: ${{ env.CURRENT_VERSION }} + body: ${{ github.event.head_commit.message }} + draft: false + prerelease: false + diff --git a/.gitignore b/.gitignore index 2e7a002..655ad28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,18 @@ -.DS_Store -.env +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` *.test -*.coverprofile + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + .idea +*.DS_Store* \ No newline at end of file diff --git a/.gitmessage b/.gitmessage deleted file mode 100644 index 5d3eced..0000000 --- a/.gitmessage +++ /dev/null @@ -1,10 +0,0 @@ -Title: -# No more than 50 chars. #### 50 chars is here: # - -Body: -# Wrap at 72 chars. ################################## which is here: # - - -# At the end: Include Co-authored-by for all contributors. -# Include at least one empty line before it. Format: -Co-authored-by: ShiuhYaw \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b8aa8bb..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,2 +0,0 @@ -## 0.0.17 - 2020-08-06 -* Update github setup \ No newline at end of file diff --git a/README.md b/README.md index 8666157..facd598 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,193 @@ -# Checkout.com Go SDK +# Checkout.com Golang SDK -![Status](https://img.shields.io/badge/status-beta-red.svg) - -The official [Checkout][checkout] Go client library. +![build-status](https://github.com/checkout/checkout-sdk-go/workflows/build-master/badge.svg) +[![Go Reference](https://pkg.go.dev/badge/github.com/checkout/checkout-sdk-go.svg)](https://pkg.go.dev/github.com/checkout/checkout-sdk-go) +[![GitHub license](https://img.shields.io/github/license/checkout/checkout-sdk-go.svg)](https://github.com/checkout/checkout-sdk-go/blob/master/LICENSE) +[![GitHub release](https://img.shields.io/github/release/checkout/checkout-sdk-go.svg)](https://github.com/checkout/checkout-sdk-go/releases/) ## Getting started -Make sure your project is using Go Modules (it will have a `go.mod` file in its root if it already is): +> **Version 1.0.0 is here!** +>

+> We improved the initialization of SDK making it easier to understand the available options.
+> Now `NAS` accounts are the default instance for the SDK and `ABC` structure was moved to a `previous` prefixes.
+### Module installer +Make sure your project is using Go Modules: ```sh -go mod init +go get github.com/checkout-sdk-go@{version} ``` - -```go -import ( - "github.com/checkout/checkout-sdk-go" -) +Then import the library into your code: +```sh +import "github.com/checkout-sdk-go" ``` -Run any of the normal `go` commands (`build`/`install`/`test`). The Go toolchain will resolve and fetch the -checkout-sdk-go module automatically. +### :rocket: Please check in [GitHub releases](https://github.com/checkout/checkout-sdk-go/releases) for all the versions available. + +### :book: Checkout our official documentation. -## API Keys +* [Official Docs (Default)](https://docs.checkout.com/) +* [Official Docs (Previous)](https://docs.checkout.com/previous) + +### :books: Check out our official API documentation guide, where you can also find more usage examples. + +* [API Reference (Default)](https://api-reference.checkout.com/) +* [API Reference (Previous)](https://api-reference.checkout.com/previous) + +## How to use the SDK This SDK can be used with two different pair of API keys provided by Checkout. However, using different API keys imply -using specific API features. Please find in the table below the types of keys that can be used within this SDK. +using specific API features.
+Please find in the table below the types of keys that can be used within this SDK. | Account System | Public Key (example) | Secret Key (example) | |----------------|-----------------------------------------|-----------------------------------------| -| default | pk_g650ff27-7c42-4ce1-ae90-5691a188ee7b | sk_gk3517a8-3z01-45fq-b4bd-4282384b0a64 | -| Four | pk_pkhpdtvabcf7hdgpwnbhw7r2uic | sk_m73dzypy7cf3gf5d2xr4k7sxo4e | +| Default | pk_pkhpdtvabcf7hdgpwnbhw7r2uic | sk_m73dzypy7cf3gf5d2xr4k7sxo4e | +| Previous | pk_g650ff27-7c42-4ce1-ae90-5691a188ee7b | sk_gk3517a8-3z01-45fq-b4bd-4282384b0a64 | -Note: sandbox keys have a `test_` or `sbox_` identifier, for Default and Four accounts respectively. +Note: sandbox keys have a `sbox_` or `test_` identifier, for Default and Previous accounts respectively. If you don't have your own API keys, you can sign up for a test account [here](https://www.checkout.com/get-test-account). -### OAuth +**PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.** -The SDK doesn't support any OAuth authentication flow natively, however it supports OAuth authorization tokens that can be used as API keys. For more information about OAuth please refer -to the official documentation. - -## How to use the SDK +### Default -The SDK is structured by different modules and each module gives you access to different business features. All these modules can -be instantiated at once, or you can choose to create single modules separately. +Default keys client instantiation can be done as follows: ```go import ( "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/configuration" ) -api := client.CheckoutApi(&secretKey, &publicKey, Sandbox) // or Production -var tokensClient = api.Tokens +api, err := checkout.Builder(). + StaticKeys(). + WithEnvironment(configuration.Sandbox()). + WithSecretKey("secret_key"). + WithPublicKey("public_key"). // optional, only required for operations related with tokens + Build() ``` -```go -import ( -"github.com/checkout/checkout-sdk-go" -"github.com/checkout/checkout-sdk-go/client" -) - -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var tokensClient = tokens.NewClient(*config) -``` +### Default OAuth -### Tokens +The SDK supports client credentials OAuth, when initialized as follows: ```go import ( "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/tokens" + "github.com/checkout/checkout-sdk-go/configuration" ) -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = tokens.NewClient(*config) // or api.Tokens -var card = &client.Card{ - Type: common.Card, - Number: "4242424242424242", - ExpiryMonth: 2, - ExpiryYear: 2022, - Name: "Customer Name", - CVV: "100", -} -var request = &tokens.Request{ - Card: card, -} -response, err := client.Request(request) +api, err := checkout.Builder(). + OAuth(). + WithAuthorizationUri("https://access.sandbox.checkout.com/connect/token"). // optional, custom authorization URI + WithClientCredentials("client_id", "client_secret"). + WithEnvironment(configuration.Sandbox()). + WithScopes(getOAuthScopes()). + Build() ``` -### Payments +### Previous + +If your pair of keys matches the previous system type, this is how the SDK should be used: ```go import ( "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/payments" + "github.com/checkout/checkout-sdk-go/configuration" ) -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments - -var source = payments.TokenSource{ - Type: common.Token.String(), - Token: "tok_", -} -var request = &payments.Request{ - Source: source, - Amount: "100", - Currency: "USD", - Reference: "Payment Reference", - Customer: &payments.Customer{ - Email: "example@email.com", - Name: "First Name Last Name", - }, - Metadata: map[string]string{ - "udf1": "User Define", - }, -} - -idempotencyKey := checkout.NewIdempotencyKey() -params := checkout.Params{ - IdempotencyKey: &idempotencyKey, -} - -response, err := client.Request(request, ¶ms) +api, err := checkout.Builder(). + Previous(). + WithEnvironment(configuration.Sandbox()). + WithSecretKey("secret_key"). + WithPublicKey("public_key"). // optional, only required for operations related with tokens + Build() ``` -### Payment Detail +Then just get any client, and start making requests: ```go import ( - "github.com/checkout/checkout-sdk-go" "github.com/checkout/checkout-sdk-go/payments" + "github.com/checkout/checkout-sdk-go/payments/nas" ) -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments - -response, err := client.Get("pay_") +request := nas.PaymentRequest{} +response, err := api.Payments.RequestPayment(request) ``` -### Actions - -```go -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/payments" -) - -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments +## Error Handling -response, err := client.Actions("pay_") -``` +All the API responses that do not fall in the 2** status codes will return a `errors.CheckoutApiError`. The +error encapsulates the `StatusCode`, `Status` and a the `ErrorDetails`, if available. -### Captures +## Custom Http Client +Go SDK supports your own configuration for `http client` using `http.Client` from the standard library. You can pass it through when instantiating the SDK as follows: ```go import ( + "net/http" + "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/payments" + "github.com/checkout/checkout-sdk-go/configuration" ) -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments - -idempotencyKey := checkout.NewIdempotencyKey() -params := checkout.Params{ - IdempotencyKey: &idempotencyKey, +httpClient := http.Client{ + Timeout: time.Duration(20) * time.Millisecond, } -request := &client.CapturesRequest{ - Amount: 100, - Reference: "Reference", - Metadata: map[string]string{ - "udf1": "User Define", - }, -} -response, err := client.Captures("pay_", request, ¶ms) +api, err := checkout.Builder(). + StaticKeys(). + WithEnvironment(configuration.Sandbox()). + WithHttpClient(&httpClient). + WithSecretKey("secret_key")). + WithPublicKey("public_key")). // optional, only required for operations related with tokens + Build() ``` -### Voids +## Custom Environment +In case that you want to use an integrator or mock server, you can specify your own URI configuration as follows: ```go import ( "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/payments" + "github.com/checkout/checkout-sdk-go/configuration" ) -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments - -idempotencyKey := checkout.NewIdempotencyKey() -params := checkout.Params{ - IdempotencyKey: &idempotencyKey, -} +environment := configuration.NewEnvironment( + "https://the.base.uri/", // the uri for all CKO operations + "https://the.oauth.uri/connect/token", // the uri used for OAUTH authorization, only required for OAuth operations + "https://the.files.uri/", // the uri used for Files operations, only required for Accounts module + "https://the.transfers.uri/", // the uri used for Transfer operations, only required for Transfers module + "https://the.balances.uri/", // the uri used for Balances operations, only required for Balances module false +) -request := &client.VoidsRequest{ - Reference: "Reference", - Metadata: map[string]string{ - "udf1": "User Define", - }, -} -response, err := client.Voids("pay_", request, ¶ms) +api, err := checkout.Builder(). + StaticKeys(). + WithEnvironment(environment). + WithSecretKey("secret_key")). + WithPublicKey("public_key")). // optional, only required for operations related with tokens + Build() ``` -### Refunds - -```go -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/payments" -) +## Building from source -config, err := checkout.SdkConfig(&secretKey, &publicKey, Sandbox) // or Production -var client = payments.NewClient(*config) // or api.Payments +Once you check out the code from GitHub, the project can be built using: -request := &payments.RefundsRequest{ - Amount: 100, - Reference: "Reference", - Metadata: map[string]string{ - "udf1": "User Define", - }, -} - -idempotencyKey := checkout.NewIdempotencyKey() -params := checkout.Params{ - IdempotencyKey: &idempotencyKey, -} +```sh +go mod tidy -response, err := client.Refunds("pay_", request, ¶ms) +go build ``` -More documentation related to Checkout API and the SDK is available at: - -* [API Reference (Default)](https://api-reference.checkout.com/) -* [API Reference (Four)](https://api-reference.checkout.com/preview/crusoe/) -* [Official Docs (Default)](https://docs.checkout.com/) -* [Official Docs (Four)](https://docs.checkout.com/four) - The execution of integration tests require the following environment variables set in your system: -* For Default account systems: `CHECKOUT_PUBLIC_KEY` & `CHECKOUT_SECRET_KEY` -* For Four account systems: `CHECKOUT_FOUR_PUBLIC_KEY` & `CHECKOUT_FOUR_SECRET_KEY` +* For default account systems (NAS): `CHECKOUT_DEFAULT_PUBLIC_KEY` & `CHECKOUT_DEFAULT_SECRET_KEY` +* For default account systems (OAuth): `CHECKOUT_DEFAULT_OAUTH_CLIENT_ID` & `CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET` +* For Previous account systems (ABC): `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY` ## Code of Conduct diff --git a/VERSION b/VERSION deleted file mode 100644 index 927734f..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.17 \ No newline at end of file diff --git a/abc/checkout_api.go b/abc/checkout_api.go new file mode 100644 index 0000000..a260907 --- /dev/null +++ b/abc/checkout_api.go @@ -0,0 +1,52 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/apm/ideal" + "github.com/checkout/checkout-sdk-go/apm/klarna" + "github.com/checkout/checkout-sdk-go/apm/sepa" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/customers" + "github.com/checkout/checkout-sdk-go/disputes" + "github.com/checkout/checkout-sdk-go/events" + "github.com/checkout/checkout-sdk-go/instruments/abc" + payments "github.com/checkout/checkout-sdk-go/payments/abc" + "github.com/checkout/checkout-sdk-go/sources" + "github.com/checkout/checkout-sdk-go/tokens" +) + +type Api struct { + Tokens *tokens.Client + Events *events.Client + Sources *sources.Client + Instruments *abc.Client + Customers *customers.Client + Payments *payments.Client + Disputes *disputes.Client + + Ideal *ideal.Client + Klarna *klarna.Client + Sepa *sepa.Client +} + +func CheckoutApi(configuration *configuration.Configuration) *Api { + apiClient := buildBaseClient(configuration) + + api := Api{} + api.Tokens = tokens.NewClient(configuration, apiClient) + api.Events = events.NewClient(configuration, apiClient) + api.Sources = sources.NewClient(configuration, apiClient) + api.Instruments = abc.NewClient(configuration, apiClient) + api.Customers = customers.NewClient(configuration, apiClient) + api.Payments = payments.NewClient(configuration, apiClient) + api.Disputes = disputes.NewClient(configuration, apiClient) + + api.Ideal = ideal.NewClient(configuration, apiClient) + api.Klarna = klarna.NewClient(configuration, apiClient) + api.Sepa = sepa.NewClient(configuration, apiClient) + return &api +} + +func buildBaseClient(configuration *configuration.Configuration) client.HttpClient { + return client.NewApiClient(configuration, configuration.Environment.BaseUri()) +} diff --git a/abc/checkout_previous_sdk_builder.go b/abc/checkout_previous_sdk_builder.go new file mode 100644 index 0000000..221ffdb --- /dev/null +++ b/abc/checkout_previous_sdk_builder.go @@ -0,0 +1,48 @@ +package abc + +import ( + "net/http" + + "github.com/checkout/checkout-sdk-go/configuration" +) + +type CheckoutPreviousSdkBuilder struct { + configuration.StaticKeysBuilder +} + +func (b *CheckoutPreviousSdkBuilder) WithEnvironment(environment configuration.Environment) *CheckoutPreviousSdkBuilder { + b.Environment = environment + return b +} + +func (b *CheckoutPreviousSdkBuilder) WithHttpClient(client *http.Client) *CheckoutPreviousSdkBuilder { + b.HttpClient = client + return b +} + +func (b *CheckoutPreviousSdkBuilder) WithPublicKey(publicKey string) *CheckoutPreviousSdkBuilder { + b.PublicKey = publicKey + return b +} + +func (b *CheckoutPreviousSdkBuilder) WithSecretKey(secretKey string) *CheckoutPreviousSdkBuilder { + b.SecretKey = secretKey + return b +} + +func (b *CheckoutPreviousSdkBuilder) Build() (*Api, error) { + err := b.ValidateSecretKey(configuration.PreviousSecretKeyPattern) + if err != nil { + return nil, err + } + + err = b.ValidatePublicKey(configuration.PreviousPublicKeyPattern) + if err != nil { + return nil, err + } + + sdkCredentials := configuration.NewPreviousKeysSdkCredentials(b.SecretKey, b.PublicKey) + newConfiguration := configuration.NewConfiguration(sdkCredentials, b.Environment, b.HttpClient) + + return CheckoutApi(newConfiguration), nil +} diff --git a/accounts/accounts.go b/accounts/accounts.go new file mode 100644 index 0000000..7adc1f7 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,54 @@ +package accounts + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +const ( + accountsPath = "accounts" + entitiesPath = "entities" + instrumentsPath = "instruments" + payoutSchedulesPath = "payout-schedules" + filesPath = "files" +) + +type AccountHolderType string + +const ( + IndividualType AccountHolderType = "individual" + Corporate AccountHolderType = "corporate" + Government AccountHolderType = "government" +) + +type AccountHolderIdentificationType string + +const ( + PassportType AccountHolderIdentificationType = "passport" + DrivingLicence AccountHolderIdentificationType = "driving_licence" + NationalId AccountHolderIdentificationType = "national_id" + CompanyRegistration AccountHolderIdentificationType = "company_registration" + TaxId AccountHolderIdentificationType = "tax_id" +) + +type ( + AccountHolder struct { + Type AccountHolderType `json:"type,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + CompanyName string `json:"company_name,omitempty"` + TaxId string `json:"tax_id,omitempty"` + DateOfBirth *DateOfBirth `json:"date_of_birth,omitempty"` + CountryOfBirth common.Country `json:"country_of_birth,omitempty"` + ResidentialStatus string `json:"residential_status,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + Identification AccountHolderIdentification `json:"identification,omitempty"` + Email string `json:"email,omitempty"` + } + + AccountHolderIdentification struct { + Type AccountHolderIdentificationType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + IssuingCountry common.Country `json:"issuing_country,omitempty"` + } +) diff --git a/accounts/client.go b/accounts/client.go new file mode 100644 index 0000000..b53bc57 --- /dev/null +++ b/accounts/client.go @@ -0,0 +1,175 @@ +package accounts + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient + filesClient client.HttpClient +} + +func NewClient( + configuration *configuration.Configuration, + apiClient client.HttpClient, + filesClient client.HttpClient, +) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + filesClient: filesClient, + } +} + +func (c *Client) CreateEntity(request OnboardEntityRequest) (*OnboardEntityResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response OnboardEntityResponse + err = c.apiClient.Post( + common.BuildPath(accountsPath, entitiesPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetEntity(entityId string) (*OnboardEntityDetails, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response OnboardEntityDetails + err = c.apiClient.Get( + common.BuildPath(accountsPath, entitiesPath, entityId), + auth, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) UpdateEntity(entityId string, request OnboardEntityRequest) (*OnboardEntityResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response OnboardEntityResponse + err = c.apiClient.Put( + common.BuildPath(accountsPath, entitiesPath, entityId), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CreatePaymentInstrument(entityId string, request PaymentInstrument) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Post( + common.BuildPath(accountsPath, entitiesPath, entityId, instrumentsPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) RetrievePayoutSchedule(entityId string) (*PayoutSchedule, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response PayoutSchedule + err = c.apiClient.Get( + common.BuildPath(accountsPath, entitiesPath, entityId, payoutSchedulesPath), + auth, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) UpdatePayoutSchedule( + entityId string, + currency common.Currency, + updateSchedule CurrencySchedule, +) (*common.IdResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + request := map[common.Currency]CurrencySchedule{ + currency: updateSchedule, + } + + var response common.IdResponse + err = c.apiClient.Put( + common.BuildPath(accountsPath, entitiesPath, entityId, payoutSchedulesPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) UploadFile(file File) (*common.IdResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + req, err := common.BuildFileUploadRequest(&file) + if err != nil { + return nil, err + } + + var response common.IdResponse + err = c.filesClient.Upload(common.BuildPath(filesPath), auth, req, &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/accounts/client_test.go b/accounts/client_test.go new file mode 100644 index 0000000..0254bc6 --- /dev/null +++ b/accounts/client_test.go @@ -0,0 +1,594 @@ +package accounts + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestCreateEntity(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + onboardEntity = OnboardEntityResponse{ + HttpMetadata: httpMetadata, + Id: "ent_1234", + Reference: "reference", + Status: Active, + Capabilities: &Capabilities{ + Payments: &Payments{Available: true}, + }, + } + ) + + cases := []struct { + name string + request OnboardEntityRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*OnboardEntityResponse, error) + }{ + { + name: "when request is correct then create entity", + request: OnboardEntityRequest{ + Reference: "reference", + ContactDetails: &ContactDetails{Phone: &Phone{Number: "2345678910"}}, + Profile: &Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + DateOfBirth: &DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &Identification{NationalIdNumber: "AB123456C"}, + }, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*OnboardEntityResponse) + *respMapping = onboardEntity + }) + }, + checker: func(response *OnboardEntityResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, onboardEntity.Id, response.Id) + assert.Equal(t, onboardEntity.Reference, response.Reference) + assert.Equal(t, onboardEntity.Status, response.Status) + assert.Equal(t, onboardEntity.Capabilities, response.Capabilities) + }, + }, + { + name: "when request is not correct then return error", + request: OnboardEntityRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable", + Data: &errors.ErrorDetails{ + ErrorType: "invalid_request", + ErrorCodes: []string{"company_or_individual_required"}, + }, + }) + }, + checker: func(response *OnboardEntityResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "invalid_request", chkErr.Data.ErrorType) + assert.Contains(t, chkErr.Data.ErrorCodes, "company_or_individual_required") + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *OnboardEntityResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + filesClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient, filesClient) + + tc.checker(client.CreateEntity(tc.request)) + }) + } +} + +func TestGetEntity(t *testing.T) { + var ( + entityId = "ent_1234" + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + entityDetails = OnboardEntityDetails{ + HttpMetadata: httpMetadata, + Id: entityId, + Reference: "reference", + Capabilities: &Capabilities{ + Payments: &Payments{Available: true}, + }, + Status: Active, + ContactDetails: &ContactDetails{Phone: &Phone{Number: "2345678910"}}, + Profile: &Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + DateOfBirth: &DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &Identification{NationalIdNumber: "AB123456C"}, + }, + } + ) + + cases := []struct { + name string + entityId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*OnboardEntityDetails, error) + }{ + { + name: "when entity exists then return entity details", + entityId: entityId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*OnboardEntityDetails) + *respMapping = entityDetails + }) + }, + checker: func(response *OnboardEntityDetails, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, entityId, response.Id) + assert.Equal(t, entityDetails.Reference, response.Reference) + assert.Equal(t, entityDetails.Reference, response.Reference) + assert.Equal(t, entityDetails.Status, response.Status) + assert.Equal(t, entityDetails.Capabilities, response.Capabilities) + assert.Equal(t, entityDetails.ContactDetails, response.ContactDetails) + assert.Equal(t, entityDetails.Profile, response.Profile) + assert.Equal(t, entityDetails.Individual, response.Individual) + }, + }, + { + name: "when entity does not exist then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *OnboardEntityDetails, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *OnboardEntityDetails, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + filesClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient, filesClient) + + tc.checker(client.GetEntity(tc.entityId)) + }) + } +} + +func TestUpdateEntity(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + onboardEntity = OnboardEntityResponse{ + HttpMetadata: httpMetadata, + Id: "ent_1234", + Reference: "reference", + Status: Active, + Capabilities: &Capabilities{ + Payments: &Payments{Available: true}, + }, + } + ) + + cases := []struct { + name string + entityId string + request OnboardEntityRequest + getAuthorization func(*mock.Mock) mock.Call + apiPut func(*mock.Mock) mock.Call + checker func(*OnboardEntityResponse, error) + }{ + { + name: "when request is correct then update entity", + entityId: "ent_1234", + request: OnboardEntityRequest{ + Reference: "reference", + ContactDetails: &ContactDetails{Phone: &Phone{Number: "2345678910"}}, + Profile: &Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + DateOfBirth: &DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &Identification{NationalIdNumber: "AB123456C"}, + }, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*OnboardEntityResponse) + *respMapping = onboardEntity + }) + }, + checker: func(response *OnboardEntityResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, onboardEntity.Id, response.Id) + assert.Equal(t, onboardEntity.Reference, response.Reference) + assert.Equal(t, onboardEntity.Status, response.Status) + assert.Equal(t, onboardEntity.Capabilities, response.Capabilities) + }, + }, + { + name: "when entity not_found then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + request: OnboardEntityRequest{ + Reference: "reference", + ContactDetails: &ContactDetails{Phone: &Phone{Number: "2345678910"}}, + Profile: &Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + DateOfBirth: &DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &Identification{NationalIdNumber: "AB123456C"}, + }, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *OnboardEntityResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + filesClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPut(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient, filesClient) + + tc.checker(client.UpdateEntity(tc.entityId, tc.request)) + }) + } +} + +func TestUpdatePayoutSchedule(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + idResponse = common.IdResponse{ + HttpMetadata: httpMetadata, + Links: map[string]common.Link{ + "self": { + HRef: &[]string{"https://www.test-link.com"}[0], + }, + }, + } + ) + + cases := []struct { + name string + entityId string + currency common.Currency + request CurrencySchedule + getAuthorization func(*mock.Mock) mock.Call + apiPut func(*mock.Mock) mock.Call + checker func(*common.IdResponse, error) + }{ + { + name: "when request is correct then update entity", + entityId: "ent_1234", + currency: common.USD, + request: CurrencySchedule{ + Enabled: true, + Threshold: 500, + Recurrence: NewScheduleFrequencyDailyRequest(), + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.IdResponse) + *respMapping = idResponse + }) + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Links) + assert.Equal(t, idResponse.Links, response.Links) + }, + }, + { + name: "when entity not_found then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + currency: common.USD, + request: CurrencySchedule{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + filesClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPut(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient, filesClient) + + tc.checker(client.UpdatePayoutSchedule(tc.entityId, tc.currency, tc.request)) + }) + } +} + +func TestGetPayoutSchedule(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + schedule = PayoutSchedule{ + HttpMetadata: httpMetadata, + Currency: map[common.Currency]CurrencySchedule{ + common.USD: { + Enabled: true, + Threshold: 500, + Recurrence: NewScheduleFrequencyDailyRequest(), + }, + }, + Links: map[string]common.Link{ + "self": { + HRef: &[]string{"https://www.test-link.com"}[0], + }, + }, + } + ) + + cases := []struct { + name string + entityId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*PayoutSchedule, error) + }{ + { + name: "when entity schedule exists then return entity's payout schedule", + entityId: "ent_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*PayoutSchedule) + *respMapping = schedule + }) + }, + checker: func(response *PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Currency[common.USD]) + assert.True(t, response.Currency[common.USD].Enabled) + assert.Equal(t, 500, response.Currency[common.USD].Threshold) + assert.Equal(t, NewScheduleFrequencyDailyRequest(), response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when entity does not exist then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *PayoutSchedule, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + filesClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient, filesClient) + + tc.checker(client.RetrievePayoutSchedule(tc.entityId)) + }) + } +} diff --git a/accounts/common.go b/accounts/common.go new file mode 100644 index 0000000..0b7eaa2 --- /dev/null +++ b/accounts/common.go @@ -0,0 +1,142 @@ +package accounts + +import "github.com/checkout/checkout-sdk-go/common" + +type BusinessType string + +const ( + GeneralPartnership BusinessType = "general_partnership" + LimitedPartnership BusinessType = "limited_partnership" + PublicLimitedCompany BusinessType = "public_limited_company" + LimitedCompany BusinessType = "limited_company" + ProfessionalAssociation BusinessType = "professional_association" + UnincorporatedAssociation BusinessType = "unincorporated_association" + AutoEntrepreneur BusinessType = "auto_entrepreneur" +) + +type ( + ContactDetails struct { + Phone *Phone `json:"phone,omitempty"` + EntityEmailAddresses *EntityEmailAddresses `json:"email_addresses,omitempty"` + } + + EntityEmailAddresses struct { + Primary []string `json:"primary,omitempty"` + } + + Profile struct { + Urls []string `json:"urls,omitempty"` + Mccs []string `json:"mccs,omitempty"` + DefaultHoldingCurrency common.Currency `json:"default_holding_currency,omitempty"` + } + + Company struct { + BusinessRegistrationNumber string `json:"business_registration_number,omitempty"` + BusinessType BusinessType `json:"business_type,omitempty"` + LegalName string `json:"legal_name,omitempty"` + TradingName string `json:"trading_name,omitempty"` + PrincipalAddress *common.Address `json:"principal_address,omitempty"` + RegisteredAddress *common.Address `json:"registered_address,omitempty"` + Document *EntityDocument `json:"document,omitempty"` + Representatives []Representative `json:"representatives,omitempty"` + FinancialDetails *EntityFinancialDetails `json:"financial_details,omitempty"` + } + + EntityDocument struct { + Type string `json:"type,omitempty"` + FileId string `json:"file_id,omitempty"` + } + + Representative struct { + FirstName string `json:"first_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Address *common.Address `json:"address,omitempty"` + Identification *Identification `json:"identification,omitempty"` + Phone *Phone `json:"phone,omitempty"` + DateOfBirth *DateOfBirth `json:"date_of_birth,omitempty"` + PlaceOfBirth *PlaceOfBirth `json:"place_of_birth,omitempty"` + Roles []string `json:"roles,omitempty"` + } + + Identification struct { + NationalIdNumber string `json:"national_id_number,omitempty"` + Document *Document `json:"document,omitempty"` + } + + Document struct { + Type DocumentType `json:"type,omitempty"` + Front string `json:"front,omitempty"` + Back string `json:"back,omitempty"` + } + + Phone struct { + Number string `json:"number,omitempty"` + } + + DateOfBirth struct { + Day int `json:"day,omitempty"` + Month int `json:"month,omitempty"` + Year int `json:"year,omitempty"` + } + + PlaceOfBirth struct { + Country common.Country `json:"country,omitempty"` + } + + Individual struct { + FirstName string `json:"first_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + LastName string `json:"last_name,omitempty"` + TradingName string `json:"trading_name,omitempty"` + NationalTaxId string `json:"national_tax_id,omitempty"` + RegisteredAddress *common.Address `json:"registered_address,omitempty"` + DateOfBirth *DateOfBirth `json:"date_of_birth,omitempty"` + PlaceOfBirth *PlaceOfBirth `json:"place_of_birth,omitempty"` + Identification *Identification `json:"identification,omitempty"` + } + + Capabilities struct { + Payments *Payments `json:"payments,omitempty"` + Payouts *Payouts `json:"payouts,omitempty"` + } + + Payments struct { + Available bool `json:"available,omitempty"` + Enabled bool `json:"enabled,omitempty"` + } + + Payouts struct { + Available bool `json:"available,omitempty"` + Enabled bool `json:"enabled,omitempty"` + } + + RequirementsDue struct { + Field string `json:"field,omitempty"` + Reason string `json:"reason,omitempty"` + } + + Instrument struct { + Id string `json:"id,omitempty"` + Label string `json:"label,omitempty"` + Status InstrumentStatus `json:"status,omitempty"` + Document *InstrumentDocument `json:"document,omitempty"` + } + + InstrumentDocument struct { + Type string `json:"type,omitempty"` + FileId string `json:"file_id,omitempty"` + } + + EntityFinancialDetails struct { + AnnualProcessingVolume int64 `json:"annual_processing_volume,omitempty"` + AverageTransactionValue int64 `json:"average_transaction_value,omitempty"` + HighestTransactionValue int64 `json:"highest_transaction_value,omitempty"` + Documents *EntityFinancialDocuments `json:"documents,omitempty"` + } + + EntityFinancialDocuments struct { + BankStatement *EntityDocument `json:"bank_statement,omitempty"` + FinancialStatement *EntityDocument `json:"financial_statement,omitempty"` + } +) diff --git a/accounts/document_types.go b/accounts/document_types.go new file mode 100644 index 0000000..252a402 --- /dev/null +++ b/accounts/document_types.go @@ -0,0 +1,12 @@ +package accounts + +type DocumentType string + +const ( + Passport DocumentType = "passport" + NationalIdentityCard DocumentType = "national_identity_card" + DrivingLicense DocumentType = "driving_license" + CitizenCard DocumentType = "citizen_card" + ResidencePermit DocumentType = "residence_permit" + ElectoralId DocumentType = "electoral_id" +) diff --git a/accounts/entities.go b/accounts/entities.go new file mode 100644 index 0000000..31da7b2 --- /dev/null +++ b/accounts/entities.go @@ -0,0 +1,36 @@ +package accounts + +import "github.com/checkout/checkout-sdk-go/common" + +type ( + OnboardEntityRequest struct { + Reference string `json:"reference,omitempty"` + ContactDetails *ContactDetails `json:"contact_details,omitempty"` + Profile *Profile `json:"profile,omitempty"` + Company *Company `json:"company,omitempty"` + Individual *Individual `json:"individual,omitempty"` + } + + OnboardEntityResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Id string `json:"id,omitempty"` + Reference string `json:"reference,omitempty"` + Capabilities *Capabilities `json:"capabilities,omitempty"` + Status OnboardingStatus `json:"status,omitempty"` + RequirementsDue []RequirementsDue `json:"requirements_due,omitempty"` + } + + OnboardEntityDetails struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Id string `json:"id,omitempty"` + Reference string `json:"reference,omitempty"` + Capabilities *Capabilities `json:"capabilities,omitempty"` + Status OnboardingStatus `json:"status,omitempty"` + RequirementsDue []RequirementsDue `json:"requirements_due,omitempty"` + ContactDetails *ContactDetails `json:"contact_details,omitempty"` + Profile *Profile `json:"profile,omitempty"` + Company *Company `json:"company,omitempty"` + Individual *Individual `json:"individual,omitempty"` + Instruments []Instrument `json:"instruments,omitempty"` + } +) diff --git a/accounts/files.go b/accounts/files.go new file mode 100644 index 0000000..ce08c29 --- /dev/null +++ b/accounts/files.go @@ -0,0 +1,20 @@ +package accounts + +import "github.com/checkout/checkout-sdk-go/common" + +type File struct { + File string + Purpose common.Purpose +} + +func (f *File) GetFile() string { + return f.File +} + +func (f *File) GetPurpose() common.Purpose { + return f.Purpose +} + +func (f *File) GetFieldName() string { + return "path" +} diff --git a/accounts/instruments.go b/accounts/instruments.go new file mode 100644 index 0000000..ead73ce --- /dev/null +++ b/accounts/instruments.go @@ -0,0 +1,37 @@ +package accounts + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type InstrumentStatus string + +const ( + Verified InstrumentStatus = "verified" + Unverified InstrumentStatus = "unverified" + InstrumentPending InstrumentStatus = "pending" +) + +type ( + PaymentInstrument struct { + Type instruments.InstrumentType `json:"type,omitempty"` + Label string `json:"label,omitempty"` + AccountType common.AccountType `json:"account_type,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + BranchCode string `json:"branch_code,omitempty"` + Iban string `json:"iban,omitempty"` + Bban string `json:"bban,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Country common.Country `json:"country,omitempty"` + Document *InstrumentDocument `json:"document,omitempty"` + AccountHolder *AccountHolder `json:"account_holder,omitempty"` + Bank *common.BankDetails `json:"bank,omitempty"` + } +) + +func NewAccountsPaymentInstrument() *PaymentInstrument { + return &PaymentInstrument{Type: instruments.BankAccount} +} diff --git a/accounts/onboarding_status.go b/accounts/onboarding_status.go new file mode 100644 index 0000000..0a10a1f --- /dev/null +++ b/accounts/onboarding_status.go @@ -0,0 +1,12 @@ +package accounts + +type OnboardingStatus string + +const ( + Active OnboardingStatus = "active" + Pending OnboardingStatus = "pending" + Restricted OnboardingStatus = "restricted" + RequirementDue OnboardingStatus = "requirements_due" + Inactive OnboardingStatus = "inactive" + Rejected OnboardingStatus = "rejected" +) diff --git a/accounts/payouts.go b/accounts/payouts.go new file mode 100644 index 0000000..0872d20 --- /dev/null +++ b/accounts/payouts.go @@ -0,0 +1,170 @@ +package accounts + +import ( + "encoding/json" + "fmt" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/errors" +) + +type Frequency string + +const ( + Weekly Frequency = "Weekly" + Daily Frequency = "Daily" + Monthly Frequency = "Monthly" +) + +type DaySchedule string + +const ( + Monday DaySchedule = "Monday" + Tuesday DaySchedule = "Tuesday" + Wednesday DaySchedule = "Wednesday" + Thursday DaySchedule = "Thursday" + Friday DaySchedule = "Friday" + Saturday DaySchedule = "Saturday" + Sunday DaySchedule = "Sunday" +) + +type ( + Recurrence interface { + GetSchedule() Frequency + } + + scheduleFrequencyDaily struct { + Frequency + } + + scheduleFrequencyWeekly struct { + Frequency + ByDay []DaySchedule `json:"by_day,omitempty"` + } + + scheduleFrequencyMonthly struct { + Frequency + ByMonthDay []int `json:"by_month_day,omitempty"` + } +) + +func NewScheduleFrequencyDailyRequest() scheduleFrequencyDaily { + return scheduleFrequencyDaily{ + Frequency: Daily, + } +} + +func NewScheduleFrequencyWeeklyRequest(days []DaySchedule) scheduleFrequencyWeekly { + return scheduleFrequencyWeekly{ + Frequency: Weekly, + ByDay: days, + } +} + +func NewScheduleFrequencyMonthlyRequest(days []int) scheduleFrequencyMonthly { + return scheduleFrequencyMonthly{ + Frequency: Monthly, + ByMonthDay: days, + } +} + +func (s scheduleFrequencyDaily) GetSchedule() Frequency { + return s.Frequency +} + +func (s scheduleFrequencyWeekly) GetSchedule() Frequency { + return s.Frequency +} + +func (s scheduleFrequencyMonthly) GetSchedule() Frequency { + return s.Frequency +} + +type ( + CurrencySchedule struct { + Enabled bool `json:"enabled,omitempty"` + Threshold int `json:"threshold,omitempty"` + Recurrence Recurrence `json:"recurrence,omitempty"` + } + + PayoutSchedule struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Currency map[common.Currency]CurrencySchedule + Links map[string]common.Link `json:"_links"` + } +) + +func (p *PayoutSchedule) UnmarshalJSON(data []byte) error { + p.Currency = make(map[common.Currency]CurrencySchedule) + + var currencyMap map[common.Currency]currencyUnmarshaler + if err := json.Unmarshal(data, ¤cyMap); err != nil { + return err + } + + var currency CurrencySchedule + for k := range currencyMap { + if k != "_links" { + switch currencyMap[k].Recurrence.Frequency { + case Daily: + var schedule map[common.Currency]dailyScheduleUnmarshaler + if err := json.Unmarshal(data, &schedule); err != nil { + return err + } + currency.Recurrence = schedule[k].Recurrence + case Weekly: + var schedule map[common.Currency]weeklyScheduleUnmarshaler + if err := json.Unmarshal(data, &schedule); err != nil { + return err + } + currency.Recurrence = schedule[k].Recurrence + case Monthly: + var schedule map[common.Currency]monthlyScheduleUnmarshaler + if err := json.Unmarshal(data, &schedule); err != nil { + return err + } + currency.Recurrence = schedule[k].Recurrence + default: + return errors.UnsupportedTypeError(fmt.Sprintf("%s currency frequency is unsupported", k)) + } + + currency.Enabled = currencyMap[k].Enabled + currency.Threshold = currencyMap[k].Threshold + p.Currency[k] = currency + } + } + + var links linksUnmarshaler + if err := json.Unmarshal(data, &links); err != nil { + return err + } + p.Links = links.Links + + return nil +} + +type ( + currencyUnmarshaler struct { + Enabled bool `json:"enabled,omitempty"` + Threshold int `json:"threshold,omitempty"` + Recurrence struct { + Frequency + } + } + + linksUnmarshaler struct { + Links map[string]common.Link `json:"_links"` + } + + dailyScheduleUnmarshaler struct { + Recurrence scheduleFrequencyDaily `json:"recurrence,omitempty"` + } + + weeklyScheduleUnmarshaler struct { + Recurrence scheduleFrequencyWeekly `json:"recurrence,omitempty"` + } + + monthlyScheduleUnmarshaler struct { + Recurrence scheduleFrequencyMonthly `json:"recurrence,omitempty"` + } +) diff --git a/apm/ideal/client.go b/apm/ideal/client.go new file mode 100644 index 0000000..b9d94cf --- /dev/null +++ b/apm/ideal/client.go @@ -0,0 +1,49 @@ +package ideal + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) GetInfo() (*IdealInfo, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response IdealInfo + err = c.apiClient.Get(common.BuildPath(idealExternalPath), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetIssuers() (*IssuerResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response IssuerResponse + err = c.apiClient.Get(common.BuildPath(idealExternalPath, issuersPath), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/apm/ideal/client_test.go b/apm/ideal/client_test.go new file mode 100644 index 0000000..2a68c73 --- /dev/null +++ b/apm/ideal/client_test.go @@ -0,0 +1,84 @@ +package ideal + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestGetInfo(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + infoLinks = InfoLinks{ + Curies: []CuriesLink{ + { + Name: "test link", + Href: "https://test-link.com", + Templated: false, + }, + }, + } + + response = IdealInfo{ + HttpMetadata: httpMetadata, + IdealInfoLinks: infoLinks, + } + ) + + cases := []struct { + name string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*IdealInfo, error) + }{ + { + name: "when auth is correct then return info links", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*IdealInfo) + *respMapping = response + }) + }, + checker: func(response *IdealInfo, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.IdealInfoLinks) + assert.NotNil(t, response.IdealInfoLinks.Curies) + assert.Equal(t, infoLinks.Curies, response.IdealInfoLinks.Curies) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetInfo()) + }) + } +} diff --git a/apm/ideal/ideal.go b/apm/ideal/ideal.go new file mode 100644 index 0000000..f3e2286 --- /dev/null +++ b/apm/ideal/ideal.go @@ -0,0 +1,45 @@ +package ideal + +import "github.com/checkout/checkout-sdk-go/common" + +const ( + idealExternalPath = "ideal-external" + issuersPath = "issuers" +) + +type ( + IdealInfo struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + IdealInfoLinks InfoLinks `json:"_links,omitempty"` + } + + InfoLinks struct { + Self common.Link `json:"self,omitempty"` + Curies []CuriesLink `json:"curies,omitempty"` + Issuers common.Link `json:"ideal:issuers,omitempty"` + } + + CuriesLink struct { + Name string `json:"name,omitempty"` + Href string `json:"href,omitempty"` + Templated bool `json:"templated,omitempty"` + } +) + +type ( + IssuerResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Countries []IdealCountry `json:"countries,omitempty"` + Links map[string]common.Link `json:"_links,omitempty"` + } + + IdealCountry struct { + Name string `json:"name,omitempty"` + Issuers []Issuer `json:"issuers,omitempty"` + } + + Issuer struct { + Bic string `json:"bic,omitempty"` + Name string `json:"name,omitempty"` + } +) diff --git a/apm/klarna/client.go b/apm/klarna/client.go new file mode 100644 index 0000000..addca8f --- /dev/null +++ b/apm/klarna/client.go @@ -0,0 +1,110 @@ +package klarna + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/payments" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) CreateCreditSession(request CreditSessionRequest) (*CreditSessionResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.PublicKey) + if err != nil { + return nil, err + } + + var response CreditSessionResponse + err = c.apiClient.Post( + common.BuildPath(c.getBaseUrl(), creditSessionPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetCreditSession(sessionId string) (*CreditSession, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.PublicKey) + if err != nil { + return nil, err + } + + var response CreditSession + err = c.apiClient.Get( + common.BuildPath(c.getBaseUrl(), creditSessionPath, sessionId), + auth, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CapturePayment(paymentId string, request OrderCaptureRequest) (*CaptureResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response CaptureResponse + err = c.apiClient.Post( + common.BuildPath(c.getBaseUrl(), ordersPath, paymentId, capturesPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) VoidPayment(paymentId string, request payments.VoidRequest) (*payments.VoidResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response payments.VoidResponse + err = c.apiClient.Post( + common.BuildPath(c.getBaseUrl(), ordersPath, paymentId, voidsPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) getBaseUrl() string { + if c.configuration.Environment.IsSandbox() { + return "klarna-external" + } + + return "klarna" +} diff --git a/apm/klarna/client_test.go b/apm/klarna/client_test.go new file mode 100644 index 0000000..5f3aa77 --- /dev/null +++ b/apm/klarna/client_test.go @@ -0,0 +1,415 @@ +package klarna + +import ( + "github.com/checkout/checkout-sdk-go/payments" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestCreateSession(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + session = CreditSessionResponse{ + HttpMetadata: httpMetadata, + SessionId: "session_id", + ClientToken: "client_token", + } + ) + + cases := []struct { + name string + request CreditSessionRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*CreditSessionResponse, error) + }{ + { + name: "when request is correct then create klarna session", + request: CreditSessionRequest{ + PurchaseCountry: common.GB, + Currency: common.GBP, + Locale: "en-GB", + Amount: 1000, + TaxAmount: 1, + Products: getKlarnaProduct(), + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CreditSessionResponse) + *respMapping = session + }) + }, + checker: func(response *CreditSessionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, session.SessionId, response.SessionId) + assert.Equal(t, session.ClientToken, response.ClientToken) + }, + }, + { + name: "when request is invalid then return error", + request: CreditSessionRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable Entity", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "amount_required", + }, + }, + }) + }, + checker: func(response *CreditSessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CreateCreditSession(tc.request)) + }) + } +} + +func getKlarnaProduct() []map[string]interface{} { + return []map[string]interface{}{ + { + "name": "test product", + "quantity": 1, + "unit_price": 1000, + "tax_rate": 0, + "total_amount": 1000, + "total_tax_amount": 0, + }, + } +} + +func TestGetCreditSession(t *testing.T) { + var ( + sessionId = "session_id" + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + session = CreditSession{ + HttpMetadata: httpMetadata, + ClientToken: "client_token", + PurchaseCountry: string(common.GB), + Currency: string(common.GBP), + Amount: 100, + TaxAmount: 0, + } + ) + + cases := []struct { + name string + sessionId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*CreditSession, error) + }{ + { + name: "when session exists then return session", + sessionId: sessionId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*CreditSession) + *respMapping = session + }) + }, + checker: func(response *CreditSession, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, session.ClientToken, response.ClientToken) + assert.Equal(t, session.PurchaseCountry, response.PurchaseCountry) + assert.Equal(t, session.Currency, response.Currency) + assert.Equal(t, session.Amount, response.Amount) + }, + }, + { + name: "when session not found then return error", + sessionId: "invalid_session_id", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }, + ) + }, + checker: func(response *CreditSession, err error) { + assert.NotNil(t, err) + assert.Nil(t, response) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetCreditSession(tc.sessionId)) + }) + } +} + +func TestCapturePayment(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + captureResponse = CaptureResponse{ + HttpMetadata: httpMetadata, + ActionId: "action_id", + Reference: "reference", + } + ) + + cases := []struct { + name string + paymentId string + request OrderCaptureRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*CaptureResponse, error) + }{ + { + name: "when request is correct then capture payment", + paymentId: "1234", + request: OrderCaptureRequest{ + Type: payments.KlarnaSource, + Amount: 1000, + Reference: "reference", + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CaptureResponse) + *respMapping = captureResponse + }) + }, + checker: func(response *CaptureResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, captureResponse.ActionId, response.ActionId) + assert.Equal(t, captureResponse.Reference, response.Reference) + }, + }, + { + name: "when request is invalid then return error", + paymentId: "1234", + request: OrderCaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable Entity", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "amount_required", + }, + }, + }) + }, + checker: func(response *CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CapturePayment(tc.paymentId, tc.request)) + }) + } +} + +func TestVoidPayment(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + voidResponse = payments.VoidResponse{ + HttpMetadata: httpMetadata, + ActionId: "action_id", + } + ) + + cases := []struct { + name string + paymentId string + request payments.VoidRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.VoidResponse, error) + }{ + { + name: "when request is correct then void payment", + paymentId: "1234", + request: payments.VoidRequest{ + Reference: "reference", + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.VoidResponse) + *respMapping = voidResponse + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, voidResponse.ActionId, response.ActionId) + assert.Equal(t, voidResponse.Reference, response.Reference) + }, + }, + { + name: "when request is invalid then return error", + paymentId: "1234", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable Entity", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "amount_required", + }, + }, + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.VoidPayment(tc.paymentId, tc.request)) + }) + } +} diff --git a/apm/klarna/klarna.go b/apm/klarna/klarna.go new file mode 100644 index 0000000..f28cb7d --- /dev/null +++ b/apm/klarna/klarna.go @@ -0,0 +1,80 @@ +package klarna + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +const ( + creditSessionPath = "credit-sessions" + ordersPath = "orders" + capturesPath = "captures" + voidsPath = "voids" +) + +type ( + CreditSessionRequest struct { + PurchaseCountry common.Country `json:"purchase_country,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Locale string `json:"locale,omitempty"` + Amount int64 `json:"amount,omitempty"` + TaxAmount int `json:"tax_amount,omitempty"` + Products []map[string]interface{} `json:"products,omitempty"` + } + + CreditSessionResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + SessionId string `json:"session_id,omitempty"` + ClientToken string `json:"client_token,omitempty"` + PaymentMethodCategories []PaymentMethod `json:"payment_method_categories,omitempty"` + } + + PaymentMethod struct { + Identifier string `json:"identifier,omitempty"` + Name string `json:"name,omitempty"` + AssetUrls *AssetUrl `json:"asset_urls,omitempty"` + } + + AssetUrl struct { + Descriptive string `json:"descriptive,omitempty"` + Standard string `json:"standard,omitempty"` + } +) + +type ( + CreditSession struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + ClientToken string `json:"client_token,omitempty"` + PurchaseCountry string `json:"purchase_country,omitempty"` + Currency string `json:"currency,omitempty"` + Locale string `json:"locale,omitempty"` + Amount int64 `json:"amount,omitempty"` + TaxAmount int `json:"tax_amount,omitempty"` + Products []map[string]interface{} `json:"products,omitempty"` + } +) + +type ( + OrderCaptureRequest struct { + Type payments.SourceType `json:"type,omitempty"` + Amount int64 `json:"amount,omitempty"` + Reference string `json:"reference,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Klarna *Klarna `json:"klarna,omitempty"` + ShippingInfo map[string]interface{} `json:"shipping_info,omitempty"` + ShippingDelay int `json:"shipping_delay,omitempty"` + } + + CaptureResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + ActionId string `json:"action_id,omitempty"` + Reference string `json:"reference,omitempty"` + } + + Klarna struct { + Description string `json:"description,omitempty"` + Products []map[string]interface{} `json:"products,omitempty"` + ShippingInfo []map[string]interface{} `json:"shipping_info,omitempty"` + ShippingDelay int `json:"shipping_delay,omitempty"` + } +) diff --git a/apm/sepa/client.go b/apm/sepa/client.go new file mode 100644 index 0000000..a2903c9 --- /dev/null +++ b/apm/sepa/client.go @@ -0,0 +1,91 @@ +package sepa + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) GetMandate(mandateId string) (*MandateResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response MandateResponse + err = c.apiClient.Get(common.BuildPath(sepaMandatesPath, mandateId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CancelMandate(mandateId string) (*SepaResource, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response SepaResource + err = c.apiClient.Post( + common.BuildPath(sepaMandatesPath, mandateId, cancelPath), + auth, + nil, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetMandateViaPpro(mandateId string) (*MandateResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response MandateResponse + err = c.apiClient.Get(common.BuildPath(pproPath, sepaMandatesPath, mandateId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CancelMandateViaPpro(mandateId string) (*SepaResource, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response SepaResource + err = c.apiClient.Post( + common.BuildPath(pproPath, sepaMandatesPath, mandateId, cancelPath), + auth, + nil, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/apm/sepa/client_test.go b/apm/sepa/client_test.go new file mode 100644 index 0000000..8b02023 --- /dev/null +++ b/apm/sepa/client_test.go @@ -0,0 +1,170 @@ +package sepa + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestGetMandate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + mandate = MandateResponse{ + HttpMetadata: httpMetadata, + MandateReference: "reference", + CustomerId: "cus_1234", + FirstName: "Bruce", + LastName: "Wayne", + City: "Gotham", + } + ) + + cases := []struct { + name string + mandateId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*MandateResponse, error) + }{ + { + name: "when mandate exists then return mandate info", + mandateId: "mandate_id", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*MandateResponse) + *respMapping = mandate + }) + }, + checker: func(response *MandateResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, mandate.MandateReference, response.MandateReference) + assert.Equal(t, mandate.CustomerId, response.CustomerId) + }, + }, + { + name: "when mandate not found then return error", + mandateId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *MandateResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetMandate(tc.mandateId)) + }) + } +} + +func TestCancelMandate(t *testing.T) { + var ( + link = "https://test-link.com" + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + cancelResponse = SepaResource{ + HttpMetadata: httpMetadata, + Links: map[string]common.Link{ + "payment": { + HRef: &link, + }, + }, + } + ) + + cases := []struct { + name string + mandateId string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*SepaResource, error) + }{ + { + name: "when request is correct then cancel mandate", + mandateId: "1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*SepaResource) + *respMapping = cancelResponse + }) + }, + checker: func(response *SepaResource, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, cancelResponse.Links, response.Links) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CancelMandate(tc.mandateId)) + }) + } +} diff --git a/apm/sepa/sepa.go b/apm/sepa/sepa.go new file mode 100644 index 0000000..272c61b --- /dev/null +++ b/apm/sepa/sepa.go @@ -0,0 +1,35 @@ +package sepa + +import "github.com/checkout/checkout-sdk-go/common" + +const ( + sepaMandatesPath = "sepa/mandates" + pproPath = "ppro" + cancelPath = "cancel" +) + +type ( + MandateResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + MandateReference string `json:"mandate_reference,omitempty"` + CustomerId string `json:"customer_id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + AddressLine1 string `json:"address_line1,omitempty"` + City string `json:"city,omitempty"` + Zip string `json:"zip,omitempty"` + Country common.Country `json:"country,omitempty"` + MaskedAccountIban string `json:"masked_account_iban,omitempty"` + AccountCurrencyCode string `json:"account_currency_code,omitempty"` + AccountCountryCode common.Country `json:"account_country_code,omitempty"` + MandateState string `json:"mandate_state,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + MandateType string `json:"mandate_type,omitempty"` + Links map[string]common.Link `json:"_links,omitempty"` + } + + SepaResource struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Links map[string]common.Link `json:"_links,omitempty"` + } +) diff --git a/checkout.go b/checkout.go index 29fa127..32465ea 100644 --- a/checkout.go +++ b/checkout.go @@ -1,374 +1,5 @@ package checkout -import ( - "bytes" - "crypto/tls" - "encoding/json" - "fmt" - "net/http" - "os/exec" - "regexp" - "runtime" - "time" - - "github.com/checkout/checkout-sdk-go/common" -) - -// ClientVersion ... -const ClientVersion = "0.0.1" - -const ( - sandboxURI = "https://api.sandbox.checkout.com" - productionURI = "https://api.checkout.com" -) - -const ( - // UPAPI - Unified Payment API - UPAPI SupportedAPI = "api" - // Access - OAuth Authorization - Access SupportedAPI = "access" -) - -var mbcLiveSecretKeyPattern = regexp.MustCompile(common.LiveSecretKeyRegex) -var fourKeyPattern = regexp.MustCompile(common.FourKeyRegex) -var fourOAuthJwtPattern = regexp.MustCompile(common.FourOAuthJwtPattern) - -const ( - // Sandbox - Sandbox - Sandbox SupportedEnvironment = "sandbox.checkout.com" - // Production - Production - Production SupportedEnvironment = "checkout.com" - // UnknownPlatform - Production - UnknownPlatform string = "unknown platform" -) - -const ( - // DefaultMaxNetworkRetries is the default maximum number of retries made - // by a Checkout.com client. - DefaultMaxNetworkRetries int64 = 2 -) - -const ( - // CKORequestID ... - CKORequestID = "cko-request-id" - // CKOVersion ... - CKOVersion = "cko-version" -) - -// SupportedAPI is an enumeration of supported Checkout.com endpoints. -// Currently supported values are "Unified Payment Gateway". -type SupportedAPI string - -// SupportedEnvironment is an enumeration of supported Checkout.com environment. -// Currently supported values are "Sandbox" & "Production". -type SupportedEnvironment string - -// Config ... -type Config struct { - PublicKey string - SecretKey string - URI *string - HTTPClient *http.Client - LeveledLogger LeveledLoggerInterface - MaxNetworkRetries *int64 - BearerAuthentication bool -} - -const ( - defaultHTTPTimeout = 30 * time.Second -) - -var httpClient = &http.Client{ - Timeout: defaultHTTPTimeout, - Transport: &http.Transport{ - TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), - }, -} - -// Deprecated: Please use SdkConfig -func Create(secretKey string, publicKey *string) (*Config, error) { - - var config, isSandbox = create(secretKey) - - if config.HTTPClient == nil { - config.HTTPClient = httpClient - } - - if config.LeveledLogger == nil { - config.LeveledLogger = DefaultLeveledLogger - } - - if config.MaxNetworkRetries == nil { - config.MaxNetworkRetries = Int64(DefaultMaxNetworkRetries) - } - - if publicKey == nil { - return &config, nil - } - - if !isSandbox { - publicKeyMatch := regexp.MustCompile(common.LivePublicKeyRegex) - if publicKeyMatch.MatchString(StringValue(publicKey)) { - config.PublicKey = StringValue(publicKey) - return &config, nil - } - return nil, &common.Error{ - Status: "Configuration Error - Please review your secret key and public key ", - } - } - publicKeyMatch := regexp.MustCompile(common.SandboxPublicKeyRegex) - if publicKeyMatch.MatchString(StringValue(publicKey)) { - config.PublicKey = StringValue(publicKey) - return &config, nil - } - return nil, &common.Error{ - Status: "Configuration Error - Please review your secret key and public key ", - } -} - -func SdkConfig(secretKey *string, publicKey *string, env SupportedEnvironment) (*Config, error) { - - var config Config - config.SecretKey = StringValue(secretKey) - config.PublicKey = StringValue(publicKey) - config.BearerAuthentication = shouldApplyBearer(config) - - if env == Sandbox { - config.URI = String(sandboxURI) - } else if env == Production { - config.URI = String(productionURI) - } - - if config.HTTPClient == nil { - config.HTTPClient = httpClient - } - if config.LeveledLogger == nil { - config.LeveledLogger = DefaultLeveledLogger - } - if config.MaxNetworkRetries == nil { - config.MaxNetworkRetries = Int64(DefaultMaxNetworkRetries) - } - - return &config, nil -} - -func shouldApplyBearer(config Config) bool { - // SecretKey or PublicKey matches a Four pattern - if fourKeyPattern.MatchString(config.SecretKey) || fourKeyPattern.MatchString(config.PublicKey) { - return true - } - // SecretKey or PublicKey matches a JWT - if fourOAuthJwtPattern.MatchString(config.SecretKey) || fourOAuthJwtPattern.MatchString(config.PublicKey) { - return true - } - return false -} - -func create(secretKey string) (Config, bool) { - - if mbcLiveSecretKeyPattern.MatchString(secretKey) { - return Config{ - URI: String(productionURI), - SecretKey: secretKey, - }, false - } - return Config{ - URI: String(sandboxURI), - SecretKey: secretKey, - }, true -} - -var appInfo *AppInfo -var encodedCheckoutUserAgent string -var encodedUserAgent string - -// AppInfo ... -type AppInfo struct { - Name string `json:"name"` - URL string `json:"url"` - Version string `json:"version"` -} - -// SetAppInfo sets app information. See AppInfo. -func SetAppInfo(info *AppInfo) { - if info != nil && info.Name == "" { - panic(fmt.Errorf("App info name cannot be empty")) - } - appInfo = info - - // This is run in init, but we need to reinitialize it now that we have - // some app info. - initUserAgent() -} - -func (a *AppInfo) formatUserAgent() string { - str := a.Name - if a.Version != "" { - str += "/" + a.Version - } - if a.URL != "" { - str += " (" + a.URL + ")" - } - return str -} - -type checkoutClientUserAgent struct { - Application *AppInfo `json:"application"` - BindingsVersion string `json:"bindings_version"` - Language string `json:"lang"` - LanguageVersion string `json:"lang_version"` - Publisher string `json:"publisher"` - Uname string `json:"uname"` -} - -func initUserAgent() { - - encodedUserAgent = "Checkout/v1 GoBindings/" + ClientVersion - if appInfo != nil { - encodedUserAgent += " " + appInfo.formatUserAgent() - } - - checkoutUserAgent := &checkoutClientUserAgent{ - Application: appInfo, - BindingsVersion: ClientVersion, - Language: "go", - LanguageVersion: runtime.Version(), - Publisher: "checkout.com", - Uname: getUname(), - } - marshaled, err := json.Marshal(checkoutUserAgent) - // Encoding this struct should never be a problem, so we're okay to panic - // in case it is for some reason. - if err != nil { - panic(err) - } - encodedCheckoutUserAgent = string(marshaled) -} - -func getUname() string { - path, err := exec.LookPath("uname") - if err != nil { - return UnknownPlatform - } - - cmd := exec.Command(path, "-a") - var out bytes.Buffer - cmd.Stderr = nil // goes to os.DevNull - cmd.Stdout = &out - err = cmd.Run() - if err != nil { - return UnknownPlatform - } - - return out.String() -} - -// StatusResponse ... -type StatusResponse struct { - Status string `json:"status,omitempty"` - StatusCode int `json:"status_code,omitempty"` - ResponseBody []byte `json:"response_body,omitempty"` - ResponseCSV [][]string `json:"response_csv,omitempty"` - Headers *Headers `json:"headers,omitempty"` -} - -// Headers ... -type Headers struct { - Header http.Header - CKORequestID *string `json:"cko-request-id,omitempty"` - CKOVersion *string `json:"cko-version,omitempty"` -} - -// HTTPClient ... -type HTTPClient interface { - Get(path string) (*StatusResponse, error) - Post(path string, request interface{}, params *Params) (*StatusResponse, error) - Put(path string, request interface{}) (*StatusResponse, error) - Patch(path string, request interface{}) (*StatusResponse, error) - Delete(path string) (*StatusResponse, error) - Upload(path, boundary string, body *bytes.Buffer) (*StatusResponse, error) - Download(path string) (*StatusResponse, error) -} - -// Int64 returns a pointer to the int64 value passed in. -func Int64(v int64) *int64 { - return &v -} - -// Int64Value returns the value of the int64 pointer passed in or -// 0 if the pointer is nil. -func Int64Value(v *int64) int64 { - if v != nil { - return *v - } - return 0 -} - -// String returns a pointer to the string value passed in. -func String(v string) *string { - return &v -} - -// StringValue returns the value of the string pointer passed in or -// "" if the pointer is nil. -func StringValue(v *string) string { - if v != nil { - return *v - } - return "" -} - -// StringSlice returns a slice of string pointers given a slice of strings. -func StringSlice(v []string) []*string { - out := make([]*string, len(v)) - for i := range v { - out[i] = &v[i] - } - return out -} - -// Bool returns a pointer to the bool value passed in. -func Bool(v bool) *bool { - return &v -} - -// BoolValue returns the value of the bool pointer passed in or -// false if the pointer is nil. -func BoolValue(v *bool) bool { - if v != nil { - return *v - } - return false -} - -// BoolSlice returns a slice of bool pointers given a slice of bools. -func BoolSlice(v []bool) []*bool { - out := make([]*bool, len(v)) - for i := range v { - out[i] = &v[i] - } - return out -} - -// Float64 returns a pointer to the float64 value passed in. -func Float64(v float64) *float64 { - return &v -} - -// Float64Value returns the value of the float64 pointer passed in or -// 0 if the pointer is nil. -func Float64Value(v *float64) float64 { - if v != nil { - return *v - } - return 0 -} - -// Float64Slice returns a slice of float64 pointers given a slice of float64s. -func Float64Slice(v []float64) []*float64 { - out := make([]*float64, len(v)) - for i := range v { - out[i] = &v[i] - } - return out +func Builder() *CheckoutSdkBuilder { + return &CheckoutSdkBuilder{} } diff --git a/checkout_sdk_builder.go b/checkout_sdk_builder.go new file mode 100644 index 0000000..f11d9f4 --- /dev/null +++ b/checkout_sdk_builder.go @@ -0,0 +1,20 @@ +package checkout + +import ( + "github.com/checkout/checkout-sdk-go/abc" + "github.com/checkout/checkout-sdk-go/nas" +) + +type CheckoutSdkBuilder struct{} + +func (b *CheckoutSdkBuilder) Previous() *abc.CheckoutPreviousSdkBuilder { + return &abc.CheckoutPreviousSdkBuilder{} +} + +func (b *CheckoutSdkBuilder) StaticKeys() *nas.CheckoutDefaultSdkBuilder { + return &nas.CheckoutDefaultSdkBuilder{} +} + +func (b *CheckoutSdkBuilder) OAuth() *nas.CheckoutOAuthSdkBuilder { + return &nas.CheckoutOAuthSdkBuilder{} +} diff --git a/checkout_test.go b/checkout_test.go deleted file mode 100644 index 1f5275f..0000000 --- a/checkout_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package checkout - -import ( - "github.com/google/uuid" - assert "github.com/stretchr/testify/require" - "testing" -) - -func TestCreateSdkConfigDifferentKeyPatterns_Sandbox(t *testing.T) { - secretKeys := []string{"sk_test_fde517a8-3f01-41ef-b4bd-4282384b0a64", "sk_sbox_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - publicKeys := []string{"pk_test_fde517a8-3f01-41ef-b4bd-4282384b0a64", "pk_sbox_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - for _, sk := range secretKeys { - for _, pk := range publicKeys { - config, err := SdkConfig(&sk, &pk, Sandbox) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, sandboxURI, *config.URI) - assert.Equal(t, sk, config.SecretKey) - assert.Equal(t, pk, config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - } - } -} - -func TestCreateSdkConfigDifferentKeyPatterns_Production(t *testing.T) { - secretKeys := []string{"sk_fde517a8-3f01-41ef-b4bd-4282384b0a64", "sk_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - publicKeys := []string{"pk_fde517a8-3f01-41ef-b4bd-4282384b0a64", "pk_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - for _, sk := range secretKeys { - for _, pk := range publicKeys { - config, err := SdkConfig(&sk, &pk, Production) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, productionURI, *config.URI) - assert.Equal(t, sk, config.SecretKey) - assert.Equal(t, pk, config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - } - } -} - -func TestCreateSdkConfigUnknownKeyPatterns_Sandbox(t *testing.T) { - secretKeys := []string{"sk_fde517a8-3f01-41ef", "cko_123_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - publicKeys := []string{"pk_-b4bd-4282384b0a64", "cko_m73dzbpy7cf3gfd46xr4yj5xo4e", uuid.New().String()} - for _, sk := range secretKeys { - for _, pk := range publicKeys { - config, err := SdkConfig(&sk, &pk, Production) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, productionURI, *config.URI) - assert.Equal(t, sk, config.SecretKey) - assert.Equal(t, pk, config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - assert.False(t, config.BearerAuthentication) - } - } -} - -func TestAssignBearerAuthenticationDifferentKeyPatterns(t *testing.T) { - validFourSecretKeys := []string{"sk_sbox_m73dzbpy7cf3gfd46xr4yj5xo4e", "sk_m73dzbpy7cf3gfd46xr4yj5xo4e", - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NDQ0MTYzMTYsImV4cCI6MTY3NTk1MjMxNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.n53cqgfiUp8q9TTNCOt43EG5IXqaL9rhqblj63OKifU"} - for _, sk := range validFourSecretKeys { - config, err := SdkConfig(&sk, nil, Production) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, productionURI, *config.URI) - assert.Equal(t, sk, config.SecretKey) - assert.Equal(t, "", config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - assert.True(t, config.BearerAuthentication) - } - validMbcPublicKeys := []string{"pk_fde517a8-3f01-41ef-b4bd-4282384b0a64", "pk_test_fde517a8-3f01-41ef-b4bd-4282384b0a64"} - for _, pk := range validMbcPublicKeys { - config, err := SdkConfig(nil, &pk, Sandbox) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, sandboxURI, *config.URI) - assert.Equal(t, "", config.SecretKey) - assert.Equal(t, pk, config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - assert.False(t, config.BearerAuthentication) - } - invalidOAuthJwts := []string{"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJFkpva2NNCOt43EG5IXqaL9rhqblj63OKifU", "123123213.adasdsadasdasdasdas"} - for _, sk := range invalidOAuthJwts { - config, err := SdkConfig(&sk, nil, Sandbox) - assert.Nil(t, err) - assert.NotNil(t, config) - assert.Equal(t, sandboxURI, *config.URI) - assert.Equal(t, sk, config.SecretKey) - assert.Equal(t, "", config.PublicKey) - assert.NotNil(t, config.HTTPClient) - assert.NotNil(t, config.LeveledLogger) - assert.NotNil(t, config.MaxNetworkRetries) - assert.False(t, config.BearerAuthentication) - } -} diff --git a/client/api.go b/client/api.go deleted file mode 100644 index e51388d..0000000 --- a/client/api.go +++ /dev/null @@ -1,74 +0,0 @@ -package client - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/customers" - "github.com/checkout/checkout-sdk-go/disputes" - "github.com/checkout/checkout-sdk-go/events" - "github.com/checkout/checkout-sdk-go/files" - "github.com/checkout/checkout-sdk-go/instruments" - "github.com/checkout/checkout-sdk-go/payments" - "github.com/checkout/checkout-sdk-go/reconciliation" - "github.com/checkout/checkout-sdk-go/sources" - "github.com/checkout/checkout-sdk-go/tokens" - "github.com/checkout/checkout-sdk-go/webhooks" -) - -// API - -type API struct { - Payments *payments.Client - Sources *sources.Client - Tokens *tokens.Client - Events *events.Client - Webhooks *webhooks.Client - Disputes *disputes.Client - Files *files.Client - Reconciliation *reconciliation.Client - Instruments *instruments.Client - Customers *customers.Client -} - -// Deprecated: This initialization method does not support the new Configuration entrypoint. To use the new entrypoint -// please use CheckoutApi -func (a *API) Init(secretKey string, useSandbox bool, publicKey *string) { - - config, err := checkout.Create(secretKey, publicKey) - if err != nil { - return - } - a.Payments = payments.NewClient(*config) - a.Sources = sources.NewClient(*config) - a.Tokens = tokens.NewClient(*config) - a.Events = events.NewClient(*config) - a.Webhooks = webhooks.NewClient(*config) - a.Disputes = disputes.NewClient(*config) - a.Files = files.NewClient(*config) - a.Reconciliation = reconciliation.NewClient(*config) -} - -// Deprecated: This initialization method does not support the new Configuration entrypoint. To use the new entrypoint -// please use CheckoutApi -func New(secretKey string, useSandbox bool, publicKey *string) *API { - - api := API{} - api.Init(secretKey, useSandbox, publicKey) - return &api -} - -func CheckoutApi(secretKey *string, publicKey *string, environment checkout.SupportedEnvironment) *API { - config, err := checkout.SdkConfig(secretKey, publicKey, environment) - if err != nil { - panic(err) - } - api := API{} - api.Payments = payments.NewClient(*config) - api.Sources = sources.NewClient(*config) - api.Tokens = tokens.NewClient(*config) - api.Events = events.NewClient(*config) - api.Webhooks = webhooks.NewClient(*config) - api.Disputes = disputes.NewClient(*config) - api.Files = files.NewClient(*config) - api.Reconciliation = reconciliation.NewClient(*config) - api.Instruments = instruments.NewClient(*config) - return &api -} diff --git a/client/api_test.go b/client/api_test.go deleted file mode 100644 index 6850543..0000000 --- a/client/api_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package client - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestCreateCheckoutApi(t *testing.T) { - - pk := os.Getenv("CHECKOUT_PUBLIC_KEY") - sk := os.Getenv("CHECKOUT_SECRET_KEY") - - checkoutApi := CheckoutApi(&sk, &pk, checkout.Sandbox) // or Production - assert.NotNil(t, checkoutApi) - assert.NotNil(t, checkoutApi.Payments) - assert.NotNil(t, checkoutApi.Sources) - assert.NotNil(t, checkoutApi.Tokens) - assert.NotNil(t, checkoutApi.Events) - assert.NotNil(t, checkoutApi.Webhooks) - assert.NotNil(t, checkoutApi.Disputes) - assert.NotNil(t, checkoutApi.Files) - assert.NotNil(t, checkoutApi.Reconciliation) - assert.NotNil(t, checkoutApi.Instruments) - -} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..dce9bb8 --- /dev/null +++ b/client/client.go @@ -0,0 +1,191 @@ +package client + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" +) + +type HttpClient interface { + Get(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error + Post(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error + Put(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error + Patch(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}) error + Delete(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error + Upload(path string, authorization *configuration.SdkAuthorization, request *common.FileUploadRequest, responseMapping interface{}) error +} + +type ApiClient struct { + HttpClient http.Client + BaseUri string +} + +const ( + CkoRequestId = "cko-request-id" + CkoVersion = "cko-version" +) + +func NewApiClient(configuration *configuration.Configuration, baseUri string) *ApiClient { + return &ApiClient{ + HttpClient: configuration.HttpClient, + BaseUri: baseUri, + } +} + +func (a *ApiClient) Get(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error { + return a.invoke(http.MethodGet, path, authorization, nil, responseMapping, nil) +} + +func (a *ApiClient) Post(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error { + return a.invoke(http.MethodPost, path, authorization, request, responseMapping, idempotencyKey) +} + +func (a *ApiClient) Put(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error { + return a.invoke(http.MethodPut, path, authorization, request, responseMapping, idempotencyKey) +} + +func (a *ApiClient) Patch(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}) error { + return a.invoke(http.MethodPatch, path, authorization, request, responseMapping, nil) +} + +func (a *ApiClient) Delete(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error { + return a.invoke(http.MethodDelete, path, authorization, nil, responseMapping, nil) +} + +func (a *ApiClient) Upload(path string, authorization *configuration.SdkAuthorization, request *common.FileUploadRequest, responseMapping interface{}) error { + return a.submit(path, authorization, request, responseMapping) +} + +func (a *ApiClient) invoke( + method string, + path string, + authorization *configuration.SdkAuthorization, + request interface{}, + responseMapping interface{}, + idempotencyKey *string, +) error { + body, err := common.Marshal(request) + if err != nil { + return err + } + + req, err := a.buildRequest(method, path, authorization, "application/json", body, idempotencyKey) + if err != nil { + return err + } + + resp, err := a.HttpClient.Do(req) + if err != nil { + return err + } + + return a.handleResponse(resp, responseMapping) +} + +func (a *ApiClient) submit( + path string, + authorization *configuration.SdkAuthorization, + request *common.FileUploadRequest, + responseMapping interface{}, +) error { + req, err := a.buildRequest( + http.MethodPost, + path, + authorization, + request.W.FormDataContentType(), + request.B, + nil, + ) + if err != nil { + return err + } + + resp, err := a.HttpClient.Do(req) + if err != nil { + return err + } + + return a.handleResponse(resp, responseMapping) +} + +func (a *ApiClient) buildRequest( + method string, + path string, + authorization *configuration.SdkAuthorization, + contentType string, + body *bytes.Buffer, + idempotencyKey *string, +) (*http.Request, error) { + req, err := http.NewRequest(method, a.BaseUri+path, body) + if err != nil { + return nil, err + } + + authorizationHeader, err := authorization.GetAuthorizationHeader() + if err != nil { + return nil, err + } + + req.Header = a.getHeaders(contentType, authorizationHeader, idempotencyKey) + + return req, nil +} + +func (a *ApiClient) handleResponse(rawResponse *http.Response, responseMapping interface{}) error { + requestId := rawResponse.Header.Get(CkoRequestId) + version := rawResponse.Header.Get(CkoVersion) + body, err := a.readBody(rawResponse) + if err != nil { + return err + } + + if rawResponse.StatusCode >= http.StatusBadRequest { + return errors.HandleError(rawResponse.StatusCode, rawResponse.Status, requestId, body) + } + + metadata := &common.HttpMetadata{ + Status: rawResponse.Status, + StatusCode: rawResponse.StatusCode, + ResponseBody: body, + Headers: &common.Headers{ + Header: rawResponse.Header, + CKORequestID: &requestId, + CKOVersion: &version, + }, + } + + return common.Unmarshal(metadata, responseMapping) +} + +func (a *ApiClient) getHeaders(contentType string, authorization string, idempotencyKey *string) http.Header { + headers := make(http.Header) + + headers.Set("User-Agent", "checkout-sdk-go/"+SDK_VERSION) + headers.Set("Accept", "application/json") + headers.Set("Content-Type", contentType) + headers.Set("Authorization", authorization) + if idempotencyKey != nil { + headers.Set("Cko-Idempotency-Key", *idempotencyKey) + } + + return headers +} + +func (a *ApiClient) readBody(response *http.Response) ([]byte, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + if errTemp := Body.Close(); errTemp != nil { + err = errTemp + } + }(response.Body) + + return body, err +} diff --git a/client/version.go b/client/version.go new file mode 100644 index 0000000..95fd98e --- /dev/null +++ b/client/version.go @@ -0,0 +1,3 @@ +package client + +const SDK_VERSION = "1.0.0-beta.1" diff --git a/common/address.go b/common/address.go deleted file mode 100644 index 4c91e07..0000000 --- a/common/address.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -// Address - Defines a postal address. -type Address struct { - AddressLine1 string `json:"address_line1,omitempty"` - AddressLine2 string `json:"address_line2,omitempty"` - City string `json:"city,omitempty"` - State string `json:"state,omitempty"` - ZIP string `json:"zip,omitempty"` - Country string `json:"country,omitempty"` -} diff --git a/common/cardcategory.go b/common/cardcategory.go deleted file mode 100644 index b1cddda..0000000 --- a/common/cardcategory.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -// CardCategory ... -type CardCategory string - -const ( - // Consumer ... - Consumer CardCategory = "Consumer" - // Commercial ... - Commercial CardCategory = "Commercial" -) diff --git a/common/cardtype.go b/common/cardtype.go deleted file mode 100644 index 55a5502..0000000 --- a/common/cardtype.go +++ /dev/null @@ -1,15 +0,0 @@ -package common - -// CardType ... -type CardType string - -const ( - // Credit ... - Credit CardType = "Credit" - // Debit ... - Debit CardType = "Debit" - // Prepaid ... - Prepaid CardType = "Prepaid" - // Charge ... - Charge CardType = "Charge" -) diff --git a/common/common.go b/common/common.go new file mode 100644 index 0000000..e89d81a --- /dev/null +++ b/common/common.go @@ -0,0 +1,228 @@ +package common + +import ( + "net/http" +) + +type AccountType string + +const ( + Savings AccountType = "savings" + Current AccountType = "current" + Cash AccountType = "cash" +) + +type CardType string + +const ( + Credit CardType = "Credit" + Debit CardType = "Debit" + Prepaid CardType = "Prepaid" + Charge CardType = "Charge" + DeferredDebit CardType = "Deferred Debit" +) + +type CardCategory string + +const ( + Consumer CardCategory = "Consumer" + Commercial CardCategory = "Commercial" +) + +type AccountHolderType string + +const ( + Individual AccountHolderType = "individual" + Corporate AccountHolderType = "corporate" + Government AccountHolderType = "government" +) + +type ChallengeIndicator string + +const ( + NoPreference ChallengeIndicator = "no_preference" + NoChallengeRequested ChallengeIndicator = "no_challenge_requested" + ChallengeRequested ChallengeIndicator = "challenge_requested" + ChallengeRequestedMandate ChallengeIndicator = "challenge_requested_mandate" +) + +type AccountHolderIdentificationType string + +const ( + Passport AccountHolderIdentificationType = "passport" + DrivingLicence AccountHolderIdentificationType = "driving_licence" + NationalId AccountHolderIdentificationType = "national_id" + CompanyRegistration AccountHolderIdentificationType = "company_registration" + TaxId AccountHolderIdentificationType = "tax_id" +) + +type ThreeDsFlowType string + +const ( + Challenged ThreeDsFlowType = "challenged" + Frictionless ThreeDsFlowType = "frictionless" + FrictionlessDelegated ThreeDsFlowType = "frictionless_delegated" +) + +type Exemption string + +const ( + None Exemption = "none" + LowValue Exemption = "low_value" + RecurringOperation Exemption = "recurring_operation" + TransactionRiskAssessment Exemption = "transaction_risk_assessment" + SecureCorporatePayment Exemption = "secure_corporate_payment" + TrustedListing Exemption = "trusted_listing" + ThreeDsOutage Exemption = "3ds_outage" + ScaDelegation Exemption = "sca_delegation" + OutOfScaScope Exemption = "out_of_sca_scope" + Other Exemption = "other" + LowRiskProgram Exemption = "low_risk_program" +) + +type ThreeDsMethodCompletion string + +const ( + Y ThreeDsMethodCompletion = "y" + N ThreeDsMethodCompletion = "n" + U ThreeDsMethodCompletion = "u" +) + +type ( + Address struct { + AddressLine1 string `json:"address_line1,omitempty"` + AddressLine2 string `json:"address_line2,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Zip string `json:"zip,omitempty"` + Country Country `json:"country,omitempty"` + } + + Phone struct { + CountryCode string `json:"country_code,omitempty"` + Number string `json:"number,omitempty"` + } + + BankDetails struct { + Name string `json:"name,omitempty"` + Branch string `json:"branch,omitempty"` + Address *Address `json:"address,omitempty"` + } +) + +type ( + IdResponse struct { + HttpMetadata HttpMetadata + Id string `json:"id,omitempty"` + Links map[string]Link `json:"_links"` + } + + MetadataResponse struct { + HttpMetadata HttpMetadata + } + + HttpMetadata struct { + Status string `json:"status,omitempty"` + StatusCode int `json:"status_code,omitempty"` + ResponseBody []byte `json:"response_body,omitempty"` + ResponseCSV [][]string `json:"response_csv,omitempty"` + Headers *Headers `json:"headers,omitempty"` + } + + AlternativeResponse map[string]interface{} + + Headers struct { + Header http.Header + CKORequestID *string `json:"cko-request-id,omitempty"` + CKOVersion *string `json:"cko-version,omitempty"` + } + + Link struct { + HRef *string `json:"href,omitempty"` + Title *string `json:"title,omitempty"` + } +) + +type ( + AccountHolderIdentification struct { + Type AccountHolderIdentificationType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + IssuingCountry Country `json:"issuing_country,omitempty"` + DateOfExpiry AccountHolderType `json:"date_of_expiry,omitempty"` + } + + AccountHolder struct { + Type AccountHolderType `json:"type,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + CompanyName string `json:"company_name,omitempty"` + TaxId string `json:"tax_id,omitempty"` + DateOfBirth string `json:"date_of_birth,omitempty"` + CountryOfBirth string `json:"country_of_birth,omitempty"` + ResidentialStatus string `json:"residential_status,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + Phone *Phone `json:"phone,omitempty"` + Identification *AccountHolderIdentification `json:"identification,omitempty"` + Email string `json:"email,omitempty"` + Gender string `json:"gender,omitempty"` + } +) + +type ( + InstrumentDetails struct { + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + InstrumentCustomerResponse *InstrumentCustomerResponse `json:"customer,omitempty"` + AccountHolder *AccountHolder `json:"account_holder,omitempty"` + } + + InstrumentCustomerResponse struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *Phone `json:"phone,omitempty"` + Default bool `json:"nas,omitempty"` + } +) + +type ( + CustomerRequest struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + TaxNumber string `json:"tax_number,omitempty"` + Phone *Phone `json:"phone,omitempty"` + Default bool `json:"default,omitempty"` + } + + CustomerResponse struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *Phone `json:"phone,omitempty"` + } + + UpdateCustomerRequest struct { + Id string `json:"id,omitempty"` + Default bool `json:"nas,omitempty"` + } +) + +type ( + MarketplaceData struct { + SubEntityId string `json:"sub_entity_id,omitempty"` + SubEntities []AmountAllocations `json:"sub_entities,omitempty"` + } + + AmountAllocations struct { + Id string `json:"id,omitempty"` + Amount int `json:"amount,omitempty"` + Reference string `json:"reference,omitempty"` + Commission *Commission `json:"commission,omitempty"` + } + + Commission struct { + Amount int `json:"amount,omitempty"` + Percentage float32 `json:"percentage,omitempty"` + } +) diff --git a/common/country.go b/common/country.go new file mode 100644 index 0000000..8af7a4a --- /dev/null +++ b/common/country.go @@ -0,0 +1,259 @@ +package common + +type Country string + +const ( + AF Country = "AF" + AX Country = "AX" + AL Country = "AL" + DZ Country = "DZ" + AS Country = "AS" + AD Country = "AD" + AO Country = "AO" + AI Country = "AI" + AG Country = "AG" + AR Country = "AR" + AM Country = "AM" + AW Country = "AW" + AC Country = "AC" + AU Country = "AU" + AQ Country = "AQ" + AT Country = "AT" + AZ Country = "AZ" + BS Country = "BS" + BH Country = "BH" + BD Country = "BD" + BB Country = "BB" + BY Country = "BY" + BE Country = "BE" + BZ Country = "BZ" + BJ Country = "BJ" + BM Country = "BM" + BT Country = "BT" + BO Country = "BO" + BQ Country = "BQ" + BA Country = "BA" + BW Country = "BW" + BV Country = "BV" + BR Country = "BR" + IO Country = "IO" + VG Country = "VG" + BN Country = "BN" + BG Country = "BG" + BF Country = "BF" + BI Country = "BI" + KH Country = "KH" + CM Country = "CM" + CA Country = "CA" + CV Country = "CV" + KY Country = "KY" + CF Country = "CF" + TD Country = "TD" + CL Country = "CL" + CN Country = "CN" + TW Country = "TW" + CX Country = "CX" + PF Country = "PF" + CC Country = "CC" + CO Country = "CO" + KM Country = "KM" + CG Country = "CG" + CD Country = "CD" + CK Country = "CK" + CR Country = "CR" + CI Country = "CI" + HR Country = "HR" + CU Country = "CU" + CW Country = "CW" + CY Country = "CY" + CZ Country = "CZ" + DK Country = "DK" + DJ Country = "DJ" + DM Country = "DM" + DO Country = "DO" + EC Country = "EC" + EG Country = "EG" + SV Country = "SV" + GQ Country = "GQ" + ER Country = "ER" + EE Country = "EE" + SZ Country = "SZ" + ET Country = "ET" + FK Country = "FK" + FO Country = "FO" + FJ Country = "FJ" + FI Country = "FI" + FR Country = "FR" + GF Country = "GF" + TF Country = "TF" + GA Country = "GA" + GM Country = "GM" + GE Country = "GE" + DE Country = "DE" + GH Country = "GH" + GI Country = "GI" + GR Country = "GR" + GL Country = "GL" + GD Country = "GD" + GP Country = "GP" + GU Country = "GU" + GT Country = "GT" + GG Country = "GG" + GN Country = "GN" + GW Country = "GW" + GY Country = "GY" + HT Country = "HT" + HM Country = "HM" + HN Country = "HN" + HK Country = "HK" + HU Country = "HU" + IS Country = "IS" + IN Country = "IN" + ID Country = "ID" + IR Country = "IR" + IQ Country = "IQ" + IE Country = "IE" + IM Country = "IM" + IL Country = "IL" + IT Country = "IT" + JM Country = "JM" + JP Country = "JP" + JE Country = "JE" + JO Country = "JO" + KZ Country = "KZ" + KE Country = "KE" + KI Country = "KI" + KP Country = "KP" + KR Country = "KR" + KW Country = "KW" + KG Country = "KG" + LA Country = "LA" + LV Country = "LV" + LB Country = "LB" + LS Country = "LS" + LR Country = "LR" + LY Country = "LY" + LI Country = "LI" + LT Country = "LT" + LU Country = "LU" + MO Country = "MO" + MK Country = "MK" + MG Country = "MG" + MW Country = "MW" + MY Country = "MY" + MV Country = "MV" + ML Country = "ML" + MT Country = "MT" + MH Country = "MH" + MQ Country = "MQ" + MR Country = "MR" + MU Country = "MU" + YT Country = "YT" + MX Country = "MX" + FM Country = "FM" + MD Country = "MD" + MC Country = "MC" + MN Country = "MN" + ME Country = "ME" + MS Country = "MS" + MA Country = "MA" + MZ Country = "MZ" + MM Country = "MM" + NA Country = "NA" + NR Country = "NR" + NP Country = "NP" + NL Country = "NL" + AN Country = "AN" + NC Country = "NC" + NZ Country = "NZ" + NI Country = "NI" + NE Country = "NE" + NG Country = "NG" + NU Country = "NU" + NF Country = "NF" + MP Country = "MP" + NO Country = "NO" + OM Country = "OM" + PK Country = "PK" + PW Country = "PW" + PA Country = "PA" + PG Country = "PG" + PY Country = "PY" + PE Country = "PE" + PH Country = "PH" + PN Country = "PN" + PL Country = "PL" + PT Country = "PT" + PR Country = "PR" + QA Country = "QA" + RE Country = "RE" + RO Country = "RO" + RU Country = "RU" + RW Country = "RW" + BL Country = "BL" + SH Country = "SH" + KN Country = "KN" + LC Country = "LC" + MF Country = "MF" + PM Country = "PM" + VC Country = "VC" + WS Country = "WS" + SM Country = "SM" + ST Country = "ST" + SA Country = "SA" + SN Country = "SN" + RS Country = "RS" + SC Country = "SC" + SL Country = "SL" + SG Country = "SG" + SX Country = "SX" + SK Country = "SK" + SI Country = "SI" + SB Country = "SB" + SO Country = "SO" + ZA Country = "ZA" + GS Country = "GS" + SS Country = "SS" + ES Country = "ES" + LK Country = "LK" + SD Country = "SD" + SR Country = "SR" + SJ Country = "SJ" + SE Country = "SE" + CH Country = "CH" + SY Country = "SY" + TJ Country = "TJ" + TZ Country = "TZ" + TH Country = "TH" + TL Country = "TL" + TG Country = "TG" + TK Country = "TK" + TO Country = "TO" + TT Country = "TT" + TA Country = "TA" + TN Country = "TN" + TR Country = "TR" + TM Country = "TM" + TC Country = "TC" + TV Country = "TV" + VI Country = "VI" + UG Country = "UG" + UA Country = "UA" + AE Country = "AE" + GB Country = "GB" + US Country = "US" + UY Country = "UY" + UZ Country = "UZ" + VU Country = "VU" + VA Country = "VA" + VE Country = "VE" + VN Country = "VN" + UM Country = "UM" + WF Country = "WF" + EH Country = "EH" + YE Country = "YE" + ZM Country = "ZM" + ZW Country = "ZW" + PS Country = "PS" + QZ Country = "QZ" +) diff --git a/common/currency.go b/common/currency.go index ddbc87f..693201c 100644 --- a/common/currency.go +++ b/common/currency.go @@ -1,482 +1,166 @@ package common -const ( - // ALL - Lek - ALL string = "ALL" - - // STN - Dobra - STN string = "STN" - - // EEK - Kroon - EEK string = "EEK" - - // BHD - Bahraini Dinar - BHD string = "BHD" - - // SCR - Seychelles Rupee - SCR string = "SCR" - - // DJF - Djibouti Franc - DJF string = "DJF" - - // EGP - Egyptian Pound - EGP string = "EGP" - - // MDL - Moldovan Leu - MDL string = "MDL" - - // MZN - Metical - MZN string = "MZN" - - // BND - Brunei Dollar - BND string = "BND" - - // ZMK - Zambian Kwacha - ZMK string = "ZMK" - - // SHP - Saint Helena Pound - SHP string = "SHP" - - // LBP - Lebanese Pound - LBP string = "LBP" - - // AWG - Aruban Guilder - AWG string = "AWG" - - // JMD - Jamaican Dollar - JMD string = "JMD" - - // KES - Kenyan Shilling - KES string = "KES" - - // BYN - Belarussian Ruble - BYN string = "BYN" - - // KHR - Riel - KHR string = "KHR" - - // LAK - Kip - LAK string = "LAK" - - // MVR - Rufiyaa - MVR string = "MVR" - - // AOA - Kwanza - AOA string = "AOA" - - // TJS - Somoni - TJS string = "TJS" - - // SVC - El Salvador Colon - SVC string = "SVC" - - // GNF - Guinea Franc - GNF string = "GNF" - - // BRL - Brazilian Real - BRL string = "BRL" - - // MOP - Pataca - MOP string = "MOP" - - // BOB - Boliviano - BOB string = "BOB" - - // CDF - Congolese Franc - CDF string = "CDF" - - // NAD - Namibia Dollar - NAD string = "NAD" - - // LYD - Libyan Dinar - LYD string = "LYD" - - // VUV - Vatu - VUV string = "VUV" - - // QAR - Qatari Rial - QAR string = "QAR" - - // CLP - Chilean Peso - CLP string = "CLP" - - // HRK - Croatian Kuna - HRK string = "HRK" - - // ISK - Iceland Krona - ISK string = "ISK" - - // FKP - Falkland Islands Pound - FKP string = "FKP" - - // XCD - East Caribbean Dollar - XCD string = "XCD" - - // NOK - Norwegian Krone - NOK string = "NOK" - - // CUP - Cuban Peso - CUP string = "CUP" - - // VND - Dong - VND string = "VND" - - // PEN - Nuevo Sol - PEN string = "PEN" - - // KMF - Comoro Franc - KMF string = "KMF" - - // LVL - Latvian Lats - LVL string = "LVL" - - // MMK - Kyat - MMK string = "MMK" - - // TRY - Turkish Lira - TRY string = "TRY" - - // VEF - Bolivar Fuerte - VEF string = "VEF" - - // AUD - Australian Dollar - AUD string = "AUD" - - // TWD - New Taiwan Dollar - TWD string = "TWD" - - // PKR - Pakistan Rupee - PKR string = "PKR" - - // SLL - Leone - SLL string = "SLL" - - // BGN - Bulgarian Lev - BGN string = "BGN" - - // LRD - Liberian Dollar - LRD string = "LRD" - - // LKR - Sri Lanka Rupee - LKR string = "LKR" - - // XAF - CFA Franc BEAC - XAF string = "XAF" - - // JOD - Jordanian Dinar - JOD string = "JOD" - - // ANG - Netherlands Antillian Guilder - ANG string = "ANG" - - // BSD - Bahamian Dollar - BSD string = "BSD" - - // CAD - Canadian Dollar - CAD string = "CAD" - - // GIP - Gibraltar Pound - GIP string = "GIP" - - // MNT - Tugrik - MNT string = "MNT" - - // LTL - Lithuanian Litas - LTL string = "LTL" - - // BBD - Barbados Dollar - BBD string = "BBD" - - // CLF - Unidades de fomento - CLF string = "CLF" - - // BWP - Pula - BWP string = "BWP" - - // COP - Colombian Peso - COP string = "COP" - - // PHP - Philippine Peso - PHP string = "PHP" - - // HUF - Forint - HUF string = "HUF" - - // FJD - Fiji Dollar - FJD string = "FJD" - - // MWK - Kwacha - MWK string = "MWK" - - // THB - Baht - THB string = "THB" - - // XPF - CFP Franc - XPF string = "XPF" - - // RSD - Serbian Dinar - RSD string = "RSD" - - // SAR - Saudi Riyal - SAR string = "SAR" - - // UYU - Peso Uruguayo - UYU string = "UYU" - - // BZD - Belize Dollar - BZD string = "BZD" - - // SYP - Syrian Pound - SYP string = "SYP" - - // GMD - Dalasi - GMD string = "GMD" - - // SZL - Lilangeni - SZL string = "SZL" - - // SBD - Solomon Islands Dollar - SBD string = "SBD" - - // ETB - Ethiopian Birr - ETB string = "ETB" +type Currency string - // CHF - Swiss Franc - CHF string = "CHF" - - // MXN - Mexican Peso - MXN string = "MXN" - - // ARS - Argentine Peso - ARS string = "ARS" - - // GTQ - Quetzal - GTQ string = "GTQ" - - // GHS - Cedi - GHS string = "GHS" - - // NIO - Cordoba Oro - NIO string = "NIO" - - // JPY - Yen - JPY string = "JPY" - - // BDT - Taka - BDT string = "BDT" - - // UZS - Uzbekistan Sum - UZS string = "UZS" - - // SOS - Somali Shilling - SOS string = "SOS" - - // BTN - Ngultrum - BTN string = "BTN" - - // NZD - New Zealand Dollar - NZD string = "NZD" - - // TZS - Tanzanian Shilling - TZS string = "TZS" - - // IQD - Iraqi Dinar - IQD string = "IQD" - - // MGA - Malagasy Ariary - MGA string = "MGA" - - // DZD - Algerian Dinar - DZD string = "DZD" - - // GYD - Guyana Dollar - GYD string = "GYD" - - // USD - US Dollar - USD string = "USD" - - // KWD - Kuwaiti Dinar - KWD string = "KWD" - - // CNY - Yuan Renminbi - CNY string = "CNY" - - // PYG - Guarani - PYG string = "PYG" - - // SGD - Singapore Dollar - SGD string = "SGD" - - // KZT - Tenge - KZT string = "KZT" - - // PGK - Kina - PGK string = "PGK" - - // AMD - Armenian Dram - AMD string = "AMD" - - // GBP - Pound Sterling - GBP string = "GBP" - - // AFN - Afghani - AFN string = "AFN" - - // CRC - Costa Rican Colon - CRC string = "CRC" - - // XOF - CFA Franc BCEAO - XOF string = "XOF" - - // YER - Yemeni Rial - YER string = "YER" - - // MRU - Ouguiya - MRU string = "MRU" - - // DKK - Danish Krone - DKK string = "DKK" - - // TOP - Paanga - TOP string = "TOP" - - // INR - Indian Rupee - INR string = "INR" - - // SDG - Sudanese Pound - SDG string = "SDG" - - // DOP - Dominican Peso - DOP string = "DOP" - - // ZWL - Zimbabwe Dollar - ZWL string = "ZWL" - - // UGX - Uganda Shilling - UGX string = "UGX" - - // SEK - Swedish Krona - SEK string = "SEK" - - // LSL - Loti - LSL string = "LSL" - - // MYR - Malaysian Ringgit - MYR string = "MYR" - - // TMT - Manat - TMT string = "TMT" - - // OMR - Rial Omani - OMR string = "OMR" - - // BMD - Bermudian Dollar - BMD string = "BMD" - - // KRW - Won - KRW string = "KRW" - - // HKD - Hong Kong Dollar - HKD string = "HKD" - - // KGS - Som - KGS string = "KGS" - - // BAM - Convertible Marks - BAM string = "BAM" - - // NGN - Naira - NGN string = "NGN" - - // ILS - New Israeli Sheqel - ILS string = "ILS" - - // MUR - Mauritius Rupee - MUR string = "MUR" - - // RON - New Leu - RON string = "RON" - - // TND - Tunisian Dinar - TND string = "TND" - - // AED - UAE Dirham - AED string = "AED" - - // PAB - Balboa - PAB string = "PAB" - - // NPR - Nepalese Rupee - NPR string = "NPR" - - // TTD - Trinidad and Tobago Dollar - TTD string = "TTD" - - // RWF - Rwanda Franc - RWF string = "RWF" - - // HTG - Gourde - HTG string = "HTG" - - // IDR - Rupiah - IDR string = "IDR" - - // EUR - Euro - EUR string = "EUR" - - // KYD - Cayman Islands Dollar - KYD string = "KYD" - - // IRR - Iranian Rial - IRR string = "IRR" - - // KPW - North Korean Won - KPW string = "KPW" - - // MKD - Denar - MKD string = "MKD" - - // SRD - Surinam Dollar - SRD string = "SRD" - - // HNL - Lempira - HNL string = "HNL" - - // AZN - Azerbaijanian Manat - AZN string = "AZN" - - // ERN - Nakfa - ERN string = "ERN" - - // CZK - Czech Koruna - CZK string = "CZK" - - // CVE - Cape Verde Escudo - CVE string = "CVE" - - // BIF - Burundi Franc - BIF string = "BIF" - // MAD - Moroccan Dirham - MAD string = "MAD" - - // RUB - Russian Ruble - RUB string = "RUB" - - // UAH - Hryvnia - UAH string = "UAH" - - // WST - Tala - WST string = "WST" - - // PLN - Zloty - PLN string = "PLN" - - // ZAR - Rand - ZAR string = "ZAR" - - // GEL - Lari - GEL string = "GEL" - - // ZMW - Zambian kwacha - ZMW string = "ZMW" +const ( + AED Currency = "AED" + AFN Currency = "AFN" + ALL Currency = "ALL" + AMD Currency = "AMD" + ANG Currency = "ANG" + AOA Currency = "AOA" + ARS Currency = "ARS" + AUD Currency = "AUD" + AWG Currency = "AWG" + AZN Currency = "AZN" + BAM Currency = "BAM" + BBD Currency = "BBD" + BDT Currency = "BDT" + BGN Currency = "BGN" + BHD Currency = "BHD" + BIF Currency = "BIF" + BMD Currency = "BMD" + BND Currency = "BND" + BOB Currency = "BOB" + BRL Currency = "BRL" + BSD Currency = "BSD" + BTN Currency = "BTN" + BWP Currency = "BWP" + BYN Currency = "BYN" + BZD Currency = "BZD" + CAD Currency = "CAD" + CDF Currency = "CDF" + CHF Currency = "CHF" + CLF Currency = "CLF" + CLP Currency = "CLP" + CNY Currency = "CNY" + COP Currency = "COP" + CRC Currency = "CRC" + CUP Currency = "CUP" + CVE Currency = "CVE" + CZK Currency = "CZK" + DJF Currency = "DJF" + DKK Currency = "DKK" + DOP Currency = "DOP" + DZD Currency = "DZD" + EEK Currency = "EEK" + EGP Currency = "EGP" + ERN Currency = "ERN" + ETB Currency = "ETB" + EUR Currency = "EUR" + FJD Currency = "FJD" + FKP Currency = "FKP" + GBP Currency = "GBP" + GEL Currency = "GEL" + GHS Currency = "GHS" + GIP Currency = "GIP" + GMD Currency = "GMD" + GNF Currency = "GNF" + GTQ Currency = "GTQ" + GYD Currency = "GYD" + HKD Currency = "HKD" + HNL Currency = "HNL" + HRK Currency = "HRK" + HTG Currency = "HTG" + HUF Currency = "HUF" + IDR Currency = "IDR" + ILS Currency = "ILS" + INR Currency = "INR" + IQD Currency = "IQD" + IRR Currency = "IRR" + ISK Currency = "ISK" + JMD Currency = "JMD" + JOD Currency = "JOD" + JPY Currency = "JPY" + KES Currency = "KES" + KGS Currency = "KGS" + KHR Currency = "KHR" + KMF Currency = "KMF" + KPW Currency = "KPW" + KRW Currency = "KRW" + KWD Currency = "KWD" + KYD Currency = "KYD" + KZT Currency = "KZT" + LAK Currency = "LAK" + LBP Currency = "LBP" + LKR Currency = "LKR" + LRD Currency = "LRD" + LSL Currency = "LSL" + LTL Currency = "LTL" + LVL Currency = "LVL" + LYD Currency = "LYD" + MAD Currency = "MAD" + MDL Currency = "MDL" + MGA Currency = "MGA" + MKD Currency = "MKD" + MMK Currency = "MMK" + MNT Currency = "MNT" + MOP Currency = "MOP" + MRU Currency = "MRU" + MUR Currency = "MUR" + MVR Currency = "MVR" + MWK Currency = "MWK" + MXN Currency = "MXN" + MYR Currency = "MYR" + MZN Currency = "MZN" + NAD Currency = "NAD" + NGN Currency = "NGN" + NIO Currency = "NIO" + NOK Currency = "NOK" + NPR Currency = "NPR" + NZD Currency = "NZD" + OMR Currency = "OMR" + PAB Currency = "PAB" + PEN Currency = "PEN" + PGK Currency = "PGK" + PHP Currency = "PHP" + PKR Currency = "PKR" + PLN Currency = "PLN" + PYG Currency = "PYG" + QAR Currency = "QAR" + RON Currency = "RON" + RSD Currency = "RSD" + RUB Currency = "RUB" + RWF Currency = "RWF" + SAR Currency = "SAR" + SBD Currency = "SBD" + SCR Currency = "SCR" + SDG Currency = "SDG" + SEK Currency = "SEK" + SGD Currency = "SGD" + SHP Currency = "SHP" + SLL Currency = "SLL" + SOS Currency = "SOS" + SRD Currency = "SRD" + STN Currency = "STN" + SVC Currency = "SVC" + SYP Currency = "SYP" + SZL Currency = "SZL" + THB Currency = "THB" + TJS Currency = "TJS" + TMT Currency = "TMT" + TND Currency = "TND" + TOP Currency = "TOP" + TRY Currency = "TRY" + TTD Currency = "TTD" + TWD Currency = "TWD" + TZS Currency = "TZS" + UAH Currency = "UAH" + UGX Currency = "UGX" + USD Currency = "USD" + UYU Currency = "UYU" + UZS Currency = "UZS" + VEF Currency = "VEF" + VND Currency = "VND" + VUV Currency = "VUV" + WST Currency = "WST" + XAF Currency = "XAF" + XCD Currency = "XCD" + XOF Currency = "XOF" + XPF Currency = "XPF" + YER Currency = "YER" + ZAR Currency = "ZAR" + ZMK Currency = "ZMK" + ZMW Currency = "ZMW" + ZWL Currency = "ZWL" ) diff --git a/common/errorresponse.go b/common/errorresponse.go deleted file mode 100644 index d8ab42b..0000000 --- a/common/errorresponse.go +++ /dev/null @@ -1,19 +0,0 @@ -package common - -// ErrorDetails ... -type ErrorDetails struct { - RequestID string `json:"request_id,omitempty"` - ErrorType string `json:"error_type,omitempty"` - ErrorCodes []string `json:"error_codes,omitempty"` -} - -// Error ... -type Error struct { - Data *ErrorDetails - Status string - StatusCode int -} - -func (e *Error) Error() string { - return e.Status -} diff --git a/common/files.go b/common/files.go new file mode 100644 index 0000000..707484e --- /dev/null +++ b/common/files.go @@ -0,0 +1,131 @@ +package common + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/textproto" + "os" + "path/filepath" + "time" + + "github.com/gabriel-vasile/mimetype" + + "github.com/checkout/checkout-sdk-go/errors" +) + +type Purpose string + +const ( + // Disputes + DisputesEvidence Purpose = "dispute_evidence" + + // Accounts + BankVerification Purpose = "bank_verification" + IdentityVerification Purpose = "identity_verification" + CompanyVerification Purpose = "company_verification" + FinancialVerification Purpose = "financial_verification" +) + +type ( + FileUpload interface { + GetFile() string + GetPurpose() Purpose + GetFieldName() string + } + + File struct { + File string + Purpose Purpose + } + + FileUploadRequest struct { + W *multipart.Writer + B *bytes.Buffer + } + + FileResponse struct { + HttpMetadata HttpMetadata + Id string `json:"id,omitempty"` + Filename string `json:"filename,omitempty"` + Purpose Purpose `json:"purpose,omitempty"` + Size uint64 `json:"size,omitempty"` + UploadedOn time.Time `json:"uploaded_on,omitempty"` + Links map[string]Link `json:"_links"` + } +) + +func BuildFileUploadRequest(upload FileUpload) (*FileUploadRequest, error) { + if err := validateFile(upload); err != nil { + return nil, err + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + defer writer.Close() + + file, err := os.Open(upload.GetFile()) + if err != nil { + return nil, err + } + defer file.Close() + + contentType, err := mimetype.DetectFile(upload.GetFile()) + if err != nil { + return nil, err + } + + part, err := createFormFile(writer, upload.GetFieldName(), filepath.Base(file.Name()), contentType.String()) + if err != nil { + return nil, err + } + + _, err = io.Copy(part, file) + if err != nil { + return nil, err + } + + err = writer.WriteField("purpose", string(upload.GetPurpose())) + if err != nil { + return nil, err + } + + return &FileUploadRequest{ + W: writer, + B: body, + }, nil +} + +func validateFile(f FileUpload) error { + if f.GetFile() == "" { + return errors.BadRequestError("Invalid file name") + } + if f.GetPurpose() == "" { + return errors.BadRequestError("Invalid purpose") + } + + return nil +} + +func createFormFile(w *multipart.Writer, fieldName string, fileName string, contentType string) (io.Writer, error) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + EscapeQuotes(fieldName), + EscapeQuotes(fileName))) + h.Set("Content-Type", EscapeQuotes(contentType)) + + return w.CreatePart(h) +} + +func (f *File) GetFile() string { + return f.File +} + +func (f *File) GetPurpose() Purpose { + return f.Purpose +} + +func (f *File) GetFieldName() string { + return "file" +} diff --git a/common/link.go b/common/link.go deleted file mode 100644 index 79b6caa..0000000 --- a/common/link.go +++ /dev/null @@ -1,7 +0,0 @@ -package common - -// Link ... -type Link struct { - HRef string `json:"href,omitempty"` - Title string `json:"title,omitempty"` -} diff --git a/common/paymentaction.go b/common/paymentaction.go deleted file mode 100644 index d3e1bbf..0000000 --- a/common/paymentaction.go +++ /dev/null @@ -1,23 +0,0 @@ -package common - -// PaymentStatus ... -type PaymentAction string - -const ( - // Authorized ... - Authorized PaymentAction = "Authorized" - // Pending ... - Pending PaymentAction = "Pending" - // CardVerified ... - CardVerified PaymentAction = "Card Verified" - // Captured ... - Captured PaymentAction = "Captured" - // Declined ... - Declined PaymentAction = "Declined" - // Paid ... - Paid PaymentAction = "Paid" -) - -func (c PaymentAction) String() string { - return string(c) -} diff --git a/common/paymenttype.go b/common/paymenttype.go deleted file mode 100644 index 6c36940..0000000 --- a/common/paymenttype.go +++ /dev/null @@ -1,17 +0,0 @@ -package common - -// PaymentType ... -type PaymentType string - -const ( - // Regular ... - Regular PaymentType = "Regular" - // Recurring ... - Recurring PaymentType = "Recurring" - // MOTO ... - MOTO PaymentType = "MOTO" -) - -func (c PaymentType) String() string { - return string(c) -} diff --git a/common/phone.go b/common/phone.go deleted file mode 100644 index 63fe47c..0000000 --- a/common/phone.go +++ /dev/null @@ -1,7 +0,0 @@ -package common - -// Phone ... -type Phone struct { - CountryCode string `json:"country_code,omitempty"` - Number string `json:"number,omitempty"` -} diff --git a/common/regex.go b/common/regex.go deleted file mode 100644 index 1658296..0000000 --- a/common/regex.go +++ /dev/null @@ -1,18 +0,0 @@ -package common - -const ( - // LiveSecretKeyRegex ... - LiveSecretKeyRegex = `^sk_?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$` - // LivePublicKeyRegex ... - LivePublicKeyRegex = `^pk_?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$` - // SandboxSecretKeyRegex ... - SandboxSecretKeyRegex = `^sk_test_?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$` - // SandboxPublicKeyRegex ... - SandboxPublicKeyRegex = `^pk_test_?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$` - FourKeyRegex = `^(pk|sk)_(sbox_)?[a-z2-7]{26}[a-z2-7*#$=]$` - FourOAuthJwtPattern = `^([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)` - // CardTokenRegex ... - CardTokenRegex = `^(tok)_(\w{26})$` - // SourceIDRegex ... - SourceIDRegex = `^(src)_(\w{26})$` -) diff --git a/common/serializer.go b/common/serializer.go new file mode 100644 index 0000000..09dde99 --- /dev/null +++ b/common/serializer.go @@ -0,0 +1,43 @@ +package common + +import ( + "bytes" + "encoding/json" + "reflect" +) + +type ( + TypeMapping struct { + Type string `json:"type"` + } +) + +func Marshal(request interface{}) (*bytes.Buffer, error) { + if request != nil { + marshal, err := json.Marshal(request) + return bytes.NewBuffer(marshal), err + } + return new(bytes.Buffer), nil +} + +func Unmarshal(metadata *HttpMetadata, responseMapping interface{}) error { + if len(metadata.ResponseBody) == 0 { + addHttpMetadata(metadata, responseMapping) + return nil + } + + if err := json.Unmarshal(metadata.ResponseBody, &responseMapping); err != nil { + return err + } + + addHttpMetadata(metadata, responseMapping) + + return nil +} + +func addHttpMetadata(metadata *HttpMetadata, response interface{}) { + v := reflect.ValueOf(response).Elem().FieldByName("HttpMetadata") + if v.IsValid() { + v.Set(reflect.ValueOf(*metadata)) + } +} diff --git a/common/tokentype.go b/common/tokentype.go deleted file mode 100644 index a4f8ce3..0000000 --- a/common/tokentype.go +++ /dev/null @@ -1,19 +0,0 @@ -package common - -// TokenType ... -type TokenType string - -const ( - // Token ... - Token TokenType = "token" - // Card ... - Card TokenType = "card" - // ApplePay ... - ApplePay TokenType = "ApplePay" - // GooglePay ... - GooglePay TokenType = "GooglePay" -) - -func (c TokenType) String() string { - return string(c) -} diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 0000000..e717e4b --- /dev/null +++ b/common/utils.go @@ -0,0 +1,38 @@ +package common + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/google/go-querystring/query" +) + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func BuildPath(params ...string) string { + var path string + for _, s := range params { + path += "/" + s + } + + return path +} + +func BuildQueryPath(path string, queryValues interface{}) (string, error) { + values, err := query.Values(queryValues) + if err != nil { + return "", err + } + + return fmt.Sprintf("/%s?%s", path, values.Encode()), nil +} + +func EscapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func BuildDefaultClient() *http.Client { + return &http.Client{Timeout: time.Duration(10) * time.Second} +} diff --git a/common/webhookcontenttype.go b/common/webhookcontenttype.go deleted file mode 100644 index 8b7eabe..0000000 --- a/common/webhookcontenttype.go +++ /dev/null @@ -1,15 +0,0 @@ -package common - -// WebhookContentType ... -type WebhookContentType string - -const ( - // JSON ... - JSON WebhookContentType = "json" - // XML ... - XML WebhookContentType = "xml" -) - -func (c WebhookContentType) String() string { - return string(c) -} diff --git a/configuration/configuration.go b/configuration/configuration.go new file mode 100644 index 0000000..21789df --- /dev/null +++ b/configuration/configuration.go @@ -0,0 +1,29 @@ +package configuration + +import ( + "net/http" + + "github.com/checkout/checkout-sdk-go/common" +) + +type Configuration struct { + Credentials SdkCredentials + Environment Environment + HttpClient http.Client +} + +func NewConfiguration(credentials SdkCredentials, environment Environment, client *http.Client) *Configuration { + if environment == nil { + environment = Sandbox() + } + + if client == nil { + client = common.BuildDefaultClient() + } + + return &Configuration{ + Credentials: credentials, + Environment: environment, + HttpClient: *client, + } +} diff --git a/configuration/default_keys_credentials.go b/configuration/default_keys_credentials.go new file mode 100644 index 0000000..8117976 --- /dev/null +++ b/configuration/default_keys_credentials.go @@ -0,0 +1,31 @@ +package configuration + +import "github.com/checkout/checkout-sdk-go/errors" + +type DefaultKeysSdkCredentials struct { + StaticKeys +} + +func NewDefaultKeysSdkCredentials(secretKey string, publicKey string) *DefaultKeysSdkCredentials { + return &DefaultKeysSdkCredentials{StaticKeys{ + SecretKey: secretKey, + PublicKey: publicKey, + }} +} + +func (f *DefaultKeysSdkCredentials) GetAuthorization(authorizationType AuthorizationType) (*SdkAuthorization, error) { + switch authorizationType { + case SecretKey, SecretKeyOrOauth: + return &SdkAuthorization{ + PlatformType: Default, + Credential: f.SecretKey, + }, nil + case PublicKey, PublicKeyOrOauth: + return &SdkAuthorization{ + PlatformType: Default, + Credential: f.PublicKey, + }, nil + default: + return nil, errors.CheckoutAuthorizationError("Invalid authorization type") + } +} diff --git a/configuration/environment.go b/configuration/environment.go new file mode 100644 index 0000000..723652d --- /dev/null +++ b/configuration/environment.go @@ -0,0 +1,79 @@ +package configuration + +type Environment interface { + BaseUri() string + AuthorizationUri() string + FilesUri() string + TransfersUri() string + BalancesUri() string + IsSandbox() bool +} + +type CheckoutEnv struct { + baseUri string + authorizationUri string + filesUri string + transfersUri string + balancesUri string + isSandbox bool +} + +func (e *CheckoutEnv) BaseUri() string { + return e.baseUri +} + +func (e *CheckoutEnv) AuthorizationUri() string { + return e.authorizationUri +} + +func (e *CheckoutEnv) FilesUri() string { + return e.filesUri +} + +func (e *CheckoutEnv) TransfersUri() string { + return e.transfersUri +} + +func (e *CheckoutEnv) BalancesUri() string { + return e.balancesUri +} + +func (e *CheckoutEnv) IsSandbox() bool { + return e.isSandbox +} + +func NewEnvironment( + baseUri string, + authorizationUri string, + filesUri string, + transfersUri string, + balancesUri string, + isSandbox bool, +) *CheckoutEnv { + return &CheckoutEnv{ + baseUri: baseUri, + authorizationUri: authorizationUri, + filesUri: filesUri, + transfersUri: transfersUri, + balancesUri: balancesUri, + isSandbox: isSandbox} +} + +func Sandbox() *CheckoutEnv { + return NewEnvironment("https://api.sandbox.checkout.com", + "https://access.sandbox.checkout.com/connect/token", + "https://files.sandbox.checkout.com", + "https://transfers.sandbox.checkout.com", + "https://balances.sandbox.checkout.com", + true) +} + +func Production() *CheckoutEnv { + return NewEnvironment( + "https://api.checkout.com", + "https://access.checkout.com/connect/token", + "https://files.checkout.com/", + "https://transfers.checkout.com/", + "https://balances.checkout.com/", + false) +} diff --git a/configuration/key_patterns.go b/configuration/key_patterns.go new file mode 100644 index 0000000..14eace5 --- /dev/null +++ b/configuration/key_patterns.go @@ -0,0 +1,8 @@ +package configuration + +const ( + PreviousSecretKeyPattern string = "^sk_(test_)?(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$" + PreviousPublicKeyPattern string = "^pk_(test_)?(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$" + DefaultSecretKeyPattern string = "^sk_(sbox_)?[a-z2-7]{26}[a-z2-7*#$=]$" + DefaultPublicKeyPattern string = "^pk_(sbox_)?[a-z2-7]{26}[a-z2-7*#$=]$" +) diff --git a/configuration/oauth_keys_credentials.go b/configuration/oauth_keys_credentials.go new file mode 100644 index 0000000..4b859a6 --- /dev/null +++ b/configuration/oauth_keys_credentials.go @@ -0,0 +1,124 @@ +package configuration + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/checkout/checkout-sdk-go/errors" +) + +type ( + OAuthSdkCredentials struct { + //HttpClient + ClientId string + ClientSecret string + AuthorizationUri string + Scopes []string + AccessToken *OAuthAccessToken + } + + OAuthAccessToken struct { + Token string + ExpirationDate time.Time + } + + OAuthServiceResponse struct { + AccessToken string `json:"access_token,omitempty"` + ExpiresIn time.Duration `json:"expires_in,omitempty"` + } +) + +func NewOAuthSdkCredentials(clientId, clientSecret, authorizationUri string, scopes []string) (*OAuthSdkCredentials, error) { + sdkCredentials := OAuthSdkCredentials{ + ClientId: clientId, + ClientSecret: clientSecret, + AuthorizationUri: authorizationUri, + Scopes: scopes, + } + + err := sdkCredentials.GetAccessToken() + if err != nil { + return nil, err + } + + return &sdkCredentials, nil +} + +func (f *OAuthSdkCredentials) GetAuthorization(authorizationType AuthorizationType) (*SdkAuthorization, error) { + switch authorizationType { + case PublicKeyOrOauth, SecretKeyOrOauth, OAuth: + err := f.GetAccessToken() + if err != nil { + return nil, err + } + return &SdkAuthorization{ + PlatformType: DefaultOAuth, + Credential: f.AccessToken.Token, + }, nil + default: + return nil, errors.CheckoutAuthorizationError("Invalid authorization type") + } +} + +func (f *OAuthSdkCredentials) GetAccessToken() error { + if f.AccessToken != nil && f.AccessToken.IsValid() { + return nil + } + + data := url.Values{} + data.Set("client_id", f.ClientId) + data.Set("client_secret", f.ClientSecret) + data.Set("grant_type", "client_credentials") + data.Set("scope", strings.Join(f.Scopes, " ")) + + req, err := http.NewRequest(http.MethodPost, f.AuthorizationUri, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + client := http.Client{Timeout: time.Duration(5) * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + var oauthResp OAuthServiceResponse + err = json.Unmarshal(body, &oauthResp) + if err != nil { + return err + } + + if resp.StatusCode >= http.StatusBadRequest { + var oauthErr errors.CheckoutOAuthError + err = json.Unmarshal(body, &oauthErr) + if err != nil { + return err + } + return errors.CheckoutAuthorizationError(oauthErr.Description) + } + + accessToken := &OAuthAccessToken{ + Token: oauthResp.AccessToken, + ExpirationDate: time.Now().Add(oauthResp.ExpiresIn * time.Second), + } + f.AccessToken = accessToken + return nil +} + +func (t *OAuthAccessToken) IsValid() bool { + if t.Token == "" { + return false + } + return t.ExpirationDate.After(time.Now()) +} diff --git a/configuration/oauth_scopes.go b/configuration/oauth_scopes.go new file mode 100644 index 0000000..abbfc94 --- /dev/null +++ b/configuration/oauth_scopes.go @@ -0,0 +1,43 @@ +package configuration + +const ( + Vault = "vault" + VaultInstruments = "vault:instruments" + VaultTokenization = "vault:tokenization" + Gateway = "gateway" + GatewayPayment = "gateway:payment" + GatewayPaymentDetails = "gateway:payment-details" + GatewayPaymentAuthorization = "gateway:payment-authorizations" + GatewayPaymentVoids = "gateway:payment-voids" + GatewayPaymentCaptures = "gateway:payment-captures" + GatewayPaymentRefunds = "gateway:payment-refunds" + Fx = "fx" + PayoutsBankDetails = "payouts:bank-details" + SessionsApp = "sessions:app" + SessionsBrowser = "sessions:browser" + Disputes = "disputes" + DisputesView = "disputes:view" + DisputesProvideEvidence = "disputes:provide-evidence" + DisputesAccept = "disputes:accept" + Marketplace = "marketplace" + Accounts = "accounts" + Flow = "flow" + FlowWorkflows = "flow:workflows" + FlowEvents = "flow:events" + Files = "files" + FilesRetrieve = "files:retrieve" + FilesUpload = "files:upload" + FilesDownload = "files:download" + Transfers = "transfers" + TransfersCreate = "transfers:create" + TransfersView = "transfers:view" + Balances = "balances" + BalancesView = "balances:view" + Middleware = "middleware" + MiddlewareGateway = "middleware:gateway" + MiddlewarePaymentContext = "middleware:payment-context" + MiddlewareMerchantsSecret = "middleware:merchants-secret" + MiddlewareMerchantsPublic = "middleware:merchants-public" + Reporting = "reporting" + ReportingView = "reporting:view" +) diff --git a/configuration/previous_keys_credentials.go b/configuration/previous_keys_credentials.go new file mode 100644 index 0000000..b7327bf --- /dev/null +++ b/configuration/previous_keys_credentials.go @@ -0,0 +1,31 @@ +package configuration + +import "github.com/checkout/checkout-sdk-go/errors" + +type PreviousKeysSdkCredentials struct { + StaticKeys +} + +func NewPreviousKeysSdkCredentials(secretKey string, publicKey string) *PreviousKeysSdkCredentials { + return &PreviousKeysSdkCredentials{StaticKeys{ + SecretKey: secretKey, + PublicKey: publicKey, + }} +} + +func (c *PreviousKeysSdkCredentials) GetAuthorization(authorizationType AuthorizationType) (*SdkAuthorization, error) { + switch authorizationType { + case SecretKey, SecretKeyOrOauth: + return &SdkAuthorization{ + PlatformType: Previous, + Credential: c.SecretKey, + }, nil + case PublicKey, PublicKeyOrOauth: + return &SdkAuthorization{ + PlatformType: Previous, + Credential: c.PublicKey, + }, nil + default: + return nil, errors.CheckoutAuthorizationError("Invalid authorization type") + } +} diff --git a/configuration/sdk_builder.go b/configuration/sdk_builder.go new file mode 100644 index 0000000..1bc72d5 --- /dev/null +++ b/configuration/sdk_builder.go @@ -0,0 +1,12 @@ +package configuration + +import "net/http" + +type SdkBuilder struct { + Environment Environment + HttpClient *http.Client +} + +func (s *SdkBuilder) GetConfiguration(string, string) *Configuration { + return new(Configuration) +} diff --git a/configuration/sdk_credentials.go b/configuration/sdk_credentials.go new file mode 100644 index 0000000..73fcdd0 --- /dev/null +++ b/configuration/sdk_credentials.go @@ -0,0 +1,50 @@ +package configuration + +import "github.com/checkout/checkout-sdk-go/errors" + +type PlatformType string + +const ( + Previous PlatformType = "PREVIOUS" + Default PlatformType = "DEFAULT" + DefaultOAuth PlatformType = "DEFAULT_OAUTH" + Custom PlatformType = "CUSTOM" +) + +type AuthorizationType string + +const ( + PublicKey AuthorizationType = "PUBLIC_KEY" + SecretKey AuthorizationType = "SECRET_KEY" + PublicKeyOrOauth AuthorizationType = "PUBLIC_KEY_OR_OAUTH" + SecretKeyOrOauth AuthorizationType = "SECRET_KEY_OR_OAUTH" + OAuth AuthorizationType = "OAUTH" + CustomAuth AuthorizationType = "CUSTOM" +) + +type ( + SdkCredentials interface { + GetAuthorization(authorizationType AuthorizationType) (*SdkAuthorization, error) + } + + SdkAuthorization struct { + PlatformType PlatformType + Credential string + } +) + +func (s *SdkAuthorization) GetAuthorizationHeader() (string, error) { + switch s.PlatformType { + case Previous, Custom: + return s.Credential, nil + case Default, DefaultOAuth: + return "Bearer " + s.Credential, nil + default: + return "", errors.CheckoutAuthorizationError("Invalid platform type") + } +} + +type StaticKeys struct { + SecretKey string + PublicKey string +} diff --git a/configuration/static_keys_builder.go b/configuration/static_keys_builder.go new file mode 100644 index 0000000..54b3276 --- /dev/null +++ b/configuration/static_keys_builder.go @@ -0,0 +1,34 @@ +package configuration + +import ( + "regexp" + + "github.com/checkout/checkout-sdk-go/errors" +) + +type StaticKeysBuilder struct { + SdkBuilder + PublicKey string + SecretKey string +} + +func (s *StaticKeysBuilder) ValidateSecretKey(regex string) error { + re := regexp.MustCompile(regex) + if !re.MatchString(s.SecretKey) { + return errors.CheckoutArgumentError("Invalid secret key") + } + + return nil +} + +func (s *StaticKeysBuilder) ValidatePublicKey(regex string) error { + if len(s.PublicKey) == 0 { + return nil + } + re := regexp.MustCompile(regex) + if !re.MatchString(s.PublicKey) { + return errors.CheckoutArgumentError("Invalid public key") + } + + return nil +} diff --git a/customers/client.go b/customers/client.go index 6e40bbe..ab78f1e 100644 --- a/customers/client.go +++ b/customers/client.go @@ -1,38 +1,79 @@ package customers import ( - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" ) -const path = "customers" - -// Client ... type Client struct { - API checkout.HTTPClient + configuration *configuration.Configuration + apiClient client.HttpClient } -// NewClient ... -func NewClient(config checkout.Config) *Client { +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { return &Client{ - API: httpclient.NewClient(config), + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) Create(request CustomerRequest) (*common.IdResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response common.IdResponse + err = c.apiClient.Post(common.BuildPath(Path), auth, request, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Get(customerId string) (*GetCustomerResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err } + + var response GetCustomerResponse + err = c.apiClient.Get(common.BuildPath(common.BuildPath(Path), customerId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil } -// Update customer details -func (c *Client) Update(customerID string, request *Request) (*Response, error) { - resp, err := c.API.Patch(fmt.Sprintf("/%v/%v", path, customerID), request) - response := &Response{ - StatusResponse: resp, +func (c *Client) Update(customerId string, request CustomerRequest) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Patch(common.BuildPath(Path, customerId), auth, request, &response) + if err != nil { + return nil, err } + + return &response, nil +} + +func (c *Client) Delete(customerId string) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusNoContent { - return response, err + + var response common.MetadataResponse + err = c.apiClient.Delete(common.BuildPath(Path, customerId), auth, &response) + if err != nil { + return nil, err } - return response, err + + return &response, nil } diff --git a/customers/client_test.go b/customers/client_test.go new file mode 100644 index 0000000..87dc80b --- /dev/null +++ b/customers/client_test.go @@ -0,0 +1,478 @@ +package customers + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +var ( + id = "cus_1234" + email = "bruce@wayne-enterprises.com" + name = "Bruce Wayne" + phone = common.Phone{ + CountryCode: "+1", + Number: "415 555 2671", + } +) + +func TestCreate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + idResponse = common.IdResponse{ + HttpMetadata: httpMetadata, + Id: "cus_1234", + } + ) + + cases := []struct { + name string + request CustomerRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*common.IdResponse, error) + }{ + { + name: "when request is correct then create customer", + request: CustomerRequest{ + Email: email, + Name: name, + Phone: &phone, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.IdResponse) + *respMapping = idResponse + }) + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, idResponse.Id, response.Id) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: CustomerRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_required", + }, + }, + }) + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Create(tc.request)) + }) + } +} + +func TestGet(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + customer = GetCustomerResponse{ + HttpMetadata: httpMetadata, + Id: id, + Email: email, + Name: name, + Phone: &phone, + } + ) + + cases := []struct { + name string + customerId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetCustomerResponse, error) + }{ + { + name: "when customer exists then return customer info", + customerId: "cus_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetCustomerResponse) + *respMapping = customer + }) + }, + checker: func(response *GetCustomerResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, customer.Id, response.Id) + assert.Equal(t, customer.Email, response.Email) + assert.Equal(t, customer.Name, response.Name) + assert.Equal(t, customer.Phone, response.Phone) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetCustomerResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when customer not found then return error", + customerId: "cus_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetCustomerResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Get(tc.customerId)) + }) + } +} + +func TestUpdate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + customerId string + request CustomerRequest + getAuthorization func(*mock.Mock) mock.Call + apiPatch func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when request is correct then update customer", + customerId: id, + request: CustomerRequest{ + Email: email, + Name: name, + Phone: &phone, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + customerId: id, + request: CustomerRequest{ + Email: email, + Name: name, + Phone: &phone, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when customer not found then return error", + customerId: "not_found", + request: CustomerRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + customerId: id, + request: CustomerRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_invalid", + }, + }, + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPatch(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Update(tc.customerId, tc.request)) + }) + } +} + +func TestDelete(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + customerId string + getAuthorization func(*mock.Mock) mock.Call + apiDelete func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when request is correct then delete customer", + customerId: id, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + customerId: id, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when customer not found then return error", + customerId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiDelete(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Delete(tc.customerId)) + }) + } +} diff --git a/customers/customer.go b/customers/customer.go deleted file mode 100644 index 133051c..0000000 --- a/customers/customer.go +++ /dev/null @@ -1,25 +0,0 @@ -package customers - -import ( - "github.com/checkout/checkout-sdk-go" -) - -type ( - // Request - - Request struct { - *Customer - } - // Customer - - Customer struct { - Email string `json:"email,omitempty"` - Name string `json:"name,omitempty"` - Default string `json:"default,omitempty"` - } -) - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - } -) diff --git a/customers/customers.go b/customers/customers.go new file mode 100644 index 0000000..154cf69 --- /dev/null +++ b/customers/customers.go @@ -0,0 +1,28 @@ +package customers + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +const Path = "customers" + +type ( + CustomerRequest struct { + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Instruments []string `json:"instruments,omitempty"` + } + + GetCustomerResponse struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + DefaultId string `json:"nas,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Instruments []common.InstrumentDetails `json:"instruments,omitempty"` + } +) diff --git a/disputes/client.go b/disputes/client.go index d885709..eac2e1b 100644 --- a/disputes/client.go +++ b/disputes/client.go @@ -1,127 +1,167 @@ package disputes import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" - "github.com/google/go-querystring/query" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" ) -const path = "disputes" - -// Client ... type Client struct { - API checkout.HTTPClient + configuration *configuration.Configuration + apiClient client.HttpClient } -// NewClient ... -func NewClient(config checkout.Config) *Client { +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { return &Client{ - API: httpclient.NewClient(config), + configuration: configuration, + apiClient: apiClient, } } -// GetDisputes ... -func (c *Client) GetDisputes(request *Request) (*Response, error) { - value, _ := query.Values(request.QueryParameter) - var query string = value.Encode() - var urlPath string = "/" + path + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, +func (c *Client) Query(queryFilter QueryFilter) (*QueryResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err } + + url, err := common.BuildQueryPath(path, queryFilter) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusOK { - var disputes Disputes - err = json.Unmarshal(resp.ResponseBody, &disputes) - response.Disputes = &disputes - return response, err + + var response QueryResponse + err = c.apiClient.Get(url, auth, &response) + if err != nil { + return nil, err } - return response, err + + return &response, nil } -// GetDispute ... -func (c *Client) GetDispute(disputeID string) (*Response, error) { - resp, err := c.API.Get(fmt.Sprintf("/%v/%v", path, disputeID)) - response := &Response{ - StatusResponse: resp, - } +func (c *Client) GetDisputeDetails(disputeId string) (*DisputeResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusOK { - var dispute Dispute - err = json.Unmarshal(resp.ResponseBody, &dispute) - response.Dispute = &dispute - return response, err + + var response DisputeResponse + err = c.apiClient.Get(common.BuildPath(path, disputeId), auth, &response) + if err != nil { + return nil, err } - return response, err + + return &response, nil } -// AcceptDispute - -func (c *Client) AcceptDispute(disputeID string) (*Response, error) { - resp, err := c.API.Post(fmt.Sprintf("/%v/%v/accept", path, disputeID), nil, nil) - response := &Response{ - StatusResponse: resp, +func (c *Client) Accept(disputeId string) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Post( + common.BuildPath(path, disputeId, accept), + auth, + nil, + &response, + nil, + ) + if err != nil { + return nil, err } + + return &response, nil +} + +func (c *Client) PutEvidence(disputeId string, evidenceRequest Evidence) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusNoContent { - return response, err + + var response common.MetadataResponse + err = c.apiClient.Put( + common.BuildPath(path, disputeId, evidence), + auth, + evidenceRequest, + &response, + nil, + ) + if err != nil { + return nil, err } - return response, err + + return &response, nil } -// ProvideDisputeEvidence ... -func (c *Client) ProvideDisputeEvidence(disputeID string, request *Request) (*Response, error) { - resp, err := c.API.Put(fmt.Sprintf("/%v/%v/evidence", path, disputeID), request) - response := &Response{ - StatusResponse: resp, +func (c *Client) GetEvidence(disputeId string) (*EvidenceResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response EvidenceResponse + err = c.apiClient.Get(common.BuildPath(path, disputeId, evidence), auth, &response) + if err != nil { + return nil, err } + + return &response, nil +} + +func (c *Client) SubmitEvidence(disputeId string) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusNoContent { - return response, err + + var response common.MetadataResponse + err = c.apiClient.Post( + common.BuildPath(path, disputeId, evidence), + auth, + nil, + &response, + nil, + ) + if err != nil { + return nil, err } - return response, err + + return &response, nil } -// GetDisputeEvidence ... -func (c *Client) GetDisputeEvidence(disputeID string) (*Response, error) { - resp, err := c.API.Get(fmt.Sprintf("/%v/%v/evidence", path, disputeID)) - response := &Response{ - StatusResponse: resp, +func (c *Client) UploadFile(file common.File) (*common.IdResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err } + + req, err := common.BuildFileUploadRequest(&file) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusOK { - var evidences DisputeEvidence - err = json.Unmarshal(resp.ResponseBody, &evidences) - response.Evidences = &evidences - return response, err + + var response common.IdResponse + err = c.apiClient.Upload(common.BuildPath(files), auth, req, &response) + if err != nil { + return nil, err } - return response, err + + return &response, nil } -// SubmitDisputeEvidence - -func (c *Client) SubmitDisputeEvidence(disputeID string) (*Response, error) { - resp, err := c.API.Post(fmt.Sprintf("/%v/%v/evidence", path, disputeID), nil, nil) - response := &Response{ - StatusResponse: resp, - } +func (c *Client) GetFileDetails(fileId string) (*common.FileResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusNoContent { - return response, err + + var response common.FileResponse + err = c.apiClient.Get(common.BuildPath(files, fileId), auth, &response) + if err != nil { + return nil, err } - return response, err + + return &response, nil } diff --git a/disputes/client_test.go b/disputes/client_test.go new file mode 100644 index 0000000..85430b6 --- /dev/null +++ b/disputes/client_test.go @@ -0,0 +1,666 @@ +package disputes + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +var ( + disputeId = "dis_1234" + + disputeEvidence = Evidence{ + ProofOfDeliveryOrServiceFile: "ProofOfDeliveryOrServiceFile", + ProofOfDeliveryOrServiceText: "ProofOfDeliveryOrServiceFile", + } +) + +func TestQuery(t *testing.T) { + var ( + dispute = DisputeSummary{ + Id: "dis_1234", + Status: EvidenceRequired, + Amount: 100, + Currency: common.GBP, + PaymentId: "pay_1234", + } + + disputesList = []DisputeSummary{dispute} + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + queryResponse = QueryResponse{ + HttpMetadata: httpMetadata, + Limit: 10, + Skip: 0, + From: time.Now().Add(-5 * time.Hour), + To: time.Now(), + TotalCount: 1, + Data: disputesList, + } + + pagingError = errors.ErrorDetails{ + RequestID: "0HL80RJLS76I7", + ErrorType: "request_invalid", + ErrorCodes: []string{"paging_limit_invalid"}, + } + ) + + cases := []struct { + name string + query QueryFilter + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*QueryResponse, error) + }{ + { + name: "when query is correct then return disputes", + query: QueryFilter{ + Limit: 10, + Skip: 0, + From: time.Now().Add(-5 * time.Hour), + To: time.Now(), + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*QueryResponse) + *respMapping = queryResponse + }) + }, + checker: func(response *QueryResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, queryResponse.Limit, response.Limit) + assert.Equal(t, queryResponse.Skip, response.Skip) + assert.Equal(t, queryResponse.From, response.From) + assert.Equal(t, queryResponse.To, response.To) + assert.Equal(t, queryResponse.Data, response.Data) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *QueryResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when invalid paging then return error", + query: QueryFilter{ + Limit: 255, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable Entity", + Data: &pagingError, + }) + }, + checker: func(response *QueryResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "422 Unprocessable Entity", chkErr.Status) + assert.Equal(t, &pagingError, chkErr.Data) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Query(tc.query)) + }) + } +} + +func TestGetDisputeDetails(t *testing.T) { + var ( + dispute = Dispute{ + Id: "dis_1234", + Category: General, + Status: EvidenceRequired, + Amount: 100, + Currency: common.GBP, + } + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + disputeResponse = DisputeResponse{ + HttpMetadata: httpMetadata, + Dispute: dispute, + } + ) + + cases := []struct { + name string + disputeId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*DisputeResponse, error) + }{ + { + name: "when disputeId is correct then return dispute", + disputeId: disputeId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*DisputeResponse) + *respMapping = disputeResponse + }) + }, + checker: func(response *DisputeResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, dispute.Id, response.Id) + assert.Equal(t, dispute.Category, response.Category) + assert.Equal(t, dispute.Status, response.Status) + assert.Equal(t, dispute.Amount, response.Amount) + assert.Equal(t, dispute.Currency, response.Currency) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *DisputeResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when dispute not found then return error", + disputeId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *DisputeResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetDisputeDetails(tc.disputeId)) + }) + } +} + +func TestAccept(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + disputeId string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when disputeId is correct then accept dispute", + disputeId: disputeId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when dispute not found then return error", + disputeId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Accept(tc.disputeId)) + }) + } +} + +func TestPutEvidence(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + disputeId string + request Evidence + getAuthorization func(*mock.Mock) mock.Call + apiPut func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when request is correct then put evidence", + disputeId: disputeId, + request: disputeEvidence, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + disputeId: disputeId, + request: Evidence{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when dispute not found then return error", + disputeId: "not_found", + request: Evidence{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPut(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.PutEvidence(tc.disputeId, tc.request)) + }) + } +} + +func TestGetEvidence(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + evidenceResponse = EvidenceResponse{ + HttpMetadata: httpMetadata, + Evidence: disputeEvidence, + } + ) + + cases := []struct { + name string + disputeId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*EvidenceResponse, error) + }{ + { + name: "when disputeId is correct then return evidence", + disputeId: disputeId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*EvidenceResponse) + *respMapping = evidenceResponse + }) + }, + checker: func(response *EvidenceResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, disputeEvidence, response.Evidence) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *EvidenceResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when dispute not found then return error", + disputeId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *EvidenceResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetEvidence(tc.disputeId)) + }) + } +} + +func TestSubmitEvidence(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + disputeId string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when disputeId is correct then submit dispute evidence", + disputeId: disputeId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when dispute not found then return error", + disputeId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.SubmitEvidence(tc.disputeId)) + }) + } +} diff --git a/disputes/dispute.go b/disputes/dispute.go deleted file mode 100644 index 9eabeed..0000000 --- a/disputes/dispute.go +++ /dev/null @@ -1,120 +0,0 @@ -package disputes - -import ( - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -type ( - // Request - - Request struct { - *QueryParameter - *DisputeEvidence - } - - // QueryParameter - - QueryParameter struct { - Limit uint64 `url:"limit,omitempty"` - Skip uint64 `url:"skip,omitempty"` - From time.Time `url:"from,omitempty"` - To time.Time `url:"to,omitempty"` - ID string `url:"id,omitempty"` - Statuses string `url:"statuses,omitempty"` - PaymentID string `url:"payment_id,omitempty"` - PaymentReference string `url:"payment_reference,omitempty"` - PaymentARN string `url:"payment_arn,omitempty"` - ThisChannelOnly *bool `url:"this_channel_only,omitempty"` - } - - // DisputeEvidence - - DisputeEvidence struct { - Links map[string]common.Link `json:"_links,omitempty"` - ProofOfDeliveryOrServiceFile string `json:"proof_of_delivery_or_service_file,omitempty"` - ProofOfDeliveryOrServiceText string `json:"proof_of_delivery_or_service_text,omitempty"` - InvoiceOrReceiptFile string `json:"invoice_or_receipt_file,omitempty"` - InvoiceOrReceiptText string `json:"invoice_or_receipt_text,omitempty"` - InvoiceShowingDistinctTransactionsFile string `json:"invoice_showing_distinct_transactions_file,omitempty"` - InvoiceShowingDistinctTransactionsText string `json:"invoice_showing_distinct_transactions_text,omitempty"` - CustomerCommunicationFile string `json:"customer_communication_file,omitempty"` - CustomerCommunicationText string `json:"customer_communication_text,omitempty"` - RefundOrCancellationPolicyFile string `json:"refund_or_cancellation_policy_file,omitempty"` - RefundOrCancellationPolicyText string `json:"refund_or_cancellation_policy_text,omitempty"` - RecurringTransactionAgreementFile string `json:"recurring_transaction_agreement_file,omitempty"` - RecurringTransactionAgreementText string `json:"recurring_transaction_agreement_text,omitempty"` - AdditionalEvidenceFile string `json:"additional_evidence_file,omitempty"` - AdditionalEvidenceText string `json:"additional_evidence_text,omitempty"` - ProofOfDeliveryOrServiceDateFile string `json:"proof_of_delivery_or_service_date_file,omitempty"` - ProofOfDeliveryOrServiceDateText string `json:"proof_of_delivery_or_service_date_text,omitempty"` - } -) -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Disputes *Disputes `json:"disputes,omitempty"` - Dispute *Dispute `json:"dispute,omitempty"` - Evidences *DisputeEvidence `json:"evidences,omitempty"` - } - - // Disputes - - Disputes struct { - Limit uint64 `json:"limit,omitempty"` - Skip uint64 `json:"skip,omitempty"` - From time.Time `json:"from,omitempty"` - To time.Time `json:"to,omitempty"` - Statuses string `json:"statuses,omitempty"` - ID string `json:"id,omitempty"` - PaymentID string `json:"payment_id,omitempty"` - PaymentReference string `json:"payment_reference,omitempty"` - PaymentARN string `json:"payment_arn,omitempty"` - ThisChannelOnly *bool `json:"this_channel_only,omitempty"` - TotalCount uint64 `json:"total_count,omitempty"` - Data []DisputeSummary `json:"data,omitempty"` - } - - // DisputeSummary - - DisputeSummary struct { - ID string `json:"id,omitempty"` - Category string `json:"category,omitempty"` - Status string `json:"status,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency,omitempty"` - PaymentID string `json:"payment_id,omitempty"` - PaymentReference string `json:"payment_reference,omitempty"` - PaymentARN string `json:"payment_arn,omitempty"` - PaymentMethod string `json:"payment_method,omitempty"` - EvidenceRequiredBy time.Time `json:"evidence_required_by,omitempty"` - ReceivedOn time.Time `json:"received_on,omitempty"` - LastUpdate time.Time `json:"last_update,omitempty"` - Links map[string]common.Link `json:"_links,omitempty"` - } - // Dispute - - Dispute struct { - ID string `json:"id,omitempty"` - Category string `json:"category,omitempty"` - Currency string `json:"currency,omitempty"` - ReasonCode string `json:"reason_code,omitempty"` - RelevantEvidence []string `json:"relevant_evidence,omitempty"` - EvidenceRequiredBy time.Time `json:"evidence_required_by,omitempty"` - ReceivedOn time.Time `json:"received_on,omitempty"` - LastUpdate time.Time `json:"last_update,omitempty"` - Payment *Payment `json:"payment,omitempty"` - Links map[string]common.Link `json:"_links,omitempty"` - } - // Payment - - Payment struct { - ID string `json:"id,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency,omitempty"` - Method string `json:"method,omitempty"` - ARN string `json:"arn,omitempty"` - ProcessedOn time.Time `json:"processed_on,omitempty"` - } - - // Evidence - - Evidence struct { - Links map[string]string `json:"-,omitempty"` - } -) diff --git a/disputes/disputes.go b/disputes/disputes.go new file mode 100644 index 0000000..dc2d7bb --- /dev/null +++ b/disputes/disputes.go @@ -0,0 +1,161 @@ +package disputes + +import ( + "time" + + "github.com/checkout/checkout-sdk-go/common" +) + +const ( + path = "disputes" + accept = "accept" + evidence = "evidence" + files = "files" +) + +type ( + Dispute struct { + Id string `json:"id,omitempty"` + Category DisputeCategory `json:"category,omitempty"` + Status DisputeStatus `json:"status,omitempty"` + Amount int64 `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + ReasonCode string `json:"reason_code,omitempty"` + ResolvedReason DisputeResolvedReason `json:"resolved_reason,omitempty"` + RelevantEvidence []RelevantEvidence `json:"relevant_evidence,omitempty"` + EvidenceRequiredBy time.Time `json:"evidence_required_by,omitempty"` + ReceivedOn time.Time `json:"received_on,omitempty"` + LastUpdate time.Time `json:"last_update,omitempty"` + Payment *PaymentDispute `json:"payment,omitempty"` + + // Not available on Previous + EntityId string `json:"entity_id,omitempty"` + SubEntityId string `json:"sub_entity_id,omitempty"` + Links map[string]common.Link `json:"_links,omitempty"` + } + + PaymentDispute struct { + Id string `json:"id,omitempty"` + Amount int64 `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Method string `json:"method,omitempty"` + Arn string `json:"arn,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + + // Not available on Previous + ActionId string `json:"actionId,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + Mcc string `json:"mcc,omitempty"` + ThreeDSVersionEnrollment *ThreeDSVersionEnrollment `json:"3ds,omitempty"` + Eci string `json:"eci,omitempty"` + HasRefund bool `json:"has_refund,omitempty"` + } + + ThreeDSVersionEnrollment struct { + Enrolled string `json:"enrolled,omitempty"` + Version string `json:"version,omitempty"` + } +) + +type ( + DisputeResponse struct { + HttpMetadata common.HttpMetadata + Dispute + } +) + +// Query +type ( + QueryFilter struct { + Limit uint8 `url:"limit,omitempty"` //min=1 - max=250 + Skip int `url:"skip,omitempty"` + From time.Time `url:"from,omitempty" layout:"2006-01-02T15:04:05Z"` + To time.Time `url:"to,omitempty" layout:"2006-01-02T15:04:05Z"` + + Id string `url:"id,omitempty"` + Statuses string `url:"statuses,omitempty"` //One or more comma-separated statuses. This works like a logical OR operator + PaymentId string `url:"payment_id,omitempty"` + PaymentReference string `url:"payment_reference,omitempty"` + PaymentArn string `url:"payment_arn,omitempty"` + ThisChannelOnly bool `url:"this_channel_only,omitempty"` + + // Not available on Previous + EntityIds string `url:"entity_ids,omitempty"` //One or more comma-separated client entities. This works like a logical OR operator + SubEntityIds string `url:"subEntity_ids,omitempty"` //One or more comma-separated client entities. This works like a logical OR operator + PaymentMcc string `url:"payment_mcc,omitempty"` + } + + QueryResponse struct { + HttpMetadata common.HttpMetadata + + Limit uint8 `json:"limit,omitempty"` //min=1 - max=250 + Skip int `json:"skip,omitempty"` + From time.Time `json:"from,omitempty" time_format:"2006-01-02T15:04:05Z"` + To time.Time `json:"to,omitempty" time_format:"2006-01-02T15:04:05Z"` + + Id string `json:"id,omitempty"` + Statuses string `json:"statuses,omitempty"` //One or more comma-separated statuses. This works like a logical OR operator + PaymentId string `json:"payment_id,omitempty"` + PaymentReference string `json:"payment_reference,omitempty"` + PaymentArn string `json:"payment_arn,omitempty"` + ThisChannelOnly bool `json:"this_channel_only,omitempty"` + + TotalCount int `json:"total_count,omitempty"` + Data []DisputeSummary `json:"data,omitempty"` + + // Not available on Previous + EntityIds string `url:"entity_ids,omitempty"` //One or more comma-separated client entities. This works like a logical OR operator//One or more comma-separated client entities. This works like a logical OR operator + SubEntityIds string `url:"subEntity_ids,omitempty"` //One or more comma-separated client entities. This works like a logical OR operator//One or more comma-separated client entities. This works like a logical OR operator + PaymentMcc string `url:"payment_mcc,omitempty"` + } + + DisputeSummary struct { + Id string `json:"id,omitempty"` + Category DisputeCategory `json:"category,omitempty"` + Status DisputeStatus `json:"status,omitempty"` + Amount int64 `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + ReasonCode string `json:"reason_code,omitempty"` + PaymentId string `json:"payment_id,omitempty"` + PaymentActionId string `json:"payment_action_id,omitempty"` + PaymentReference string `json:"payment_reference,omitempty"` + PaymentArn string `json:"payment_arn,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + EvidenceRequiredBy time.Time `json:"evidence_required_by,omitempty"` + ReceivedOn time.Time `json:"received_on,omitempty"` + LastUpdate time.Time `json:"last_update,omitempty"` + + // Not available on Previous + EntityId string `json:"entity_id,omitempty"` + SubEntityId string `json:"sub_entity_id,omitempty"` + PaymentMcc string `json:"payment_mcc,omitempty"` + } +) + +// Evidence +type ( + Evidence struct { + ProofOfDeliveryOrServiceFile string `json:"proof_of_delivery_or_service_file,omitempty"` + ProofOfDeliveryOrServiceText string `json:"proof_of_delivery_or_service_text,omitempty"` // max 500 + InvoiceOrReceiptFile string `json:"invoice_or_receipt_file,omitempty"` + InvoiceOrReceiptText string `json:"invoice_or_receipt_text,omitempty"` + InvoiceShowingDistinctTransactionsFile string `json:"invoice_showing_distinct_transactions_file,omitempty"` + InvoiceShowingDistinctTransactionsText string `json:"invoice_showing_distinct_transactions_text,omitempty"` // max 500 + CustomerCommunicationFile string `json:"customer_communication_file,omitempty"` + CustomerCommunicationText string `json:"customer_communication_text,omitempty"` // max 500 + RefundOrCancellationPolicyFile string `json:"refund_or_cancellation_policy_file,omitempty"` + RefundOrCancellationPolicyText string `json:"refund_or_cancellation_policy_text,omitempty"` // max 500 + RecurringTransactionAgreementFile string `json:"recurring_transaction_agreement_file,omitempty"` + RecurringTransactionAgreementText string `json:"recurring_transaction_agreement_text,omitempty"` // max 500 + AdditionalEvidenceFile string `json:"additional_evidence_file,omitempty"` + AdditionalEvidenceText string `json:"additional_evidence_text,omitempty"` // max 500 + ProofOfDeliveryOrServiceDateFile string `json:"proof_of_delivery_or_service_date_file,omitempty"` + ProofOfDeliveryOrServiceDateText string `json:"proof_of_delivery_or_service_date_text,omitempty"` // max 500 + Links map[string]common.Link `json:"_links,omitempty"` + } + + EvidenceResponse struct { + HttpMetadata common.HttpMetadata + Evidence + } +) diff --git a/disputes/disputes_categories.go b/disputes/disputes_categories.go new file mode 100644 index 0000000..ddcaddd --- /dev/null +++ b/disputes/disputes_categories.go @@ -0,0 +1,15 @@ +package disputes + +type DisputeCategory string + +const ( + General DisputeCategory = "general" + Duplicate DisputeCategory = "duplicate" + Fraudulent DisputeCategory = "fraudulent" + Unrecognized DisputeCategory = "unrecognized" + IncorrectAmount DisputeCategory = "incorrect_amount" + NotAsDescribed DisputeCategory = "not_as_described" + CreditNotIssued DisputeCategory = "credit_not_issued" + CanceledRecurring DisputeCategory = "canceled_recurring" + ProductServiceNotReceived DisputeCategory = "product_service_not_received" +) diff --git a/disputes/disputes_relevant_evidence.go b/disputes/disputes_relevant_evidence.go new file mode 100644 index 0000000..cb8e371 --- /dev/null +++ b/disputes/disputes_relevant_evidence.go @@ -0,0 +1,13 @@ +package disputes + +type RelevantEvidence string + +const ( + ProofOfDeliveryOrService RelevantEvidence = "proof_of_delivery_or_service" + InvoiceOrReceipt RelevantEvidence = "invoice_or_receipt" + InvoiceShowingDistinctTransactions RelevantEvidence = "invoice_showing_distinct_transactions" + CustomerCommunication RelevantEvidence = "customer_communication" + RefundOrCancellationPolicy RelevantEvidence = "refund_or_cancellation_policy" + RecurringTransactionAgreement RelevantEvidence = "recurring_transaction_agreement" + AdditionalEvidence RelevantEvidence = "additional_evidence" +) diff --git a/disputes/disputes_resolved_reasons.go b/disputes/disputes_resolved_reasons.go new file mode 100644 index 0000000..059aa21 --- /dev/null +++ b/disputes/disputes_resolved_reasons.go @@ -0,0 +1,9 @@ +package disputes + +type DisputeResolvedReason string + +const ( + RapidDisputeResolution DisputeResolvedReason = "rapid_dispute_resolution" + NegativeAmount DisputeResolvedReason = "negative_amount" + AlreadyRefunded DisputeResolvedReason = "already_refunded" +) diff --git a/disputes/disputes_status.go b/disputes/disputes_status.go new file mode 100644 index 0000000..617f40b --- /dev/null +++ b/disputes/disputes_status.go @@ -0,0 +1,17 @@ +package disputes + +type DisputeStatus string + +const ( + WON DisputeStatus = "won" + LOST DisputeStatus = "lost" + EXPIRED DisputeStatus = "expired" + ACCEPTED DisputeStatus = "accepted" + CANCELED DisputeStatus = "canceled" + RESOLVED DisputeStatus = "resolved" + ArbitrationWon DisputeStatus = "arbitration_won" + ArbitrationLost DisputeStatus = "arbitration_lost" + EvidenceRequired DisputeStatus = "evidence_required" + EvidenceUnderReview DisputeStatus = "evidence_under_review" + ArbitrationUnderReview DisputeStatus = "arbitration_under_review" +) diff --git a/errors/error.go b/errors/error.go new file mode 100644 index 0000000..6b7c028 --- /dev/null +++ b/errors/error.go @@ -0,0 +1,47 @@ +package errors + +import "fmt" + +type ErrorDetails struct { + RequestID string `json:"request_id,omitempty"` + ErrorType string `json:"error_type,omitempty"` + ErrorCodes []string `json:"error_codes,omitempty"` +} + +type ( + CheckoutArgumentError string + CheckoutAuthorizationError string + + CheckoutAPIError struct { + StatusCode int + Status string + Data *ErrorDetails + } + + CheckoutOAuthError struct { + Description string `json:"error"` + } +) + +func (e CheckoutArgumentError) Error() string { return string(e) } +func (e CheckoutAuthorizationError) Error() string { return string(e) } +func (e CheckoutAPIError) Error() string { return e.Status } +func (e CheckoutOAuthError) Error() string { return e.Description } + +type ( + UnsupportedTypeError string + BadRequestError string + InternalError string +) + +func (e UnsupportedTypeError) Error() string { return string(e) } +func (e BadRequestError) Error() string { return string(e) } +func (e InternalError) Error() string { return string(e) } + +func InvalidKey(key string) CheckoutAuthorizationError { + return CheckoutAuthorizationError(fmt.Sprintf("%s is required for this operation", key)) +} + +func InvalidAuthorizationType(authType string) CheckoutAuthorizationError { + return CheckoutAuthorizationError(fmt.Sprintf("Operation requires %s authorization type", authType)) +} diff --git a/errors/error_handler.go b/errors/error_handler.go new file mode 100644 index 0000000..a8c640e --- /dev/null +++ b/errors/error_handler.go @@ -0,0 +1,33 @@ +package errors + +import ( + "encoding/json" + "net/http" +) + +func HandleError(statusCode int, status string, requestId string, body []byte) CheckoutAPIError { + if statusCode >= http.StatusInternalServerError { + return CheckoutAPIError{ + StatusCode: statusCode, + Status: string(body), + } + } + + var details ErrorDetails + if len(body) != 0 { + if err := json.Unmarshal(body, &details); err != nil { + return CheckoutAPIError{ + StatusCode: http.StatusBadRequest, + Status: "Unparsable error", + } + } + } + + details.RequestID = requestId + + return CheckoutAPIError{ + StatusCode: statusCode, + Status: status, + Data: &details, + } +} diff --git a/errors/error_handler_test.go b/errors/error_handler_test.go new file mode 100644 index 0000000..4d0a7f9 --- /dev/null +++ b/errors/error_handler_test.go @@ -0,0 +1,81 @@ +package errors + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandleError(t *testing.T) { + cases := []struct { + name string + inputStatusCode int + inputStatus string + inputRequestId string + inputBody []byte + expectedError CheckoutAPIError + }{ + { + name: "when body is nil then return CheckoutAPIError with empty Details", + inputStatusCode: http.StatusNotFound, + inputStatus: "404 Not Found", + inputRequestId: "12345", + expectedError: CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + Data: &ErrorDetails{ + RequestID: "12345", + ErrorType: "", + ErrorCodes: nil, + }, + }, + }, + { + name: "when body is not nil then return CheckoutAPIError with Details", + inputStatusCode: http.StatusUnprocessableEntity, + inputStatus: "422 Unprocessable Entity", + inputRequestId: "12345", + inputBody: getErrorBody(), + expectedError: CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable Entity", + Data: &ErrorDetails{ + RequestID: "12345", + ErrorType: "request_invalid", + ErrorCodes: []string{"invalid"}, + }, + }, + }, + { + name: "when error body is invalid then return unparsable error", + inputStatusCode: http.StatusNotFound, + inputStatus: "404 Not Found", + inputRequestId: "12345", + inputBody: []byte("unparsable_body"), + expectedError: CheckoutAPIError{ + StatusCode: http.StatusBadRequest, + Status: "Unparsable error", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedError, HandleError(tc.inputStatusCode, tc.inputStatus, tc.inputRequestId, tc.inputBody)) + }) + } +} + +func getErrorBody() []byte { + errorDetails := ErrorDetails{ + RequestID: "12345", + ErrorType: "request_invalid", + ErrorCodes: []string{"invalid"}, + } + + body, _ := json.Marshal(errorDetails) + + return body +} diff --git a/events/client.go b/events/client.go index f255847..e4667d0 100644 --- a/events/client.go +++ b/events/client.go @@ -1,137 +1,39 @@ package events import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" - "github.com/google/go-querystring/query" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" ) -const path = "events" - -// Client ... type Client struct { - API checkout.HTTPClient + configuration *configuration.Configuration + apiClient client.HttpClient } -// NewClient ... -func NewClient(config checkout.Config) *Client { +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { return &Client{ - API: httpclient.NewClient(config), - } -} - -// RetrieveEventTypes - -func (c *Client) RetrieveEventTypes(request *Request) (*Response, error) { - - value, _ := query.Values(request.EventTypeRequest) - var query string = value.Encode() - var urlPath string = "/event-types" + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var eventTypes []EventType - err = json.Unmarshal(resp.ResponseBody, &eventTypes) - response.EventTypes = eventTypes - return response, err - } - return response, err -} - -// RetrieveEvents - -func (c *Client) RetrieveEvents(request *Request) (*Response, error) { - - value, _ := query.Values(request.QueryParameter) - var query string = value.Encode() - var urlPath string = "/" + path + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var events Events - err = json.Unmarshal(resp.ResponseBody, &events) - response.Events = &events - return response, err + configuration: configuration, + apiClient: apiClient, } - return response, err - } -// RetrieveEvent - -func (c *Client) RetrieveEvent(eventID string) (*Response, error) { - - resp, err := c.API.Get(fmt.Sprintf("/%v/%v", path, eventID)) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err +func (c *Client) RetrieveAllEventTypes(version ...string) (*EventTypesResponse, error) { + path := path + if version != nil { + path += "?version=" + version[0] } - if resp.StatusCode == http.StatusOK { - var event Event - err = json.Unmarshal(resp.ResponseBody, &event) - response.Event = &event - return response, err - } - return response, err -} -// RetrieveEventNotification - -func (c *Client) RetrieveEventNotification(eventID string, notificationID string) (*Response, error) { - resp, err := c.API.Get(fmt.Sprintf("/%v/%v/notifications/%v", path, eventID, notificationID)) - response := &Response{ - StatusResponse: resp, - } + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var notification Notification - err = json.Unmarshal(resp.ResponseBody, ¬ification) - response.Notification = ¬ification - return response, err + return nil, err } - return response, err -} -// Retry - -func (c *Client) Retry(eventID string, webhookID string) (*Response, error) { - resp, err := c.API.Post(fmt.Sprintf("/%v/%v/webhooks/%v/retry", path, eventID, webhookID), nil, nil) - response := &Response{ - StatusResponse: resp, - } + var response EventTypesResponse + err = c.apiClient.Get(common.BuildPath(path), auth, &response) if err != nil { - return response, err + return nil, err } - if resp.StatusCode == http.StatusOK { - return response, err - } - return response, err -} -// RetryAll - -func (c *Client) RetryAll(eventID string) (*Response, error) { - resp, err := c.API.Post(fmt.Sprintf("/%v/%v/webhooks/retry", path, eventID), nil, nil) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - return response, err - } - return response, err + return &response, nil } diff --git a/events/client_test.go b/events/client_test.go new file mode 100644 index 0000000..79b5703 --- /dev/null +++ b/events/client_test.go @@ -0,0 +1,128 @@ +package events + +import ( + "github.com/checkout/checkout-sdk-go/errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestRetrieveAllEventTypes(t *testing.T) { + var ( + eventTypes = []EventTypes{ + { + Version: "1.0", + EventTypes: []string{"event.1", "event.2", "event.3"}, + }, + { + Version: "2.0", + EventTypes: []string{"event.4", "event.5"}, + }, + } + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: 200, + } + + response = EventTypesResponse{ + HttpResponse: httpMetadata, + EventTypes: eventTypes, + } + ) + + cases := []struct { + name string + requestVersions string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*EventTypesResponse, error) + }{ + { + name: "when no event versions sent then return all events", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*EventTypesResponse) + *respMapping = response + }) + }, + checker: func(response *EventTypesResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpResponse.StatusCode) + assert.Equal(t, httpMetadata, response.HttpResponse) + assert.Equal(t, eventTypes, response.EventTypes) + + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *EventTypesResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + + }, + }, + { + name: "when request invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnauthorized, + Status: "401 Unauthorized", + }) + }, + checker: func(response *EventTypesResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnauthorized, chkErr.StatusCode) + + }, + }, + // TODO complete with more cases + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + eventsClient := NewClient(configuration, apiClient) + + tc.checker(eventsClient.RetrieveAllEventTypes(tc.requestVersions)) + }) + } +} diff --git a/events/event.go b/events/event.go deleted file mode 100644 index a12a8cc..0000000 --- a/events/event.go +++ /dev/null @@ -1,82 +0,0 @@ -package events - -import ( - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" - "github.com/checkout/checkout-sdk-go/payments" -) - -type ( - // Request - - Request struct { - *QueryParameter - *EventTypeRequest - } - - // QueryParameter - - QueryParameter struct { - From time.Time `url:"from,omitempty"` - To time.Time `url:"to,omitempty"` - Limit uint64 `url:"limit,omitempty"` - PaymentID string `url:"payment_id,omitempty"` - } - - // EventTypeRequest - - EventTypeRequest struct { - Version string `url:"version,omitempty"` - } -) - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - EventTypes []EventType `json:"event_types,omitempty"` - Events *Events `json:"events,omitempty"` - Event *Event `json:"event,omitempty"` - Notification *Notification `json:"notification,omitempty"` - } - // EventType - - EventType struct { - Version string `json:"version,omitempty"` - EventTypes []string `json:"event_types,omitempty"` - } - // Events - - Events struct { - TotalCount uint64 `json:"total_count,omitempty"` - Limit uint64 `json:"limit,omitempty"` - Skip uint64 `json:"skip,omitempty"` - From time.Time `json:"from,omitempty"` - To time.Time `json:"to,omitempty"` - Data []Event `json:"data,omitempty"` - } - - // Event - - Event struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Version string `json:"version,omitempty"` - CreatedOn string `json:"created_on,omitempty"` - Data *payments.Processed `json:"data,omitempty"` - Notifications []Notification `json:"notifications,omitempty"` - Links map[string]common.Link `json:"_links"` - } - // Notification - - Notification struct { - ID string `json:"id,omitempty"` - URL string `json:"url,omitempty"` - Success *bool `json:"success,omitempty"` - ContentType string `json:"content_type,omitempty"` - Attempts []NotificationAttempt `json:"attempts,omitempty"` - Links map[string]common.Link `json:"_links"` - } - // NotificationAttempt - - NotificationAttempt struct { - StatusCode uint64 `json:"status_code,omitempty"` - ResponseBody string `json:"response_body,omitempty"` - RetryMode string `json:"retry_mode,omitempty"` - Timestamp time.Time `json:"timestamp,omitempty"` - } -) diff --git a/events/events.go b/events/events.go new file mode 100644 index 0000000..af00e0b --- /dev/null +++ b/events/events.go @@ -0,0 +1,29 @@ +package events + +import ( + "encoding/json" + "github.com/checkout/checkout-sdk-go/common" +) + +const path = "event-types" + +type ( + EventTypesResponse struct { + HttpResponse common.HttpMetadata + EventTypes []EventTypes + } + + EventTypes struct { + Version string `json:"version"` + EventTypes []string `json:"event_types"` + } +) + +func (e *EventTypesResponse) UnmarshalJSON(data []byte) error { + var eventTypes []EventTypes + if err := json.Unmarshal(data, &eventTypes); err != nil { + return err + } + e.EventTypes = eventTypes + return nil +} diff --git a/files/client.go b/files/client.go deleted file mode 100644 index 9f11961..0000000 --- a/files/client.go +++ /dev/null @@ -1,68 +0,0 @@ -package files - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" -) - -const path = "files" - -// Client ... -type Client struct { - API checkout.HTTPClient -} - -// NewClient ... -func NewClient(config checkout.Config) *Client { - return &Client{ - API: httpclient.NewClient(config), - } -} - -// UploadFile - -func (c *Client) UploadFile(file *FileUpload) (*Response, error) { - if file == nil { - return nil, fmt.Errorf("file cannot be nil, and params.Purpose and params.File must be set") - } - bodyBuffer, boundary, err := file.GetBody() - if err != nil { - return nil, err - } - resp, err := c.API.Upload(fmt.Sprintf("/%v", path), boundary, bodyBuffer) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusCreated { - var file File - err = json.Unmarshal(resp.ResponseBody, &file) - response.File = &file - return response, err - } - return response, err -} - -// GetFile - -func (c *Client) GetFile(fileID string) (*Response, error) { - - resp, err := c.API.Get(fmt.Sprintf("/%v/%v", path, fileID)) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var file File - err = json.Unmarshal(resp.ResponseBody, &file) - response.File = &file - return response, err - } - return response, err -} diff --git a/files/file.go b/files/file.go deleted file mode 100644 index 9a2aadd..0000000 --- a/files/file.go +++ /dev/null @@ -1,105 +0,0 @@ -package files - -import ( - "bytes" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "os" - "path/filepath" - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -type ( - // Request - - Request struct { - *QueryParameter - *FileUpload - } - - // QueryParameter - - QueryParameter struct { - } - - // FileUpload - - FileUpload struct { - FileReader *os.File - File *string - Purpose *string - } -) - -// GetFileContentType - -func GetFileContentType(out *os.File) (string, error) { - - buffer := make([]byte, 512) - _, err := out.Read(buffer) - if err != nil { - return "", err - } - contentType := http.DetectContentType(buffer) - return contentType, nil -} - -// CreateFormFile - -func CreateFormFile(w *multipart.Writer, fieldname string, filename string, contentType string) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldname, filename)) - h.Set("Content-Type", contentType) - return w.CreatePart(h) -} - -// GetBody - -func (f *FileUpload) GetBody() (*bytes.Buffer, string, error) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - if f.Purpose != nil { - err := writer.WriteField("purpose", checkout.StringValue(f.Purpose)) - if err != nil { - return nil, "", err - } - } - if f.FileReader != nil && f.File != nil { - contentType, err := GetFileContentType(f.FileReader) - if err != nil { - return nil, "", err - } - part, err := CreateFormFile(writer, "file", filepath.Base(checkout.StringValue(f.File)), checkout.StringValue(&contentType)) - if err != nil { - return nil, "", err - } - var r io.Reader - r = f.FileReader - _, err = io.Copy(part, r) - if err != nil { - return nil, "", err - } - } - err := writer.Close() - if err != nil { - return nil, "", err - } - return body, writer.Boundary(), nil -} - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - File *File `json:"file,omitempty"` - } - // File - - File struct { - ID string `json:"id,omitempty"` - Filename string `json:"filename,omitempty"` - Purpose string `json:"purpose,omitempty"` - Size uint64 `json:"size,omitempty"` - UploadedOn time.Time `json:"uploaded_on,omitempty"` - Links map[string]common.Link `json:"_links"` - } -) diff --git a/forex/client.go b/forex/client.go new file mode 100644 index 0000000..5bc40b9 --- /dev/null +++ b/forex/client.go @@ -0,0 +1,34 @@ +package forex + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) RequestQuote(request QuoteRequest) (*QuoteResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response QuoteResponse + err = c.apiClient.Post(common.BuildPath(forex, quotes), auth, request, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/forex/client_test.go b/forex/client_test.go new file mode 100644 index 0000000..43435f3 --- /dev/null +++ b/forex/client_test.go @@ -0,0 +1,118 @@ +package forex + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +func TestRequestQuote(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + quote = QuoteResponse{ + HttpMetadata: httpMetadata, + Id: "qte_id", + SourceCurrency: common.GBP, + SourceAmount: 30000, + DestinationCurrency: common.USD, + DestinationAmount: 35700, + Rate: 1.19, + ExpiresOn: time.Now(), + IsSingleUse: false, + } + ) + + cases := []struct { + name string + request QuoteRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*QuoteResponse, error) + }{ + { + name: "when request is correct then should request quote", + request: QuoteRequest{ + SourceCurrency: common.GBP, + SourceAmount: 30000, + DestinationCurrency: common.USD, + ProcessingChannelId: "pc_abcdefghijklmnopqrstuvwxyz", + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*QuoteResponse) + *respMapping = quote + }) + }, + checker: func(response *QuoteResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.NotNil(t, response.Rate) + assert.NotNil(t, response.ExpiresOn) + }, + }, + { + name: "when request is not correct then return error", + request: QuoteRequest{ + ProcessingChannelId: "pc_abcdefghijklmnopqrstuvwxyz", + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Unprocessable", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{"source_currency_required"}, + }, + }) + }, + checker: func(response *QuoteResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + assert.Contains(t, chkErr.Data.ErrorCodes, "source_currency_required") + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestQuote(tc.request)) + }) + } +} diff --git a/forex/forex.go b/forex/forex.go new file mode 100644 index 0000000..fa452e2 --- /dev/null +++ b/forex/forex.go @@ -0,0 +1,34 @@ +package forex + +import ( + "time" + + "github.com/checkout/checkout-sdk-go/common" +) + +const ( + forex = "forex" + quotes = "quotes" +) + +type ( + QuoteRequest struct { + SourceCurrency common.Currency `json:"source_currency,omitempty"` + SourceAmount int `json:"source_amount,omitempty"` + DestinationCurrency common.Currency `json:"destination_currency,omitempty"` + DestinationAmount int `json:"destination_amount,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + } + + QuoteResponse struct { + HttpMetadata common.HttpMetadata `json:"http_metadata,omitempty"` + Id string `json:"id,omitempty"` + SourceCurrency common.Currency `json:"source_currency,omitempty"` + SourceAmount int `json:"source_amount,omitempty"` + DestinationCurrency common.Currency `json:"destination_currency,omitempty"` + DestinationAmount int `json:"destination_amount,omitempty"` + Rate float64 `json:"rate,omitempty"` + ExpiresOn time.Time `json:"expires_on,omitempty"` + IsSingleUse bool `json:"is_single_use,omitempty"` + } +) diff --git a/go.mod b/go.mod index df883b1..ceb589f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/checkout/checkout-sdk-go go 1.14 require ( - github.com/google/go-querystring v1.0.0 + github.com/gabriel-vasile/mimetype v1.4.1 + github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.7.0 ) diff --git a/go.sum b/go.sum index 10ec2f9..e8deeb0 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,28 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/httpclient/client.go b/httpclient/client.go deleted file mode 100644 index 90d72ce..0000000 --- a/httpclient/client.go +++ /dev/null @@ -1,409 +0,0 @@ -package httpclient - -import ( - "bytes" - "encoding/csv" - "encoding/json" - "io" - "io/ioutil" - "net/http" - "strings" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -var client *HTTPClient - -// HTTPClient ... -type HTTPClient struct { - HTTPClient *http.Client - PublicKey string - SecretKey string - URI string - LeveledLogger checkout.LeveledLoggerInterface - MaxNetworkRetries int64 - networkRetriesSleep bool - BearerAuthentication bool -} - -// GetClient ... -func GetClient() *HTTPClient { - return client -} - -type nopReadCloser struct { - io.Reader -} - -func (nopReadCloser) Close() error { return nil } - -// NewClient ... -func NewClient(config checkout.Config) *HTTPClient { - - client = &HTTPClient{ - HTTPClient: config.HTTPClient, - PublicKey: config.PublicKey, - SecretKey: config.SecretKey, - URI: checkout.StringValue(config.URI), - LeveledLogger: config.LeveledLogger, - MaxNetworkRetries: *config.MaxNetworkRetries, - networkRetriesSleep: true, - BearerAuthentication: config.BearerAuthentication, - } - return client -} - -// Get ... -func (c *HTTPClient) Get(path string) (*checkout.StatusResponse, error) { - - request, err := c.NewRequest(http.MethodGet, c.URI+path, nil) - if err != nil { - return nil, err - } - c.setContentType(request) - c.setUserAgent(request) - c.setAuthorization(c.URI+path, request) - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - Header: response.Header, - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} - -// Post ... -func (c *HTTPClient) Post(path string, body interface{}, params *checkout.Params) (*checkout.StatusResponse, error) { - - request, err := c.NewRequest(http.MethodPost, c.URI+path, body) - if err != nil { - return nil, err - } - c.setContentType(request) - c.setUserAgent(request) - c.setAuthorization(c.URI+path, request) - c.setIdempotencyKey(request, params) - - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} - -// NewRequest ... -func (c *HTTPClient) NewRequest(method, path string, body interface{}) (*http.Request, error) { - - if body != nil { - requestBody, err := json.Marshal(body) - if err != nil { - return nil, err - } - request, err := http.NewRequest(method, path, bytes.NewBuffer(requestBody)) - if err != nil { - return nil, err - } - return request, nil - } - request, err := http.NewRequest(method, path, nil) - if err != nil { - return nil, err - } - return request, nil -} - -// Upload - -func (c *HTTPClient) Upload(path string, boundary string, body *bytes.Buffer) (resp *checkout.StatusResponse, err error) { - - contentType := "multipart/form-data; boundary=" + boundary - request, err := c.NewRequest(http.MethodPost, c.URI+path, nil) - if err != nil { - return nil, err - } - if body != nil { - reader := bytes.NewReader(body.Bytes()) - request.Body = nopReadCloser{reader} - request.GetBody = func() (io.ReadCloser, error) { - reader := bytes.NewReader(body.Bytes()) - return nopReadCloser{reader}, nil - } - } - c.setAuthorization(c.URI+path, request) - c.setUserAgent(request) - request.Header.Add("Content-Type", contentType) - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} - -// Download - -func (c *HTTPClient) Download(path string) (resp *checkout.StatusResponse, err error) { - - request, err := c.NewRequest(http.MethodGet, c.URI+path, nil) - // Setting headers if needed - c.setAuthorization(c.URI+path, request) - c.setUserAgent(request) - request.Header.Add("Content-Type", "text/csv;") - - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - reader := csv.NewReader(response.Body) - data, err := reader.ReadAll() - if err != nil { - return apiResponse, err - } - apiResponse.ResponseCSV = data - return apiResponse, nil -} - -func (c *HTTPClient) setAuthorization(path string, req *http.Request) { - - if strings.Contains(path, "/tokens") { - req.Header.Add("Authorization", getAuthorizationHeader(c.PublicKey, c.BearerAuthentication)) - } else { - req.Header.Add("Authorization", getAuthorizationHeader(c.SecretKey, c.BearerAuthentication)) - } -} - -func getAuthorizationHeader(key string, bearerAuthentication bool) string { - if bearerAuthentication { - return "Bearer " + key - } - return key -} - -func (c *HTTPClient) setUserAgent(req *http.Request) { - req.Header.Add("User-Agent", "checkout-sdk-go/"+checkout.ClientVersion) -} - -func (c *HTTPClient) setContentType(req *http.Request) { - req.Header.Add("Content-Type", "application/json") -} - -func isHTTPWriteMethod(method string) bool { - return method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch || method == http.MethodDelete -} - -func (c *HTTPClient) setIdempotencyKey(req *http.Request, params *checkout.Params) { - if params != nil { - if params.IdempotencyKey != nil { - idempotencyKey := strings.TrimSpace(*params.IdempotencyKey) - req.Header.Add("Cko-Idempotency-Key", idempotencyKey) - } else if isHTTPWriteMethod(req.Method) { - req.Header.Add("Cko-Idempotency-Key", checkout.NewIdempotencyKey()) - } - for k, v := range params.Headers { - for _, line := range v { - // Use Set to override the default value possibly set before - req.Header.Set(k, line) - } - } - } -} - -func responseToError(apiRes *checkout.StatusResponse, body []byte) *common.Error { - err := &common.Error{} - if apiRes.StatusCode == 422 { - var details common.ErrorDetails - json.Unmarshal(body, &details) - err.Data = &details - } - err.Status = apiRes.Status - err.StatusCode = apiRes.StatusCode - return err -} - -// Delete ... -func (c *HTTPClient) Delete(path string) (*checkout.StatusResponse, error) { - - request, err := c.NewRequest(http.MethodDelete, c.URI+path, nil) - if err != nil { - return nil, err - } - c.setContentType(request) - c.setUserAgent(request) - c.setAuthorization(c.URI+path, request) - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} - -// Put ... -func (c *HTTPClient) Put(path string, body interface{}) (*checkout.StatusResponse, error) { - - request, err := c.NewRequest(http.MethodPut, c.URI+path, body) - if err != nil { - return nil, err - } - c.setContentType(request) - c.setUserAgent(request) - c.setAuthorization(c.URI+path, request) - - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} - -// Patch ... -func (c *HTTPClient) Patch(path string, body interface{}) (*checkout.StatusResponse, error) { - - request, err := c.NewRequest(http.MethodPatch, c.URI+path, body) - if err != nil { - return nil, err - } - c.setContentType(request) - c.setUserAgent(request) - c.setAuthorization(c.URI+path, request) - - response, err := c.HTTPClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - requestID := response.Header.Get(checkout.CKORequestID) - version := response.Header.Get(checkout.CKOVersion) - apiResponse := &checkout.StatusResponse{ - Status: response.Status, - StatusCode: response.StatusCode, - Headers: &checkout.Headers{ - CKORequestID: &requestID, - CKOVersion: &version, - }, - } - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return apiResponse, err - } - apiResponse.ResponseBody = responseBody - if response.StatusCode >= http.StatusBadRequest { - err := responseToError(apiResponse, responseBody) - return apiResponse, err - } - return apiResponse, nil -} diff --git a/instruments/abc/client.go b/instruments/abc/client.go new file mode 100644 index 0000000..b54d1a4 --- /dev/null +++ b/instruments/abc/client.go @@ -0,0 +1,91 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) Create(request CreateInstrumentRequest) (*CreateInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response CreateInstrumentResponse + err = c.apiClient.Post( + common.BuildPath(instruments.Path), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Get(instrumentId string) (*GetInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response GetInstrumentResponse + err = c.apiClient.Get(common.BuildPath(instruments.Path, instrumentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Update(instrumentId string, request UpdateInstrumentRequest) (*UpdateInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response UpdateInstrumentResponse + err = c.apiClient.Patch( + common.BuildPath(instruments.Path, instrumentId), + auth, + request, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Delete(instrumentId string) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Delete(common.BuildPath(instruments.Path, instrumentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/instruments/abc/client_test.go b/instruments/abc/client_test.go new file mode 100644 index 0000000..c679eb1 --- /dev/null +++ b/instruments/abc/client_test.go @@ -0,0 +1,487 @@ +package abc + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/instruments" + "github.com/checkout/checkout-sdk-go/mocks" +) + +var ( + tokenId = "token" + instrumentId = "src_wmlfc3zyhqzehihu7giusaaawu" + email = "bruce@wayne-enterprises.com" + name = "Bruce Wayne" + instrumentType = instruments.Card + customer = common.CustomerResponse{ + Email: email, + Name: name, + } +) + +func TestCreate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + instrumentResponse = CreateInstrumentResponse{ + HttpMetadata: httpMetadata, + Type: instrumentType, + Id: instrumentId, + Customer: &customer, + } + ) + + cases := []struct { + name string + request CreateInstrumentRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*CreateInstrumentResponse, error) + }{ + { + name: "when request is correct then create instrument", + request: CreateInstrumentRequest{ + Type: instruments.Card, + Token: tokenId, + Customer: &InstrumentCustomerRequest{ + Email: email, + Name: name, + }, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CreateInstrumentResponse) + *respMapping = instrumentResponse + }) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, instrumentResponse.Type, response.Type) + assert.Equal(t, instrumentResponse.Id, response.Id) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: CreateInstrumentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_required", + }, + }, + }) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Create(tc.request)) + }) + } +} + +func TestGet(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + instrument = GetInstrumentResponse{ + HttpMetadata: httpMetadata, + Type: instrumentType, + Id: instrumentId, + Customer: &instruments.InstrumentCustomerResponse{ + Email: email, + Name: name, + }, + } + ) + + cases := []struct { + name string + instrumentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetInstrumentResponse, error) + }{ + { + name: "when instrument exists then return instrument info", + instrumentId: instrumentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetInstrumentResponse) + *respMapping = instrument + }) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, instrument.Id, response.Id) + assert.Equal(t, instrument.Type, response.Type) + assert.Equal(t, instrument.Customer, response.Customer) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Get(tc.instrumentId)) + }) + } +} + +func TestUpdate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + updateResponse = UpdateInstrumentResponse{ + HttpMetadata: httpMetadata, + Type: instrumentType, + } + ) + + cases := []struct { + name string + instrumentId string + request UpdateInstrumentRequest + getAuthorization func(*mock.Mock) mock.Call + apiPatch func(*mock.Mock) mock.Call + checker func(*UpdateInstrumentResponse, error) + }{ + { + name: "when request is correct then update instrument", + instrumentId: instrumentId, + request: UpdateInstrumentRequest{ + ExpiryMonth: 01, + ExpiryYear: 30, + Name: name, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*UpdateInstrumentResponse) + *respMapping = updateResponse + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + assert.Equal(t, updateResponse.Type, response.Type) + }, + }, + { + name: "when credentials invalid then return error", + instrumentId: instrumentId, + request: UpdateInstrumentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + request: UpdateInstrumentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + instrumentId: instrumentId, + request: UpdateInstrumentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_invalid", + }, + }, + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPatch(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Update(tc.instrumentId, tc.request)) + }) + } +} + +func TestDelete(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + instrumentId string + getAuthorization func(*mock.Mock) mock.Call + apiDelete func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when request is correct then delete instrument", + instrumentId: instrumentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiDelete(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Delete(tc.instrumentId)) + }) + } +} diff --git a/instruments/abc/create.go b/instruments/abc/create.go new file mode 100644 index 0000000..37556af --- /dev/null +++ b/instruments/abc/create.go @@ -0,0 +1,42 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + CreateInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Token string `json:"token,omitempty"` + AccountHolder *InstrumentAccountHolder `json:"account_holder,omitempty"` + Customer *InstrumentCustomerRequest `json:"customer,omitempty"` + } + + CreateInstrumentResponse struct { + HttpMetadata common.HttpMetadata + Type instruments.InstrumentType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Scheme string `json:"scheme,omitempty"` + Last4 string `json:"last4,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + } +) + +type InstrumentCustomerRequest struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + IsDefault bool `json:"nas,omitempty"` +} diff --git a/instruments/abc/get.go b/instruments/abc/get.go new file mode 100644 index 0000000..77ef1b7 --- /dev/null +++ b/instruments/abc/get.go @@ -0,0 +1,29 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + GetInstrumentResponse struct { + HttpMetadata common.HttpMetadata + Type instruments.InstrumentType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Scheme string `json:"scheme,omitempty"` + Last4 string `json:"last4,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + AccountHolder *InstrumentAccountHolder `json:"account_holder,omitempty"` + Customer *instruments.InstrumentCustomerResponse `json:"customer,omitempty"` + } +) diff --git a/instruments/abc/instruments.go b/instruments/abc/instruments.go new file mode 100644 index 0000000..f7eda4c --- /dev/null +++ b/instruments/abc/instruments.go @@ -0,0 +1,10 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +type InstrumentAccountHolder struct { + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` +} diff --git a/instruments/abc/update.go b/instruments/abc/update.go new file mode 100644 index 0000000..96c97b3 --- /dev/null +++ b/instruments/abc/update.go @@ -0,0 +1,29 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + UpdateInstrumentRequest struct { + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + AccountHolder *InstrumentAccountHolder `json:"account_holder,omitempty"` + Customer *InstrumentCustomerUpdateRequest `json:"customer,omitempty"` + } + + UpdateInstrumentResponse struct { + HttpMetadata common.HttpMetadata + Type instruments.InstrumentType `json:"type" binding:"required"` + Fingerprint string `json:"fingerprint,omitempty"` + } +) + +type ( + InstrumentCustomerUpdateRequest struct { + Id string `json:"id,omitempty"` + IsDefault bool `json:"nas,omitempty"` + } +) diff --git a/instruments/client.go b/instruments/client.go deleted file mode 100644 index 1a503ba..0000000 --- a/instruments/client.go +++ /dev/null @@ -1,80 +0,0 @@ -package instruments - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" - "github.com/checkout/checkout-sdk-go/payments" -) - -const path = "instruments" - -// Client ... -type Client struct { - API checkout.HTTPClient -} - -// NewClient ... -func NewClient(config checkout.Config) *Client { - return &Client{ - API: httpclient.NewClient(config), - } -} - -// Create an instrument -func (c *Client) Create(request *Request) (*Response, error) { - resp, err := c.API.Post("/"+path, request, nil) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusCreated { - var source payments.SourceResponse - err = json.Unmarshal(resp.ResponseBody, &source) - response.Source = &source - return response, err - } - return response, err -} - -// Get instrument details -func (c *Client) Get(sourceID string) (*Response, error) { - - resp, err := c.API.Get(fmt.Sprintf("/%v/%v", path, sourceID)) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var source payments.SourceResponse - err = json.Unmarshal(resp.ResponseBody, &source) - response.Source = &source - return response, err - } - return response, err -} - -// Update instrument details -func (c *Client) Update(sourceID string, request *Request) (*Response, error) { - resp, err := c.API.Patch(fmt.Sprintf("/%v/%v", path, sourceID), request) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var instrumentResponse InstrumentResponse - err = json.Unmarshal(resp.ResponseBody, &instrumentResponse) - response.InstrumentResponse = &instrumentResponse - return response, err - } - return response, err -} diff --git a/instruments/instrument.go b/instruments/instrument.go deleted file mode 100644 index 916980a..0000000 --- a/instruments/instrument.go +++ /dev/null @@ -1,53 +0,0 @@ -package instruments - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" - "github.com/checkout/checkout-sdk-go/payments" -) - -type ( - // Request - - Request struct { - *Instrument - *Source - } - // Instrument - - Instrument struct { - Type string `json:"type" binding:"required"` - Token string `json:"token" binding:"required"` - } - // Source - - Source struct { - ExpiryMonth uint64 `json:"expiry_month,omitempty"` - ExpiryYear uint64 `json:"expiry_year,omitempty"` - Name string `json:"name,omitempty"` - AccountHolder *AccountHolder `json:"account_holder,omitempty"` - Customer *Customer `json:"customer,omitempty"` - } - // AccountHolder - - AccountHolder struct { - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - // Customer - - Customer struct { - ID string `json:"id,omitempty"` - Default *bool `json:"default,omitempty"` - } -) - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Source *payments.SourceResponse `json:"source,omitempty"` - InstrumentResponse *InstrumentResponse `json:"instrument_response,omitempty"` - } - - // InstrumentResponse - - InstrumentResponse struct { - Type string `json:"type" binding:"required"` - Fingerprint string `json:"fingerprint,omitempty"` - } -) diff --git a/instruments/instruments.go b/instruments/instruments.go new file mode 100644 index 0000000..c37496c --- /dev/null +++ b/instruments/instruments.go @@ -0,0 +1,21 @@ +package instruments + +import "github.com/checkout/checkout-sdk-go/common" + +const Path = "instruments" + +type InstrumentType string + +const ( + Card InstrumentType = "card" + BankAccount InstrumentType = "bank_account" + Token InstrumentType = "token" +) + +type InstrumentCustomerResponse struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + Default bool `json:"nas,omitempty"` +} diff --git a/instruments/nas/client.go b/instruments/nas/client.go new file mode 100644 index 0000000..b807d1c --- /dev/null +++ b/instruments/nas/client.go @@ -0,0 +1,91 @@ +package nas + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) Create(request CreateInstrumentRequest) (*CreateInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response CreateInstrumentResponse + err = c.apiClient.Post( + common.BuildPath(instruments.Path), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Get(instrumentId string) (*GetInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response GetInstrumentResponse + err = c.apiClient.Get(common.BuildPath(instruments.Path, instrumentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Update(instrumentId string, request UpdateInstrumentRequest) (*UpdateInstrumentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response UpdateInstrumentResponse + err = c.apiClient.Patch( + common.BuildPath(instruments.Path, instrumentId), + auth, + request, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Delete(instrumentId string) (*common.MetadataResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Delete(common.BuildPath(instruments.Path, instrumentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/instruments/nas/client_test.go b/instruments/nas/client_test.go new file mode 100644 index 0000000..181876b --- /dev/null +++ b/instruments/nas/client_test.go @@ -0,0 +1,565 @@ +package nas + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/instruments" + "github.com/checkout/checkout-sdk-go/mocks" +) + +const ( + instrumentId = "src_wmlfc3zyhqzehihu7giusaaawu" +) + +var ( + accountHolder = common.AccountHolder{ + FirstName: "Bruce", + LastName: "Wayne", + } + + customerRequest = CreateCustomerInstrumentRequest{ + Id: "cus_y3oqhf46pyzuxjbcn2giaqnb44", + Email: "bruce@wayne-enterprises.com", + Name: "Bruce Wayne", + } + + customerResponse = common.CustomerResponse{ + Id: "cus_y3oqhf46pyzuxjbcn2giaqnb44", + Email: "bruce@wayne-enterprises.com", + Name: "Bruce Wayne", + } + + bank = common.BankDetails{ + Name: "Lloyds TSB", + Branch: "Bournemouth", + } +) + +func TestCreate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + token = CreateTokenInstrumentResponse{ + Type: instruments.Card, + Id: "src_wmlfc3zyhqzehihu7giusaaawu", + CustomerResponse: &customerResponse, + ExpiryMonth: 6, + ExpiryYear: 2025, + Last4: "1234", + } + + createTokenResponse = CreateInstrumentResponse{ + HttpMetadata: httpMetadata, + CreateTokenInstrumentResponse: &token, + } + + bankAccount = CreateBankAccountInstrumentResponse{ + Type: instruments.BankAccount, + Id: "src_wmlfc3zyhqzehihu7giusaaawu", + CustomerResponse: &customerResponse, + BankDetails: &bank, + SwiftBic: "37040044", + AccountNumber: "12345", + Iban: "HU93116000060000000012345676", + } + + createBankAccountResponse = CreateInstrumentResponse{ + HttpMetadata: httpMetadata, + CreateBankAccountInstrumentResponse: &bankAccount, + } + ) + + cases := []struct { + name string + request CreateInstrumentRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*CreateInstrumentResponse, error) + }{ + { + name: "when request is for token instrument then create token instrument", + request: getCreateTokenInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CreateInstrumentResponse) + *respMapping = createTokenResponse + }) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.CreateTokenInstrumentResponse) + assert.Equal(t, token.Id, response.CreateTokenInstrumentResponse.Id) + }, + }, + { + name: "when request is for bank account instrument then create bank account instrument", + request: getCreateBankAccountInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CreateInstrumentResponse) + *respMapping = createBankAccountResponse + }) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.CreateBankAccountInstrumentResponse) + assert.Equal(t, bankAccount.Id, response.CreateBankAccountInstrumentResponse.Id) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: NewCreateTokenInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_required", + }, + }, + }) + }, + checker: func(response *CreateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Create(tc.request)) + }) + } +} + +func getCreateTokenInstrumentRequest() *createTokenInstrumentRequest { + r := NewCreateTokenInstrumentRequest() + r.Token = "tok_asoto22g2fsu7prwomy12sgfsa" + r.AccountHolder = &accountHolder + r.Customer = &customerRequest + return r +} + +func getCreateBankAccountInstrumentRequest() *createBankAccountInstrumentRequest { + r := NewCreateBankAccountInstrumentRequest() + r.AccountType = common.Savings + r.AccountNumber = "12345" + r.Iban = "HU93116000060000000012345676" + r.SwiftBic = "37040044" + r.Currency = common.GBP + r.Country = common.GB + r.AccountHolder = &accountHolder + r.BankDetails = &bank + return r +} + +func TestGet(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + cardInstrument = GetCardInstrumentResponse{ + Type: instruments.Card, + Id: "src_wmlfc3zyhqzehihu7giusaaawu", + ExpiryMonth: 6, + ExpiryYear: 2025, + Last4: "1234", + } + + response = GetInstrumentResponse{ + HttpMetadata: httpMetadata, + GetCardInstrumentResponse: &cardInstrument, + } + ) + + cases := []struct { + name string + instrumentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetInstrumentResponse, error) + }{ + { + name: "when instrument exists then return instrument info", + instrumentId: instrumentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetInstrumentResponse) + *respMapping = response + }) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.GetCardInstrumentResponse) + assert.Equal(t, cardInstrument.Id, response.GetCardInstrumentResponse.Id) + assert.Equal(t, cardInstrument.Type, response.GetCardInstrumentResponse.Type) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Get(tc.instrumentId)) + }) + } +} + +func TestUpdate(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + updateCardResponse = UpdateCardInstrumentResponse{ + Type: instruments.Card, + Fingerprint: "smoua2sbuqhupeofwbe77n5nsm", + } + + response = UpdateInstrumentResponse{ + HttpMetadata: httpMetadata, + UpdateCardInstrumentResponse: &updateCardResponse, + } + ) + + cases := []struct { + name string + instrumentId string + request UpdateInstrumentRequest + getAuthorization func(*mock.Mock) mock.Call + apiPatch func(*mock.Mock) mock.Call + checker func(*UpdateInstrumentResponse, error) + }{ + { + name: "when request is correct then update instrument", + instrumentId: instrumentId, + request: NewUpdateCardInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*UpdateInstrumentResponse) + *respMapping = response + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.UpdateCardInstrumentResponse) + }, + }, + { + name: "when credentials invalid then return error", + instrumentId: instrumentId, + request: NewUpdateCardInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + request: NewUpdateCardInstrumentRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + instrumentId: instrumentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPatch: func(m *mock.Mock) mock.Call { + return *m.On("Patch", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_invalid", + }, + }, + }) + }, + checker: func(response *UpdateInstrumentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPatch(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Update(tc.instrumentId, tc.request)) + }) + } +} + +func TestDelete(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + instrumentId string + getAuthorization func(*mock.Mock) mock.Call + apiDelete func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when request is correct then delete instrument", + instrumentId: instrumentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when instrument not found then return error", + instrumentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiDelete: func(m *mock.Mock) mock.Call { + return *m.On("Delete", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiDelete(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Delete(tc.instrumentId)) + }) + } +} diff --git a/instruments/nas/create.go b/instruments/nas/create.go new file mode 100644 index 0000000..1023981 --- /dev/null +++ b/instruments/nas/create.go @@ -0,0 +1,122 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + CreateInstrumentRequest interface{} + + createBankAccountInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + AccountType common.AccountType `json:"account_type,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + BranchCode string `json:"branch_code,omitempty"` + Iban string `json:"iban,omitempty"` + Bban string `json:"bban,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Country common.Country `json:"country,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + BankDetails *common.BankDetails `json:"bank,omitempty"` + Customer *CreateCustomerInstrumentRequest `json:"customer,omitempty"` + } + + createTokenInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Token string `json:"token" binding:"required"` + AccountHolder *common.AccountHolder `json:"account_holder" binding:"required"` + Customer *CreateCustomerInstrumentRequest `json:"customer,omitempty"` + } +) + +func NewCreateBankAccountInstrumentRequest() *createBankAccountInstrumentRequest { + return &createBankAccountInstrumentRequest{ + Type: instruments.BankAccount, + } +} + +func NewCreateTokenInstrumentRequest() *createTokenInstrumentRequest { + return &createTokenInstrumentRequest{ + Type: instruments.Token, + } +} + +type ( + CreateInstrumentResponse struct { + HttpMetadata common.HttpMetadata + CreateBankAccountInstrumentResponse *CreateBankAccountInstrumentResponse + CreateTokenInstrumentResponse *CreateTokenInstrumentResponse + AlternativeResponse *common.AlternativeResponse + } + + CreateBankAccountInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + // common + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + CustomerResponse *common.CustomerResponse `json:"customer,omitempty"` + // specific + BankDetails *common.BankDetails `json:"bank,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + Iban string `json:"iban,omitempty"` + } + + CreateTokenInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + // common + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + CustomerResponse *common.CustomerResponse `json:"customer,omitempty"` + // specific + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Scheme string `json:"scheme,omitempty"` + SchemeLocal string `json:"scheme_local,omitempty"` + Last4 string `json:"last4,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + } +) + +func (s *CreateInstrumentResponse) UnmarshalJSON(data []byte) error { + var typeMapping common.TypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Type { + case string(instruments.BankAccount): + var response CreateBankAccountInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.CreateBankAccountInstrumentResponse = &response + case string(instruments.Card): + var response CreateTokenInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.CreateTokenInstrumentResponse = &response + default: + var response common.AlternativeResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.AlternativeResponse = &response + } + + return nil +} diff --git a/instruments/nas/get.go b/instruments/nas/get.go new file mode 100644 index 0000000..6072766 --- /dev/null +++ b/instruments/nas/get.go @@ -0,0 +1,87 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + GetInstrumentResponse struct { + HttpMetadata common.HttpMetadata + GetCardInstrumentResponse *GetCardInstrumentResponse + GetBankAccountInstrumentResponse *GetBankAccountInstrumentResponse + AlternativeResponse *common.AlternativeResponse + } + + GetCardInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Customer *instruments.InstrumentCustomerResponse `json:"customer,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Scheme string `json:"scheme,omitempty"` + SchemeLocal string `json:"scheme_local,omitempty"` + Last4 string `json:"last4,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + } + + GetBankAccountInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Customer *instruments.InstrumentCustomerResponse `json:"customer,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + + AccountType common.AccountType `json:"account_type,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + Iban string `json:"iban,omitempty"` + Bban string `json:"bban,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Country common.Country `json:"country,omitempty"` + BankDetails *common.BankDetails `json:"bank,omitempty"` + } +) + +func (s *GetInstrumentResponse) UnmarshalJSON(data []byte) error { + var typeMapping common.TypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Type { + case string(instruments.BankAccount): + var response GetBankAccountInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.GetBankAccountInstrumentResponse = &response + case string(instruments.Card): + var response GetCardInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.GetCardInstrumentResponse = &response + default: + var response common.AlternativeResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.AlternativeResponse = &response + } + + return nil +} diff --git a/instruments/nas/instuments.go b/instruments/nas/instuments.go new file mode 100644 index 0000000..a05179d --- /dev/null +++ b/instruments/nas/instuments.go @@ -0,0 +1,13 @@ +package nas + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +type CreateCustomerInstrumentRequest struct { + Id string `json:"id,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + Default bool `json:"nas,omitempty"` +} diff --git a/instruments/nas/update.go b/instruments/nas/update.go new file mode 100644 index 0000000..26c202e --- /dev/null +++ b/instruments/nas/update.go @@ -0,0 +1,114 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/instruments" +) + +type ( + UpdateInstrumentRequest interface{} + + updateBankAccountInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + AccountType common.AccountType `json:"account_type,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + BranchCode string `json:"branch_code,omitempty"` + Iban string `json:"iban,omitempty"` + Bban string `json:"bban,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Country common.Country `json:"country,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + BankDetails *common.BankDetails `json:"bank,omitempty"` + Customer *CreateCustomerInstrumentRequest `json:"customer,omitempty"` + } + + updateCardInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + ExpiryMonth int `json:"expiry_month" binding:"required"` + ExpiryYear int `json:"expiry_year" binding:"required"` + Name string `json:"name" binding:"required"` + Customer *common.UpdateCustomerRequest `json:"customer" binding:"required"` + AccountHolder *common.AccountHolder `json:"account_holder" binding:"required"` + } + + updateTokenInstrumentRequest struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Token string `json:"account_number,omitempty"` + } +) + +func NewUpdateBankAccountInstrumentRequest() *updateBankAccountInstrumentRequest { + return &updateBankAccountInstrumentRequest{ + Type: instruments.BankAccount, + } +} + +func NewUpdateCardInstrumentRequest() *updateCardInstrumentRequest { + return &updateCardInstrumentRequest{ + Type: instruments.Card, + } +} + +func NewUpdateTokenInstrumentRequest() *updateTokenInstrumentRequest { + return &updateTokenInstrumentRequest{ + Type: instruments.Token, + } +} + +type ( + UpdateInstrumentResponse struct { + HttpMetadata common.HttpMetadata + UpdateCardInstrumentResponse *UpdateCardInstrumentResponse + UpdateBankAccountInstrumentResponse *UpdateBankAccountInstrumentResponse + AlternativeResponse *common.AlternativeResponse + } + + // UpdateCardInstrumentResponse TODO review this response struct to check if we need both + UpdateCardInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + } + + // UpdateBankAccountInstrumentResponse TODO review this response struct to check if we need both + UpdateBankAccountInstrumentResponse struct { + Type instruments.InstrumentType `json:"type" binding:"required"` + Id string `json:"id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + } +) + +func (s *UpdateInstrumentResponse) UnmarshalJSON(data []byte) error { + var typeMapping common.TypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Type { + case string(instruments.BankAccount): + var response UpdateBankAccountInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.UpdateBankAccountInstrumentResponse = &response + case string(instruments.Card): + var response UpdateCardInstrumentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.UpdateCardInstrumentResponse = &response + default: + var response common.AlternativeResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil + } + s.AlternativeResponse = &response + } + + return nil +} diff --git a/log.go b/log.go deleted file mode 100644 index 912f9ae..0000000 --- a/log.go +++ /dev/null @@ -1,104 +0,0 @@ -package checkout - -import ( - "fmt" - "io" - "os" -) - -const ( - // LevelNull sets a logger to show no messages at all - LevelNull Level = 0 - // LevelError sets a logger to show error messages only - LevelError Level = 1 - // LevelWarn sets a logger to show warning messages or anything more severe. - LevelWarn Level = 2 - // LevelInfo sets a logger to show informational messages or anything more severe. - LevelInfo Level = 3 - // LevelDebug sets a logger to show information messages or anything more severe. - LevelDebug Level = 4 -) - -// DefaultLeveledLogger - -var DefaultLeveledLogger LeveledLoggerInterface = &LeveledLogger{ - Level: LevelError, -} - -// Level represents a logging level. -type Level uint32 - -// LeveledLogger is a leveled logger implementation. -// -// It prints warnings and errors to `os.Stderr` and other messages to -// `os.Stdout`. -type LeveledLogger struct { - // Level is the minimum logging level that will be emitted by this logger. - // - // For example, a Level set to LevelWarn will emit warnings and errors, but not information or debug messages. - // - // Always set this with a constant like LevelWarn because the individual values are not guaranteed to be stable. - Level Level - - // Internal testing use only. - stderrOverride io.Writer - stdoutOverride io.Writer -} - -// Debugf - logs a debug message using Printf conventions. -func (l *LeveledLogger) Debugf(format string, v ...interface{}) { - if l.Level >= LevelDebug { - fmt.Fprintf(l.stdout(), "[DEBUG] "+format+"\n", v...) - } -} - -// Errorf - logs a warning message using Printf conventions. -func (l *LeveledLogger) Errorf(format string, v ...interface{}) { - if l.Level >= LevelError { - fmt.Fprintf(l.stdout(), "[ERROR] "+format+"\n", v...) - } -} - -// Infof - logs an informational message using Printf conventions. -func (l *LeveledLogger) Infof(format string, v ...interface{}) { - if l.Level >= LevelInfo { - fmt.Fprintf(l.stdout(), "[INFO] "+format+"\n", v...) - } -} - -// Warnf - logs a warning message using Printf conventions. -func (l *LeveledLogger) Warnf(format string, v ...interface{}) { - if l.Level >= LevelWarn { - fmt.Fprintf(l.stderr(), "[WARN] "+format+"\n", v...) - } -} - -func (l *LeveledLogger) stderr() io.Writer { - if l.stderrOverride != nil { - return l.stderrOverride - } - return os.Stderr -} - -func (l *LeveledLogger) stdout() io.Writer { - if l.stdoutOverride != nil { - return l.stdoutOverride - } - return os.Stdout -} - -// LeveledLoggerInterface provides a basic leveled logging interface for -// printing debug, informational, warning, and error messages. -// -// It's implemented by LeveledLogger and also provides out-of-the-box -// compatibility with a Logrus Logger, but may require a thin shim for use with -// other logging libraries that you use less standard conventions like Zap. -type LeveledLoggerInterface interface { - // Debugf logs a debug message using Printf conventions. - Debugf(format string, v ...interface{}) - // Errorf logs a warning message using Printf conventions. - Errorf(format string, v ...interface{}) - // Infof logs an informational message using Printf conventions. - Infof(format string, v ...interface{}) - // Warnf logs a warning message using Printf conventions. - Warnf(format string, v ...interface{}) -} diff --git a/log_test.go b/log_test.go deleted file mode 100644 index 7cf59a0..0000000 --- a/log_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package checkout - -import ( - "bytes" - "testing" - - assert "github.com/stretchr/testify/require" -) - -// -// Tests -// - -func TestDefaultLeveledLogger(t *testing.T) { - _, ok := DefaultLeveledLogger.(*LeveledLogger) - assert.True(t, ok) -} - -// -// LeveledLogger -// - -func TestLeveledLoggerDebugf(t *testing.T) { - var stdout, stderr bytes.Buffer - logger := &LeveledLogger{stdoutOverride: &stdout, stderrOverride: &stderr} - - { - clearBuffers(&stdout, &stderr) - logger.Level = LevelDebug - - logger.Debugf("test") - assert.Equal(t, "[DEBUG] test\n", stdout.String()) - assert.Equal(t, "", stderr.String()) - } - - // Expect no logging - for _, level := range []Level{LevelInfo, LevelWarn, LevelError} { - clearBuffers(&stdout, &stderr) - logger.Level = level - - logger.Debugf("test") - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) - } -} - -func TestLeveledLoggerInfof(t *testing.T) { - var stdout, stderr bytes.Buffer - logger := &LeveledLogger{stdoutOverride: &stdout, stderrOverride: &stderr} - - for _, level := range []Level{LevelDebug, LevelInfo} { - clearBuffers(&stdout, &stderr) - logger.Level = level - - logger.Infof("test") - assert.Equal(t, "[INFO] test\n", stdout.String()) - assert.Equal(t, "", stderr.String()) - } - - // Expect no logging - for _, level := range []Level{LevelWarn, LevelError} { - clearBuffers(&stdout, &stderr) - logger.Level = level - - logger.Infof("test") - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) - } -} - -func TestLeveledLoggerWarnf(t *testing.T) { - var stdout, stderr bytes.Buffer - logger := &LeveledLogger{stdoutOverride: &stdout, stderrOverride: &stderr} - - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn} { - clearBuffers(&stdout, &stderr) - logger.Level = level - - logger.Warnf("test") - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "[WARN] test\n", stderr.String()) - } - - // Expect no logging - { - clearBuffers(&stdout, &stderr) - logger.Level = LevelError - - logger.Warnf("test") - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) - } -} - -func TestLeveledLoggerErrorf(t *testing.T) { - var stdout, stderr bytes.Buffer - logger := &LeveledLogger{stdoutOverride: &stdout, stderrOverride: &stderr} - - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError} { - clearBuffers(&stdout, &stderr) - logger.Level = level - - logger.Errorf("test") - assert.Equal(t, "[ERROR] test\n", stdout.String()) - assert.Equal(t, "", stderr.String()) - } -} - -// -// Private functions -// - -func clearBuffers(buffers ...*bytes.Buffer) { - for _, b := range buffers { - b.Truncate(0) - } -} diff --git a/mocks/client_mock.go b/mocks/client_mock.go new file mode 100644 index 0000000..dfd2556 --- /dev/null +++ b/mocks/client_mock.go @@ -0,0 +1,72 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" +) + +type ( + ApiClientMock struct{ mock.Mock } +) + +func (m *ApiClientMock) Get(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error { + args := m.Called(path, authorization, responseMapping) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} + +func (m *ApiClientMock) Post(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error { + args := m.Called(path, authorization, request, responseMapping, idempotencyKey) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} + +func (m *ApiClientMock) Put(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}, idempotencyKey *string) error { + args := m.Called(path, authorization, request, responseMapping, idempotencyKey) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} + +func (m *ApiClientMock) Patch(path string, authorization *configuration.SdkAuthorization, request interface{}, responseMapping interface{}) error { + args := m.Called(path, authorization, request, responseMapping) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} + +func (m *ApiClientMock) Delete(path string, authorization *configuration.SdkAuthorization, responseMapping interface{}) error { + args := m.Called(path, authorization, responseMapping) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} + +func (m *ApiClientMock) Upload(path string, authorization *configuration.SdkAuthorization, request *common.FileUploadRequest, responseMapping interface{}) error { + args := m.Called(path, authorization, request, responseMapping) + + if args.Get(0) != nil { + return args.Get(0).(error) + } + + return nil +} diff --git a/mocks/credentials_mock.go b/mocks/credentials_mock.go new file mode 100644 index 0000000..7c191c2 --- /dev/null +++ b/mocks/credentials_mock.go @@ -0,0 +1,21 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/configuration" +) + +type ( + CredentialsMock struct{ mock.Mock } +) + +func (m *CredentialsMock) GetAuthorization(authorizationType configuration.AuthorizationType) (*configuration.SdkAuthorization, error) { + args := m.Called(authorizationType) + + if args.Get(0) != nil { + return args.Get(0).(*configuration.SdkAuthorization), nil + } + + return nil, args.Get(1).(error) +} diff --git a/mocks/environment_mock.go b/mocks/environment_mock.go new file mode 100644 index 0000000..90935c5 --- /dev/null +++ b/mocks/environment_mock.go @@ -0,0 +1,31 @@ +package mocks + +import "github.com/stretchr/testify/mock" + +type ( + EnvironmentMock struct{ mock.Mock } +) + +func (m *EnvironmentMock) BaseUri() string { + return "" +} + +func (m *EnvironmentMock) AuthorizationUri() string { + return "" +} + +func (m *EnvironmentMock) FilesUri() string { + return "" +} + +func (m *EnvironmentMock) TransfersUri() string { + return "" +} + +func (m *EnvironmentMock) BalancesUri() string { + return "" +} + +func (m *EnvironmentMock) IsSandbox() bool { + return true +} diff --git a/nas/checkout_api.go b/nas/checkout_api.go new file mode 100644 index 0000000..b004c60 --- /dev/null +++ b/nas/checkout_api.go @@ -0,0 +1,59 @@ +package nas + +import ( + "github.com/checkout/checkout-sdk-go/accounts" + "github.com/checkout/checkout-sdk-go/apm/ideal" + "github.com/checkout/checkout-sdk-go/apm/klarna" + "github.com/checkout/checkout-sdk-go/apm/sepa" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/customers" + "github.com/checkout/checkout-sdk-go/disputes" + "github.com/checkout/checkout-sdk-go/forex" + instruments "github.com/checkout/checkout-sdk-go/instruments/nas" + payments "github.com/checkout/checkout-sdk-go/payments/nas" + "github.com/checkout/checkout-sdk-go/sessions" + "github.com/checkout/checkout-sdk-go/tokens" +) + +type Api struct { + Tokens *tokens.Client + Instruments *instruments.Client + Customers *customers.Client + Payments *payments.Client + Disputes *disputes.Client + Forex *forex.Client + Accounts *accounts.Client + Sessions *sessions.Client + + Ideal *ideal.Client + Klarna *klarna.Client + Sepa *sepa.Client +} + +func CheckoutApi(configuration *configuration.Configuration) *Api { + apiClient := buildBaseClient(configuration) + + api := Api{} + api.Tokens = tokens.NewClient(configuration, apiClient) + api.Instruments = instruments.NewClient(configuration, apiClient) + api.Customers = customers.NewClient(configuration, apiClient) + api.Payments = payments.NewClient(configuration, apiClient) + api.Disputes = disputes.NewClient(configuration, apiClient) + api.Forex = forex.NewClient(configuration, apiClient) + api.Accounts = accounts.NewClient(configuration, apiClient, buildFilesClient(configuration)) + api.Sessions = sessions.NewClient(configuration, apiClient) + + api.Ideal = ideal.NewClient(configuration, apiClient) + api.Klarna = klarna.NewClient(configuration, apiClient) + api.Sepa = sepa.NewClient(configuration, apiClient) + return &api +} + +func buildBaseClient(configuration *configuration.Configuration) client.HttpClient { + return client.NewApiClient(configuration, configuration.Environment.BaseUri()) +} + +func buildFilesClient(configuration *configuration.Configuration) client.HttpClient { + return client.NewApiClient(configuration, configuration.Environment.FilesUri()) +} diff --git a/nas/checkout_default_sdk_builder.go b/nas/checkout_default_sdk_builder.go new file mode 100644 index 0000000..a298812 --- /dev/null +++ b/nas/checkout_default_sdk_builder.go @@ -0,0 +1,48 @@ +package nas + +import ( + "net/http" + + "github.com/checkout/checkout-sdk-go/configuration" +) + +type CheckoutDefaultSdkBuilder struct { + configuration.StaticKeysBuilder +} + +func (b *CheckoutDefaultSdkBuilder) WithEnvironment(environment configuration.Environment) *CheckoutDefaultSdkBuilder { + b.Environment = environment + return b +} + +func (b *CheckoutDefaultSdkBuilder) WithHttpClient(client *http.Client) *CheckoutDefaultSdkBuilder { + b.HttpClient = client + return b +} + +func (b *CheckoutDefaultSdkBuilder) WithPublicKey(publicKey string) *CheckoutDefaultSdkBuilder { + b.PublicKey = publicKey + return b +} + +func (b *CheckoutDefaultSdkBuilder) WithSecretKey(secretKey string) *CheckoutDefaultSdkBuilder { + b.SecretKey = secretKey + return b +} + +func (b *CheckoutDefaultSdkBuilder) Build() (*Api, error) { + err := b.ValidateSecretKey(configuration.DefaultSecretKeyPattern) + if err != nil { + return nil, err + } + + err = b.ValidatePublicKey(configuration.DefaultPublicKeyPattern) + if err != nil { + return nil, err + } + + sdkCredentials := configuration.NewDefaultKeysSdkCredentials(b.SecretKey, b.PublicKey) + newConfiguration := configuration.NewConfiguration(sdkCredentials, b.Environment, b.HttpClient) + + return CheckoutApi(newConfiguration), nil +} diff --git a/nas/checkout_oauth_sdk_builder.go b/nas/checkout_oauth_sdk_builder.go new file mode 100644 index 0000000..85f74d1 --- /dev/null +++ b/nas/checkout_oauth_sdk_builder.go @@ -0,0 +1,65 @@ +package nas + +import ( + "net/http" + + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" +) + +type CheckoutOAuthSdkBuilder struct { + configuration.SdkBuilder + ClientId string + ClientSecret string + AuthorizationUri string + Scopes []string +} + +func (b *CheckoutOAuthSdkBuilder) WithClientCredentials(id string, secret string) *CheckoutOAuthSdkBuilder { + b.ClientId = id + b.ClientSecret = secret + return b +} + +func (b *CheckoutOAuthSdkBuilder) WithAuthorizationUri(uri string) *CheckoutOAuthSdkBuilder { + b.AuthorizationUri = uri + return b +} + +func (b *CheckoutOAuthSdkBuilder) WithScopes(scopes []string) *CheckoutOAuthSdkBuilder { + b.Scopes = scopes + return b +} + +func (b *CheckoutOAuthSdkBuilder) WithEnvironment(environment configuration.Environment) *CheckoutOAuthSdkBuilder { + b.Environment = environment + return b +} + +func (b *CheckoutOAuthSdkBuilder) WithHttpClient(client *http.Client) *CheckoutOAuthSdkBuilder { + b.HttpClient = client + return b +} + +func (b *CheckoutOAuthSdkBuilder) Build() (*Api, error) { + if b.ClientId == "" || b.ClientSecret == "" { + return nil, errors.CheckoutArgumentError("Invalid OAuth 'client_id' or 'client_secret'") + } + + if b.AuthorizationUri == "" { + b.AuthorizationUri = b.SdkBuilder.Environment.AuthorizationUri() + } + + sdkCredentials, err := configuration.NewOAuthSdkCredentials( + b.ClientId, + b.ClientSecret, + b.AuthorizationUri, + b.Scopes) + if err != nil { + return nil, err + } + + newConfiguration := configuration.NewConfiguration(sdkCredentials, b.Environment, b.HttpClient) + + return CheckoutApi(newConfiguration), nil +} diff --git a/nas/default_sdk_builder.go b/nas/default_sdk_builder.go new file mode 100644 index 0000000..118eb2e --- /dev/null +++ b/nas/default_sdk_builder.go @@ -0,0 +1,5 @@ +package nas + +type DefaultSdkBuilder interface { + Build() (*Api, error) +} diff --git a/params.go b/params.go deleted file mode 100644 index 2c88f66..0000000 --- a/params.go +++ /dev/null @@ -1,42 +0,0 @@ -package checkout - -import ( - "crypto/rand" - "encoding/base64" - "fmt" - "net/http" - "time" -) - -// Params is the structure that contains the common properties -// of any *Params structure. -type Params struct { - // Headers may be used to provide extra header lines on the HTTP request. - Headers http.Header `form:"-"` - IdempotencyKey *string `form:"-"` // Passed as header -} - -// GetParams - -func (p *Params) GetParams() *Params { - return p -} - -// SetIdempotencyKey sets a value for the Idempotency-Key header. -func (p *Params) SetIdempotencyKey(val string) { - p.IdempotencyKey = &val -} - -// ParamsContainer is a general interface for which all parameter structs -// should comply. They achieve this by embedding a Params struct and inheriting -// its implementation of this interface. -type ParamsContainer interface { - GetParams() *Params -} - -// NewIdempotencyKey - -func NewIdempotencyKey() string { - now := time.Now().UnixNano() - buf := make([]byte, 4) - rand.Read(buf) - return fmt.Sprintf("%v_%v", now, base64.URLEncoding.EncodeToString(buf)[:6]) -} diff --git a/payments/abc/client.go b/payments/abc/client.go new file mode 100644 index 0000000..75afb3d --- /dev/null +++ b/payments/abc/client.go @@ -0,0 +1,171 @@ +package abc + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/payments" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) RequestPayment(request PaymentRequest, idempotencyKey *string) (*PaymentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response PaymentResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments), + auth, + request, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) RequestPayout(request PayoutRequest, idempotencyKey *string) (*PaymentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response PaymentResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments), + auth, + request, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetPaymentDetails(paymentId string) (*GetPaymentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response GetPaymentResponse + err = c.apiClient.Get(common.BuildPath(payments.PathPayments, paymentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetPaymentActions(paymentId string) (*GetPaymentActionsResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response GetPaymentActionsResponse + err = c.apiClient.Get( + common.BuildPath(payments.PathPayments, paymentId, "actions"), + auth, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CapturePayment( + paymentId string, + captureRequest CaptureRequest, + idempotencyKey *string, +) (*payments.CaptureResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response payments.CaptureResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "captures"), + auth, + captureRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) RefundPayment( + paymentId string, + refundRequest *payments.RefundRequest, + idempotencyKey *string, +) (*payments.RefundResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response payments.RefundResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "refunds"), + auth, + refundRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) VoidPayment( + paymentId string, + voidRequest *payments.VoidRequest, + idempotencyKey *string, +) (*payments.VoidResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) + if err != nil { + return nil, err + } + + var response payments.VoidResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "voids"), + auth, + voidRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/payments/abc/client_test.go b/payments/abc/client_test.go new file mode 100644 index 0000000..b2f7880 --- /dev/null +++ b/payments/abc/client_test.go @@ -0,0 +1,981 @@ +package abc + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" + "github.com/checkout/checkout-sdk-go/payments" + "github.com/checkout/checkout-sdk-go/payments/abc/sources" +) + +var ( + amount = 100 + currency = common.GBP + reference = "reference" + description = "description" + + paymentId = "pay_1234" + + actionId = "1234" + actionTypeAuth = payments.AuthorizationYes + actionCapture = payments.Capture +) + +func TestRequestPayment(t *testing.T) { + var ( + paymentRequest = PaymentRequest{ + Source: sources.NewRequestCardSource(), + Amount: amount, + Currency: currency, + Reference: reference, + Description: description, + Capture: false, + } + + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + paymentResponse = PaymentResponse{ + HttpMetadata: httpMetadata, + Amount: amount, + Id: paymentId, + Currency: currency, + Source: &SourceResponse{ + ResponseCardSource: &ResponseCardSource{ + Type: payments.CardSource, + }, + }, + Reference: reference, + } + ) + + cases := []struct { + name string + request PaymentRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*PaymentResponse, error) + }{ + { + name: "when request is correct then return payment", + request: paymentRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*PaymentResponse) + *respMapping = paymentResponse + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentResponse.Id, response.Id) + assert.Equal(t, paymentResponse.Amount, response.Amount) + assert.Equal(t, paymentResponse.Currency, response.Currency) + assert.Equal(t, paymentResponse.Source.ResponseCardSource.Type, response.Source.ResponseCardSource.Type) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: PaymentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestPayment(tc.request, tc.idempotencyKey)) + }) + } +} + +func TestRequestPayout(t *testing.T) { + var ( + cardDestination = NewRequestCardDestination() + + payoutRequest = PayoutRequest{ + Destination: cardDestination, + Amount: amount, + Currency: currency, + Reference: reference, + Description: description, + Capture: false, + } + + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + paymentResponse = PaymentResponse{ + HttpMetadata: httpMetadata, + Amount: amount, + Id: paymentId, + Currency: currency, + Reference: reference, + } + ) + + cases := []struct { + name string + request PayoutRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*PaymentResponse, error) + }{ + { + name: "when request is correct then return payout", + request: payoutRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*PaymentResponse) + *respMapping = paymentResponse + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentResponse.Id, response.Id) + assert.Equal(t, paymentResponse.Amount, response.Amount) + assert.Equal(t, paymentResponse.Currency, response.Currency) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: PayoutRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestPayout(tc.request, tc.idempotencyKey)) + }) + } +} + +func TestGetPaymentDetails(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + paymentResponse = GetPaymentResponse{ + HttpMetadata: httpMetadata, + Id: paymentId, + Amount: amount, + Currency: currency, + Reference: reference, + Description: description, + } + ) + + cases := []struct { + name string + paymentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetPaymentResponse, error) + }{ + { + name: "when paymentId is correct then return payment", + paymentId: paymentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetPaymentResponse) + *respMapping = paymentResponse + }) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentResponse.Id, response.Id) + assert.Equal(t, paymentResponse.Amount, response.Amount) + assert.Equal(t, paymentResponse.Currency, response.Currency) + assert.Equal(t, paymentResponse.Reference, response.Reference) + assert.Equal(t, paymentResponse.Description, response.Description) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetPaymentDetails(tc.paymentId)) + }) + } +} + +func TestGetPaymentActions(t *testing.T) { + var ( + auth = PaymentAction{ + Type: actionTypeAuth, + Amount: amount, + } + + capture = PaymentAction{ + Type: actionCapture, + Amount: amount, + } + + paymentActions = []PaymentAction{auth, capture} + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: 200, + } + + paymentActionsResponse = GetPaymentActionsResponse{ + HttpMetadata: httpMetadata, + Actions: paymentActions, + } + ) + + cases := []struct { + name string + paymentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetPaymentActionsResponse, error) + }{ + { + name: "when request is valid then return payment actions", + paymentId: paymentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetPaymentActionsResponse) + *respMapping = paymentActionsResponse + }) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentActionsResponse.Actions, response.Actions) + + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetPaymentActions(tc.paymentId)) + }) + } +} + +func TestCapturePayment(t *testing.T) { + var ( + captureRequest = CaptureRequest{ + Amount: amount, + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + captureResponse = payments.CaptureResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request CaptureRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.CaptureResponse, error) + }{ + { + name: "when request is correct then capture payment", + paymentId: paymentId, + request: captureRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.CaptureResponse) + *respMapping = captureResponse + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, captureResponse.ActionId, response.ActionId) + assert.Equal(t, captureResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when capture not allowed then return error", + paymentId: paymentId, + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + paymentId: paymentId, + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CapturePayment(tc.paymentId, tc.request, tc.idempotencyKey)) + }) + } +} + +func TestRefundPayment(t *testing.T) { + var ( + refundRequest = payments.RefundRequest{ + Amount: amount, + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + refundResponse = payments.RefundResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request payments.RefundRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.RefundResponse, error) + }{ + { + name: "when request is correct then refund payment", + paymentId: paymentId, + request: refundRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.RefundResponse) + *respMapping = refundResponse + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, refundResponse.ActionId, response.ActionId) + assert.Equal(t, refundResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when refund not allowed then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RefundPayment(tc.paymentId, &tc.request, tc.idempotencyKey)) + }) + } +} + +func TestVoidPayment(t *testing.T) { + var ( + voidRequest = payments.VoidRequest{ + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + voidResponse = payments.VoidResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request payments.VoidRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.VoidResponse, error) + }{ + { + name: "when request is correct then void payment", + paymentId: paymentId, + request: voidRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.VoidResponse) + *respMapping = voidResponse + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, voidResponse.ActionId, response.ActionId) + assert.Equal(t, voidResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when void not allowed then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.VoidPayment(tc.paymentId, &tc.request, tc.idempotencyKey)) + }) + } +} diff --git a/payments/abc/destinations.go b/payments/abc/destinations.go new file mode 100644 index 0000000..090e3cc --- /dev/null +++ b/payments/abc/destinations.go @@ -0,0 +1,96 @@ +package abc + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + RequestCardDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Name string `json:"name,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } + + RequestIdDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + } + + RequestTokenDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + Token string `json:"token,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } +) + +func NewRequestCardDestination() *RequestCardDestination { + return &RequestCardDestination{Type: payments.CardDestination} +} + +func NewRequestIdDestination() *RequestIdDestination { + return &RequestIdDestination{Type: payments.IdDestination} +} + +func NewRequestTokenDestination() *RequestTokenDestination { + return &RequestTokenDestination{Type: payments.TokenDestination} +} + +type ( + DestinationResponse struct { + *ResponseCardDestination + *common.AlternativeResponse + } + + ResponseCardDestination struct { + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Last4 string `json:"last4,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + } +) + +func (s *DestinationResponse) UnmarshalJSON(data []byte) error { + var typeMapping payments.DestinationTypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Destination { + case string(payments.CardDestination): + var typeMapping ResponseCardDestination + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.ResponseCardDestination = &typeMapping + default: + var typeMapping common.AlternativeResponse + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.AlternativeResponse = &typeMapping + } + + return nil +} diff --git a/payments/abc/payments.go b/payments/abc/payments.go new file mode 100644 index 0000000..79be3dd --- /dev/null +++ b/payments/abc/payments.go @@ -0,0 +1,159 @@ +package abc + +import ( + "encoding/json" + "time" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type FundTransferType string + +const ( + AA FundTransferType = "AA" + PP FundTransferType = "PP" + FT FundTransferType = "FT" + FD FundTransferType = "FD" + PD FundTransferType = "PD" + LO FundTransferType = "LO" + OG FundTransferType = "OG" +) + +// Request +type ( + PaymentRequest struct { + Source interface{} `json:"source,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + PaymentType payments.PaymentType `json:"payment_type,omitempty"` + MerchantInitiated bool `json:"merchant_initiated"` + Reference string `json:"reference,omitempty"` + Description string `json:"description,omitempty"` + Capture bool `json:"capture"` + CaptureOn time.Time `json:"capture_on,omitempty"` + Customer *common.CustomerRequest `json:"customer,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + ShippingDetails *payments.ShippingDetails `json:"shipping,omitempty"` + PreviousPaymentId string `json:"previous_payment_id,omitempty"` + Risk *payments.RiskRequest `json:"risk,omitempty"` + SuccessUrl string `json:"success_url,omitempty"` + FailureUrl string `json:"failure_url,omitempty"` + PaymentIp string `json:"payment_ip,omitempty"` + ThreeDsRequest *payments.ThreeDsRequest `json:"3ds,omitempty"` + PaymentRecipient *payments.PaymentRecipient `json:"recipient,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Processing map[string]interface{} `json:"processing,omitempty"` + } + + PayoutRequest struct { + Destination interface{} `json:"destination,omitempty"` + Amount int `json:"amount,omitempty"` + FundTransferType FundTransferType `json:"fund_transfer_type,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + PaymentType payments.PaymentType `json:"payment_type,omitempty"` + Reference string `json:"reference,omitempty"` + Description string `json:"description,omitempty"` + Capture bool `json:"capture"` + CaptureOn time.Time `json:"capture_on,omitempty"` + Customer *common.CustomerRequest `json:"customer,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + ShippingDetails *payments.ShippingDetails `json:"shipping,omitempty"` + PreviousPaymentId string `json:"previous_payment_id,omitempty"` + Risk *payments.RiskRequest `json:"risk,omitempty"` + SuccessUrl string `json:"success_url,omitempty"` + FailureUrl string `json:"failure_url,omitempty"` + PaymentIp string `json:"payment_ip,omitempty"` + PaymentRecipient *payments.PaymentRecipient `json:"recipient,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Processing map[string]interface{} `json:"processing,omitempty"` + } + + CaptureRequest struct { + Amount int `json:"amount,omitempty"` + Reference string `json:"reference,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } +) + +// Response +type ( + PaymentResponse struct { + HttpMetadata common.HttpMetadata + ActionId string `json:"action_id,omitempty"` + Amount int `json:"amount,omitempty"` + Approved bool `json:"approved,omitempty"` + AuthCode string `json:"auth_code,omitempty"` + Id string `json:"id,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + Source *SourceResponse `json:"source,omitempty"` + Status payments.PaymentStatus `json:"status,omitempty"` + ThreeDs *payments.ThreeDsEnrollment `json:"3ds,omitempty"` + Reference string `json:"reference,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + ResponseSummary string `json:"response_summary,omitempty"` + Risk *payments.RiskAssessment `json:"risk,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + Processing *payments.PaymentProcessing `json:"processing,omitempty"` + Eci string `json:"eci,omitempty"` + SchemeId string `json:"scheme_id,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + GetPaymentResponse struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + RequestedOn time.Time `json:"requested_on,omitempty"` + Source interface{} `json:"source,omitempty"` + Destination interface{} `json:"destination,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + PaymentType payments.PaymentType `json:"payment_type,omitempty"` + Reference string `json:"reference,omitempty"` + Description string `json:"description,omitempty"` + Approved bool `json:"approved,omitempty"` + Status payments.PaymentStatus `json:"status,omitempty"` + ThreeDs *payments.ThreeDsData `json:"3ds,omitempty"` + Risk *payments.RiskAssessment `json:"risk,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + ShippingDetails *payments.ShippingDetails `json:"shipping,omitempty"` + PaymentIp string `json:"payment_ip,omitempty"` + PaymentRecipient *payments.PaymentRecipient `json:"recipient,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Eci string `json:"eci,omitempty"` + SchemeId string `json:"scheme_id,omitempty"` + Actions []payments.PaymentActionSummary `json:"actions,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + GetPaymentActionsResponse struct { + HttpMetadata common.HttpMetadata + Actions []PaymentAction `json:"actions,omitempty"` + } + + PaymentAction struct { + Id string `json:"id,omitempty"` + Type payments.ActionType `json:"type,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + Amount int `json:"amount,omitempty"` + Approved bool `json:"approved,omitempty"` + AuthCode string `json:"auth_code,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + ResponseSummary string `json:"response_summary,omitempty"` + Reference string `json:"reference,omitempty"` + Processing *payments.Processing `json:"processing,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Links map[string]common.Link `json:"_links"` + } +) + +func (p *GetPaymentActionsResponse) UnmarshalJSON(data []byte) error { + var actions []PaymentAction + if err := json.Unmarshal(data, &actions); err != nil { + return err + } + p.Actions = actions + return nil +} diff --git a/payments/abc/source.go b/payments/abc/source.go new file mode 100644 index 0000000..6177c77 --- /dev/null +++ b/payments/abc/source.go @@ -0,0 +1,65 @@ +package abc + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + SourceResponse struct { + ResponseCardSource *ResponseCardSource + AlternativeResponse *common.AlternativeResponse + } + + ResponseCardSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Scheme string `json:"scheme,omitempty"` + SchemeLocal string `json:"scheme_local,omitempty"` + Last4 string `json:"last4,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + AvsCheck string `json:"avs_check,omitempty"` + CvvCheck string `json:"cvv_check,omitempty"` + Payouts bool `json:"payouts,omitempty"` + FastFunds string `json:"fast_funds,omitempty"` + PaymentAccountReference string `json:"payment_account_reference,omitempty"` + } +) + +func (s *SourceResponse) UnmarshalJSON(data []byte) error { + var typeMapping common.TypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Type { + case string(payments.CardSource): + var typeMapping ResponseCardSource + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.ResponseCardSource = &typeMapping + default: + var typeMapping common.AlternativeResponse + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.AlternativeResponse = &typeMapping + } + + return nil +} diff --git a/payments/abc/sources/apm/apm.go b/payments/abc/sources/apm/apm.go new file mode 100644 index 0000000..bbad222 --- /dev/null +++ b/payments/abc/sources/apm/apm.go @@ -0,0 +1,286 @@ +package apm + +import ( + "time" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + RequestAlipaySource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestBancontactSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + Language string `json:"language,omitempty"` + } + + RequestBenefitPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + IntegrationType IntegrationType `json:"integration_type,omitempty"` + } + + RequestBoletoSource struct { + Type payments.SourceType `json:"type,omitempty"` + IntegrationType IntegrationType `json:"integration_type,omitempty"` + Country common.Country `json:"country,omitempty"` + Description string `json:"description,omitempty"` + Payer *payments.Payer `json:"payer,omitempty"` + } + + RequestEpsSource struct { + Type payments.SourceType `json:"type,omitempty"` + Purpose string `json:"purpose,omitempty"` + Bic string `json:"bic,omitempty"` + } + + RequestFawrySource struct { + Type payments.SourceType `json:"type,omitempty"` + Description string `json:"description,omitempty"` + CustomerProfileId string `json:"customer_profile_id,omitempty"` + CustomerEmail string `json:"customer_email,omitempty"` + CustomerMobile string `json:"customer_mobile,omitempty"` + ExpiresOn time.Time `json:"expires_on,omitempty"` + Products []FawryProduct `json:"products,omitempty"` + } + + RequestGiropaySource struct { + Type payments.SourceType `json:"type,omitempty"` + Purpose string `json:"purpose,omitempty"` + Bic string `json:"bic,omitempty"` + InfoFields []InfoFields `json:"info_fields,omitempty"` + } + + RequestIdealSource struct { + Type payments.SourceType `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Bic string `json:"bic,omitempty"` + Language string `json:"language,omitempty"` + } + + RequestKlarnaSource struct { + Type payments.SourceType `json:"type,omitempty"` + AuthorizationToken string `json:"authorization_token,omitempty"` + Locale string `json:"locale,omitempty"` + PurchaseCountry string `json:"purchase_country,omitempty"` + AutoCapture bool `json:"auto_capture,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + ShippingAddress map[string]interface{} `json:"shipping_address,omitempty"` + TaxAmount int `json:"tax_amount,omitempty"` + Products []map[string]interface{} `json:"products,omitempty"` + Customer map[string]interface{} `json:"customer,omitempty"` + MerchantReference1 string `json:"merchant_reference1,omitempty"` + MerchantReference2 string `json:"merchant_reference2,omitempty"` + MerchantData string `json:"merchant_data,omitempty"` + Attachment map[string]interface{} `json:"attachment,omitempty"` + CustomPaymentMethodIds []map[string]string `json:"custom_payment_method_ids,omitempty"` + } + + RequestKnetSource struct { + Type payments.SourceType `json:"type,omitempty"` + Language string `json:"language,omitempty"` + UserDefinedField1 string `json:"user_defined_field1,omitempty"` + UserDefinedField2 string `json:"user_defined_field2,omitempty"` + UserDefinedField3 string `json:"user_defined_field3,omitempty"` + UserDefinedField4 string `json:"user_defined_field4,omitempty"` + UserDefinedField5 string `json:"user_defined_field5,omitempty"` + CardToken string `json:"card_token,omitempty"` + Ptlf string `json:"ptlf,omitempty"` + } + + RequestMultiBancoSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + } + + RequestOxxoSource struct { + Type payments.SourceType `json:"type,omitempty"` + IntegrationType IntegrationType `json:"integration_type,omitempty"` + Country common.Country `json:"country,omitempty"` + Description string `json:"description,omitempty"` + Payer *payments.Payer `json:"payer,omitempty"` + } + + RequestP24Source struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + AccountHolderEmail string `json:"account_holder_email,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + } + + RequestPagoFacilSource struct { + Type payments.SourceType `json:"type,omitempty"` + IntegrationType IntegrationType `json:"integration_type,omitempty"` + Country common.Country `json:"country,omitempty"` + Description string `json:"description,omitempty"` + Payer *payments.Payer `json:"payer,omitempty"` + } + + RequestPayPalSource struct { + Type payments.SourceType `json:"type,omitempty"` + InvoiceNumber string `json:"invoice_number,omitempty"` + RecipientName string `json:"recipient_name,omitempty"` + LogoUrl string `json:"logo_url,omitempty"` + Stc map[string]interface{} `json:"stc,omitempty"` + } + + RequestPoliSource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestQPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + Quantity int `json:"quantity,omitempty"` + Description string `json:"description,omitempty"` + Language string `json:"language,omitempty"` + NationalId string `json:"national_id,omitempty"` + } + + RequestRapiPagoSource struct { + Type payments.SourceType `json:"type,omitempty"` + IntegrationType IntegrationType `json:"integration_type,omitempty"` + Country common.Country `json:"country,omitempty"` + Description string `json:"description,omitempty"` + Payer *payments.Payer `json:"payer,omitempty"` + } + + RequestSepaSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + } + + RequestSofortSource struct { + Type payments.SourceType `json:"type,omitempty"` + CountryCode common.Country `json:"countryCode,omitempty"` + LanguageCode string `json:"languageCode,omitempty"` + } +) + +func NewRequestAlipaySource() *RequestAlipaySource { + return &RequestAlipaySource{Type: payments.AlipaySource} +} + +func NewRequestBancontactSource() *RequestBancontactSource { + return &RequestBancontactSource{ + Type: payments.BancontactSource, + } +} + +func NewRequestBenefitPaySource() *RequestBenefitPaySource { + return &RequestBenefitPaySource{ + Type: payments.BenefitPaySource, + IntegrationType: Mobile, + } +} + +func NewRequestBoletoSource() *RequestBoletoSource { + return &RequestBoletoSource{ + Type: payments.BoletoSource, + } +} + +func NewRequestEpsSource() *RequestEpsSource { + return &RequestEpsSource{Type: payments.EpsSource} +} + +func NewRequestFawrySource() *RequestFawrySource { + return &RequestFawrySource{Type: payments.FawrySource} +} + +func NewRequestGiropaySource() *RequestGiropaySource { + return &RequestGiropaySource{Type: payments.GiropaySource} +} + +func NewRequestIdealSource() *RequestIdealSource { + return &RequestIdealSource{Type: payments.IdealSource} +} + +func NewRequestKlarnaSource() *RequestKlarnaSource { + return &RequestKlarnaSource{Type: payments.KlarnaSource} +} + +func NewRequestKnetSource() *RequestKnetSource { + return &RequestKnetSource{Type: payments.KnetSource} +} + +func NewRequestMultiBancoSource() *RequestMultiBancoSource { + return &RequestMultiBancoSource{ + Type: payments.MultiBancoSource, + } +} + +func NewRequestOxxoSource() *RequestOxxoSource { + return &RequestOxxoSource{ + Type: payments.OxxoSource, + } +} + +func NewRequestP24Source() *RequestP24Source { + return &RequestP24Source{ + Type: payments.P24Source, + } +} + +func NewRequestPagoFacilSource() *RequestPagoFacilSource { + return &RequestPagoFacilSource{ + Type: payments.PagoFacilSource, + IntegrationType: Redirect, + } +} + +func NewRequestPayPalSource() *RequestPayPalSource { + return &RequestPayPalSource{Type: payments.PayPalSource} +} + +func NewRequestPoliSource() *RequestPoliSource { + return &RequestPoliSource{Type: payments.PoliSource} +} + +func NewRequestQPaySource() *RequestQPaySource { + return &RequestQPaySource{Type: payments.QPaySource} +} + +func NewRequestRapiPagoSource() *RequestRapiPagoSource { + return &RequestRapiPagoSource{ + Type: payments.RapiPagoSource, + IntegrationType: Redirect, + } +} + +func NewRequestSepaSource() *RequestSepaSource { + return &RequestSepaSource{Type: payments.SepaSource} +} + +func NewRequestSofortSource() *RequestSofortSource { + return &RequestSofortSource{Type: payments.SofortSource} +} + +type IntegrationType string + +const ( + Direct IntegrationType = "direct" + Redirect IntegrationType = "redirect" + Mobile IntegrationType = "mobile" +) + +type InfoFields struct { + Label string `json:"label,omitempty"` + Text string `json:"text,omitempty"` +} + +type ( + FawryProduct struct { + ProductId string `json:"product_id,omitempty"` + Quantity int `json:"quantity,omitempty"` + Price int64 `json:"price,omitempty"` + Description string `json:"description,omitempty"` + } +) diff --git a/payments/abc/sources/sources.go b/payments/abc/sources/sources.go new file mode 100644 index 0000000..581bb6e --- /dev/null +++ b/payments/abc/sources/sources.go @@ -0,0 +1,91 @@ +package sources + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + RequestCardSource struct { + Type payments.SourceType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Cvv string `json:"cvv,omitempty"` + Stored bool `json:"stored,omitempty"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } + + RequestIdSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Cvv string `json:"cvv,omitempty"` + } + + RequestCustomerSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"number,omitempty"` + } + + RequestTokenSource struct { + Type payments.SourceType `json:"type,omitempty"` + Token string `json:"token,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + } + + RequestNetworkTokenSource struct { + Type payments.SourceType `json:"type,omitempty"` + Token string `json:"token,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + TokenType payments.NetworkTokenType `json:"token_type,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Eci string `json:"eci,omitempty"` + Stored bool `json:"stored"` + Name string `json:"name,omitempty"` + Cvv string `json:"cvv,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } + + RequestDLocalSource struct { + Type payments.SourceType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Cvv string `json:"cvv,omitempty"` + Stored bool `json:"stored"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } +) + +func NewRequestCardSource() *RequestCardSource { + return &RequestCardSource{Type: payments.CardSource} +} + +func NewRequestIdSource() *RequestIdSource { + return &RequestIdSource{Type: payments.IdSource} +} + +func NewRequestCustomerSource() *RequestCustomerSource { + return &RequestCustomerSource{Type: payments.CustomerSource} +} + +func NewRequestTokenSource() *RequestTokenSource { + return &RequestTokenSource{Type: payments.TokenSource} +} + +func NewRequestNetworkTokenSource() *RequestNetworkTokenSource { + return &RequestNetworkTokenSource{Type: payments.NetworkTokenSource} +} + +func NewRequestDLocalSource() *RequestDLocalSource { + return &RequestDLocalSource{Type: payments.DLocalSource} +} diff --git a/payments/actions.go b/payments/actions.go deleted file mode 100644 index 2a080a7..0000000 --- a/payments/actions.go +++ /dev/null @@ -1,67 +0,0 @@ -package payments - -import ( - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -const ( - // Authorized ... - Authorized string = "Authorized" - // Canceled ... - Canceled string = "Canceled" - // Captured ... - Captured string = "Captured" - // Declined ... - Declined string = "Declined" - // Expired ... - Expired string = "Expired" - // PartiallyCaptured ... - PartiallyCaptured string = "Partially Captured" - // PartiallyRefunded ... - PartiallyRefunded string = "Partially Refunded" - // Pending ... - Pending string = "Pending" - // Refunded ... - Refunded string = "Refunded" - // Voided ... - Voided string = "Voided" - // CardVerified ... - CardVerified string = "Card Verified" - // Chargeback ... - Chargeback string = "Chargeback" -) - -type ( - // ActionsResponse ... - ActionsResponse struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Actions []*Action `json:"actions,omitempty"` - } -) - -type ( - // Action ... - Action struct { - ID string `json:"id,omitempty"` - Type common.PaymentAction `json:"type,omitempty"` - ProcessedOn time.Time `json:"processed_on,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Approved *bool `json:"approved,omitempty"` - AuthCode string `json:"auth_code,omitempty"` - Reference string `json:"reference,omitempty"` - ResponseCode string `json:"response_code,omitempty"` - ResponseSummary *string `json:"response_summary,omitempty"` - Processing *Processing `json:"processing,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - } - // ActionSummary ... - ActionSummary struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - ResponseCode string `json:"response_code,omitempty"` - ResponseSummary *string `json:"response_summary,omitempty"` - } -) diff --git a/payments/authorizations.go b/payments/authorizations.go deleted file mode 100644 index 5740199..0000000 --- a/payments/authorizations.go +++ /dev/null @@ -1,52 +0,0 @@ -package payments - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" - "time" -) - -// AuthorizationRequest .. -type AuthorizationRequest struct { - Amount uint64 `json:"amount,omitempty"` - Reference string `json:"reference,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -// AuthorizationResponse ... -type AuthorizationResponse struct { - ActionID string `json:"action_id,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency,omitempty"` - Approved *bool `json:"approved,omitempty"` - Status common.PaymentAction `json:"status,omitempty"` - AuthCode string `json:"auth_code,omitempty"` - ResponseCode string `json:"response_code,omitempty"` - ResponseSummary string `json:"response_summary,omitempty"` - ExpiresOn time.Time `json:"expires_on,omitempty"` - Balances *Balances `json:"balances,omitempty"` - ProcessedOn time.Time `json:"processed_on,omitempty"` - Reference string `json:"reference,omitempty"` - Processing *PaymentProcessing `json:"processing,omitempty"` // review - ECI string `json:"eci,omitempty"` - SchemeID string `json:"scheme_id,omitempty"` - Risk RiskAssessment `json:"risk,omitempty"` - Links map[string]common.Link `json:"_links,omitempty"` - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` -} - -type Balances struct { - TotalAuthorized uint64 `json:"total_authorized,omitempty"` - TotalVoided uint64 `json:"total_voided,omitempty"` - AvailableToVoid uint64 `json:"available_to_void,omitempty"` - TotalCaptured uint64 `json:"total_captured,omitempty"` - AvailableToCapture uint64 `json:"available_to_capture,omitempty"` - TotalRefunded uint64 `json:"total_refunded,omitempty"` - AvailableToRefund uint64 `json:"available_to_refund,omitempty"` -} - -type PaymentProcessing struct { - RetrievalReferenceNumber string `json:"retrieval_reference_number,omitempty"` - AcquirerTransactionId string `json:"acquirer_transaction_id,omitempty"` - RecommendationCode string `json:"recommendation_code,omitempty"` -} diff --git a/payments/authorizations_test.go b/payments/authorizations_test.go deleted file mode 100644 index 80b83fa..0000000 --- a/payments/authorizations_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package payments - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "os" - "testing" -) - -func TestIncrementAuthorization(t *testing.T) { - - pk := os.Getenv("CHECKOUT_FOUR_PUBLIC_KEY") - sk := os.Getenv("CHECKOUT_FOUR_SECRET_KEY") - - config, err := checkout.SdkConfig(&sk, &pk, checkout.Sandbox) - if err != nil { - require.Fail(t, "failed creating the creating the configuration!") - } - var paymentsClient = NewClient(*config) - - paymentResponse := requestPayment(t, paymentsClient) - assert.NotNil(t, paymentResponse) - - var authorizationRequest = AuthorizationRequest{ - Amount: 10, - Reference: "Auth Reference", - } - authorizationResponse, err := paymentsClient.IncrementAuthorization(paymentResponse.Processed.ID, &authorizationRequest, nil) - - assert.Nil(t, err) - assert.NotNil(t, authorizationResponse) - assert.NotNil(t, authorizationResponse.Amount) - assert.NotNil(t, authorizationResponse.ActionID) - assert.NotNil(t, authorizationResponse.Currency) - assert.NotNil(t, authorizationResponse.Approved) - assert.NotNil(t, authorizationResponse.ResponseCode) - assert.NotNil(t, authorizationResponse.ResponseSummary) - assert.NotNil(t, authorizationResponse.ExpiresOn) - assert.NotNil(t, authorizationResponse.ProcessedOn) - assert.NotNil(t, authorizationResponse.Balances) - assert.NotNil(t, authorizationResponse.Links) - assert.NotNil(t, authorizationResponse.Risk) - -} - -func requestPayment(t *testing.T, client *Client) *Response { - - var source = CardSource{ - Type: common.Card.String(), - Number: "4556447238607884", - ExpiryMonth: 6, - ExpiryYear: 2025, - CVV: "100", - Stored: checkout.Bool(false), - } - var request = &Request{ - Capture: checkout.Bool(false), - Reference: "Payment Reference", - Amount: 10, - Currency: "EUR", - AuthorizationType: "Estimated", - Source: source, - Customer: &Customer{ - Email: "example@email.com", - Name: "First Name Last Name", - }, - Metadata: map[string]string{ - "test1": "test_metadata_1", - }, - } - response, err := client.Request(request, nil) - if err != nil || response.Processed == nil { - require.Fail(t, "payment request failed!") - } - return response -} diff --git a/payments/captures.go b/payments/captures.go deleted file mode 100644 index 72a1822..0000000 --- a/payments/captures.go +++ /dev/null @@ -1,33 +0,0 @@ -package payments - -import ( - "github.com/checkout/checkout-sdk-go" -) - -// CaptureType ... -type CaptureType string - -const ( - // NonFinal ... - NonFinal CaptureType = "NonFinal" - // Final ... - Final CaptureType = "Final" -) - -func (c CaptureType) String() string { - return string(c) -} - -// CapturesRequest .. -type CapturesRequest struct { - Amount uint64 `json:"amount,omitempty"` - Reference string `json:"reference,omitempty"` - CaptureType CaptureType `json:"capture_type,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// CapturesResponse ... -type CapturesResponse struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Accepted *Accepted `json:"accepted,omitempty"` -} diff --git a/payments/captures_test.go b/payments/captures_test.go deleted file mode 100644 index ad870e0..0000000 --- a/payments/captures_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package payments - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "os" - "testing" -) - -func TestCapturePayment(t *testing.T) { - - pk := os.Getenv("CHECKOUT_FOUR_PUBLIC_KEY") - sk := os.Getenv("CHECKOUT_FOUR_SECRET_KEY") - - config, err := checkout.SdkConfig(&sk, &pk, checkout.Sandbox) - if err != nil { - require.Fail(t, "failed creating the creating the configuration!") - } - var paymentsClient = NewClient(*config) - - paymentResponse := requestCardPayment(t, paymentsClient) - assert.NotNil(t, paymentResponse) - - var captureRequest = CapturesRequest{ - Amount: 5, - Reference: "Capture Reference", - CaptureType: Final, - } - captureResponse, err := paymentsClient.Captures(paymentResponse.Processed.ID, &captureRequest, nil) - - assert.Nil(t, err) - assert.NotNil(t, captureResponse) - assert.NotNil(t, captureResponse.Accepted) - assert.NotNil(t, captureResponse.Accepted.ActionID) - assert.NotNil(t, captureResponse.Accepted.Reference) - assert.NotNil(t, captureResponse.Accepted.Links) - -} - -func requestCardPayment(t *testing.T, client *Client) *Response { - - var source = CardSource{ - Type: common.Card.String(), - Number: "4556447238607884", - ExpiryMonth: 6, - ExpiryYear: 2025, - CVV: "100", - Stored: checkout.Bool(false), - } - var request = &Request{ - Capture: checkout.Bool(false), - Reference: "Payment Reference", - Amount: 10, - Currency: "EUR", - Source: source, - Customer: &Customer{ - Email: "example@email.com", - Name: "First Name Last Name", - }, - Metadata: map[string]string{ - "test1": "test_metadata_1", - }, - } - response, err := client.Request(request, nil) - if err != nil || response.Processed == nil { - require.Fail(t, "payment request failed!") - } - return response -} diff --git a/payments/client.go b/payments/client.go deleted file mode 100644 index 2fc2d90..0000000 --- a/payments/client.go +++ /dev/null @@ -1,131 +0,0 @@ -package payments - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" -) - -const path = "payments" - -// Client ... -type Client struct { - API checkout.HTTPClient -} - -// NewClient ... -func NewClient(config checkout.Config) *Client { - return &Client{ - API: httpclient.NewClient(config), - } -} - -// Request ... -func (c *Client) Request(request *Request, params *checkout.Params) (*Response, error) { - response, err := c.API.Post("/"+path, request, params) - resp := &Response{ - StatusResponse: response, - } - if err != nil { - return resp, err - } - if response.StatusCode == http.StatusCreated { - var processed Processed - err = json.Unmarshal(response.ResponseBody, &processed) - resp.Processed = &processed - } else if response.StatusCode == http.StatusAccepted { - var pending PaymentPending - err = json.Unmarshal(response.ResponseBody, &pending) - resp.Pending = &pending - } - return resp, err -} - -// Get ... -func (c *Client) Get(paymentID string) (*PaymentResponse, error) { - response, err := c.API.Get(fmt.Sprintf("/%v/%v", path, paymentID)) - payment := &PaymentResponse{ - StatusResponse: response, - } - if err != nil { - return payment, err - } - var paymentDetails Payment - err = json.Unmarshal(response.ResponseBody, &paymentDetails) - payment.Payment = &paymentDetails - return payment, err -} - -// Actions ... -func (c *Client) Actions(paymentID string) (*ActionsResponse, error) { - response, err := c.API.Get(fmt.Sprintf("/%v/%v/actions", path, paymentID)) - act := &ActionsResponse{ - StatusResponse: response, - } - if err != nil { - return act, err - } - actions := make([]*Action, 0) - err = json.Unmarshal(response.ResponseBody, &actions) - act.Actions = actions - return act, err -} - -// Captures ... -func (c *Client) Captures(paymentID string, request *CapturesRequest, params *checkout.Params) (*CapturesResponse, error) { - response, err := c.API.Post(fmt.Sprintf("/%v/%v/captures", path, paymentID), request, params) - cap := &CapturesResponse{ - StatusResponse: response, - } - if err != nil { - return cap, err - } - - var accepted Accepted - err = json.Unmarshal(response.ResponseBody, &accepted) - cap.Accepted = &accepted - return cap, err -} - -// Refunds ... -func (c *Client) Refunds(paymentID string, request *RefundsRequest, params *checkout.Params) (*RefundsResponse, error) { - response, err := c.API.Post(fmt.Sprintf("/%v/%v/refunds", path, paymentID), request, params) - ref := &RefundsResponse{ - StatusResponse: response, - } - if err != nil { - return ref, err - } - - var accepted Accepted - err = json.Unmarshal(response.ResponseBody, &accepted) - ref.Accepted = &accepted - return ref, err -} - -// Voids ... -func (c *Client) Voids(paymentID string, request *VoidsRequest, params *checkout.Params) (*VoidsResponse, error) { - response, err := c.API.Post(fmt.Sprintf("/%v/%v/voids", path, paymentID), request, params) - void := &VoidsResponse{ - StatusResponse: response, - } - if err != nil { - return void, err - } - - var accepted Accepted - err = json.Unmarshal(response.ResponseBody, &accepted) - void.Accepted = &accepted - return void, err -} - -// IncrementAuthorization ... -func (c *Client) IncrementAuthorization(paymentID string, request *AuthorizationRequest, params *checkout.Params) (*AuthorizationResponse, error) { - response, err := c.API.Post(fmt.Sprintf("/%v/%v/authorizations", path, paymentID), request, params) - var authorization *AuthorizationResponse - err = json.Unmarshal(response.ResponseBody, &authorization) - return authorization, err -} diff --git a/payments/nas/client.go b/payments/nas/client.go new file mode 100644 index 0000000..629e765 --- /dev/null +++ b/payments/nas/client.go @@ -0,0 +1,171 @@ +package nas + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/payments" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) RequestPayment(request PaymentRequest, idempotencyKey *string) (*PaymentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response PaymentResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments), + auth, + request, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) RequestPayout(request PayoutRequest, idempotencyKey *string) (*PayoutResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response PayoutResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments), + auth, + request, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetPaymentDetails(paymentId string) (*GetPaymentResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response GetPaymentResponse + err = c.apiClient.Get(common.BuildPath(payments.PathPayments, paymentId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) GetPaymentActions(paymentId string) (*GetPaymentActionsResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response GetPaymentActionsResponse + err = c.apiClient.Get( + common.BuildPath(payments.PathPayments, paymentId, "actions"), + auth, + &response, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CapturePayment( + paymentId string, + captureRequest CaptureRequest, + idempotencyKey *string, +) (*payments.CaptureResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response payments.CaptureResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "captures"), + auth, + captureRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) RefundPayment( + paymentId string, + refundRequest *payments.RefundRequest, + idempotencyKey *string, +) (*payments.RefundResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response payments.RefundResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "refunds"), + auth, + refundRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) VoidPayment( + paymentId string, + voidRequest *payments.VoidRequest, + idempotencyKey *string, +) (*payments.VoidResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKeyOrOauth) + if err != nil { + return nil, err + } + + var response payments.VoidResponse + err = c.apiClient.Post( + common.BuildPath(payments.PathPayments, paymentId, "voids"), + auth, + voidRequest, + &response, + idempotencyKey, + ) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/payments/nas/client_test.go b/payments/nas/client_test.go new file mode 100644 index 0000000..f9a08b4 --- /dev/null +++ b/payments/nas/client_test.go @@ -0,0 +1,972 @@ +package nas + +import ( + "github.com/checkout/checkout-sdk-go/payments/nas/sources" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" + "github.com/checkout/checkout-sdk-go/payments" +) + +var ( + amount = 100 + currency = common.GBP + reference = "reference" + description = "description" + + paymentId = "pay_1234" + + actionId = "1234" + actionTypeAuth = payments.AuthorizationYes + actionCapture = payments.Capture +) + +func TestRequestPayment(t *testing.T) { + var ( + paymentRequest = PaymentRequest{ + Source: sources.NewRequestCardSource(), + Amount: amount, + Currency: currency, + Reference: reference, + Description: description, + Capture: false, + } + + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + paymentResponse = PaymentResponse{ + HttpMetadata: httpMetadata, + Amount: amount, + Id: paymentId, + Currency: currency, + Source: &SourceResponse{ + ResponseCardSource: &ResponseCardSource{ + Type: payments.CardSource, + }, + }, + Reference: reference, + } + ) + + cases := []struct { + name string + request PaymentRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*PaymentResponse, error) + }{ + { + name: "when request is correct then return payment", + request: paymentRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*PaymentResponse) + *respMapping = paymentResponse + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentResponse.Id, response.Id) + assert.Equal(t, paymentResponse.Amount, response.Amount) + assert.Equal(t, paymentResponse.Currency, response.Currency) + assert.Equal(t, paymentResponse.Source.ResponseCardSource.Type, response.Source.ResponseCardSource.Type) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: PaymentRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *PaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestPayment(tc.request, tc.idempotencyKey)) + }) + } +} + +func TestRequestPayout(t *testing.T) { + var ( + payoutRequest = PayoutRequest{ + Amount: amount, + Currency: currency, + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + payoutResponse = PayoutResponse{ + HttpMetadata: httpMetadata, + Id: paymentId, + Reference: reference, + } + ) + + cases := []struct { + name string + request PayoutRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*PayoutResponse, error) + }{ + { + name: "when request is correct then return payout", + request: payoutRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*PayoutResponse) + *respMapping = payoutResponse + }) + }, + checker: func(response *PayoutResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.Equal(t, payoutResponse.Id, response.Id) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *PayoutResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: PayoutRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *PayoutResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestPayout(tc.request, tc.idempotencyKey)) + }) + } +} + +func TestGetPaymentDetails(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + paymentResponse = GetPaymentResponse{ + HttpMetadata: httpMetadata, + Id: paymentId, + Amount: amount, + Currency: currency, + Reference: reference, + Description: description, + } + ) + + cases := []struct { + name string + paymentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetPaymentResponse, error) + }{ + { + name: "when paymentId is correct then return payment", + paymentId: paymentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetPaymentResponse) + *respMapping = paymentResponse + }) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentResponse.Id, response.Id) + assert.Equal(t, paymentResponse.Amount, response.Amount) + assert.Equal(t, paymentResponse.Currency, response.Currency) + assert.Equal(t, paymentResponse.Reference, response.Reference) + assert.Equal(t, paymentResponse.Description, response.Description) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetPaymentResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetPaymentDetails(tc.paymentId)) + }) + } +} + +func TestGetPaymentActions(t *testing.T) { + var ( + auth = PaymentAction{ + Type: actionTypeAuth, + Amount: amount, + } + + capture = PaymentAction{ + Type: actionCapture, + Amount: amount, + } + + paymentActions = []PaymentAction{auth, capture} + + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: 200, + } + + paymentActionsResponse = GetPaymentActionsResponse{ + HttpMetadata: httpMetadata, + Actions: paymentActions, + } + ) + + cases := []struct { + name string + paymentId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetPaymentActionsResponse, error) + }{ + { + name: "when request is valid then return payment actions", + paymentId: paymentId, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetPaymentActionsResponse) + *respMapping = paymentActionsResponse + }) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, paymentActionsResponse.Actions, response.Actions) + + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetPaymentActionsResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetPaymentActions(tc.paymentId)) + }) + } +} + +func TestCapturePayment(t *testing.T) { + var ( + captureRequest = CaptureRequest{ + Amount: amount, + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + captureResponse = payments.CaptureResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request CaptureRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.CaptureResponse, error) + }{ + { + name: "when request is correct then capture payment", + paymentId: paymentId, + request: captureRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.CaptureResponse) + *respMapping = captureResponse + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, captureResponse.ActionId, response.ActionId) + assert.Equal(t, captureResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when capture not allowed then return error", + paymentId: paymentId, + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + paymentId: "not_found", + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + paymentId: paymentId, + request: CaptureRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.CaptureResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CapturePayment(tc.paymentId, tc.request, tc.idempotencyKey)) + }) + } +} + +func TestRefundPayment(t *testing.T) { + var ( + refundRequest = payments.RefundRequest{ + Amount: amount, + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + refundResponse = payments.RefundResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request payments.RefundRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.RefundResponse, error) + }{ + { + name: "when request is correct then refund payment", + paymentId: paymentId, + request: refundRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.RefundResponse) + *respMapping = refundResponse + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, refundResponse.ActionId, response.ActionId) + assert.Equal(t, refundResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when refund not allowed then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + request: payments.RefundRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.RefundResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RefundPayment(tc.paymentId, &tc.request, tc.idempotencyKey)) + }) + } +} + +func TestVoidPayment(t *testing.T) { + var ( + voidRequest = payments.VoidRequest{ + Reference: reference, + } + + httpMetadata = common.HttpMetadata{ + Status: "202 Accepted", + StatusCode: http.StatusAccepted, + } + + voidResponse = payments.VoidResponse{ + HttpMetadata: httpMetadata, + ActionId: actionId, + Reference: reference, + } + ) + + cases := []struct { + name string + paymentId string + request payments.VoidRequest + idempotencyKey *string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*payments.VoidResponse, error) + }{ + { + name: "when request is correct then void payment", + paymentId: paymentId, + request: voidRequest, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*payments.VoidResponse) + *respMapping = voidResponse + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusAccepted, response.HttpMetadata.StatusCode) + assert.Equal(t, voidResponse.ActionId, response.ActionId) + assert.Equal(t, voidResponse.Reference, response.Reference) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when void not allowed then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{StatusCode: http.StatusForbidden}) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusForbidden, chkErr.StatusCode) + }, + }, + { + name: "when payment not found then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + { + name: "when request invalid then return error", + request: payments.VoidRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "payment_source_required", + }, + }, + }) + }, + checker: func(response *payments.VoidResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.VoidPayment(tc.paymentId, &tc.request, tc.idempotencyKey)) + }) + } +} diff --git a/payments/nas/destinations.go b/payments/nas/destinations.go new file mode 100644 index 0000000..321df7a --- /dev/null +++ b/payments/nas/destinations.go @@ -0,0 +1,85 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type AccountType string + +const ( + Savings AccountType = "savings" + Current AccountType = "current" + Cash AccountType = "cash" +) + +type ( + PaymentRequestBankAccountDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + AccountType AccountType `json:"account_type,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + BranchCode string `json:"branch_code,omitempty"` + Iban string `json:"iban,omitempty"` + Bban string `json:"bban,omitempty"` + SwiftBic string `json:"swift_bic,omitempty"` + Country common.Country `json:"country,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + Bank *common.BankDetails `json:"bank,omitempty"` + } + + RequestIdDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + } +) + +type ( + DestinationResponse struct { + HttpMetadata common.HttpMetadata + ResponseBankAccountDestination *ResponseBankAccountDestination + AlternativeResponse *common.AlternativeResponse + } + + ResponseBankAccountDestination struct { + Type payments.PaymentDestinationType `json:"type,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Last4 string `json:"last4,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + } +) + +func (s *DestinationResponse) UnmarshalJSON(data []byte) error { + var typeMapping payments.DestinationTypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Destination { + case string(payments.BankAccountDestination): + var typeMapping ResponseBankAccountDestination + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.ResponseBankAccountDestination = &typeMapping + default: + var typeMapping common.AlternativeResponse + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.AlternativeResponse = &typeMapping + } + + return nil +} diff --git a/payments/nas/payments.go b/payments/nas/payments.go new file mode 100644 index 0000000..b6d8879 --- /dev/null +++ b/payments/nas/payments.go @@ -0,0 +1,257 @@ +package nas + +import ( + "encoding/json" + "time" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type AuthorizationType string + +const ( + FinalAuthorizationType AuthorizationType = "Final" + EstimatedAuthorizationType AuthorizationType = "Estimated" + IncrementalAuthorizationType AuthorizationType = "Incremental" +) + +type CaptureType string + +const ( + NonFinalCaptureType CaptureType = "NonFinal" + FinalCaptureType CaptureType = "Final" +) + +type InstructionScheme string + +const ( + SwiftInstructionScheme InstructionScheme = "swift" + LocalInstructionScheme InstructionScheme = "local" + InstantInstructionScheme InstructionScheme = "instant" +) + +type IdentificationType string + +const ( + Passport IdentificationType = "passport" + DrivingLicence IdentificationType = "driving_licence" + NationalId IdentificationType = "national_id" +) + +type ( + Product struct { + Name string `json:"name,omitempty"` + Quantity int `json:"quantity,omitempty"` + UnitPrice int `json:"unit_price,omitempty"` + Reference string `json:"reference,omitempty"` + CommodityCode string `json:"commodity_code,omitempty"` + UnitOfMeasure string `json:"unit_of_measure,omitempty"` + TotalAmount int64 `json:"total_amount,omitempty"` + TaxAmount int64 `json:"tax_amount,omitempty"` + DiscountAmount int64 `json:"discount_amount,omitempty"` + WxpayGoodsId string `json:"wxpay_goods_id,omitempty"` + ImageUrl string `json:"image_url,omitempty"` + Url string `json:"url,omitempty"` + Sku string `json:"sku,omitempty"` + } + + PayoutBillingDescriptor struct { + Reference string `json:"reference,omitempty"` + } + + PaymentInstruction struct { + Purpose string `json:"purpose,omitempty"` + ChargeBearer string `json:"charge_bearer,omitempty"` + Repair bool `json:"repair"` + Scheme *InstructionScheme `json:"scheme,omitempty"` + QuoteId string `json:"quote_id,omitempty"` + SkipExpiry string `json:"skip_expiry,omitempty"` + FundsTransferType string `json:"funds_transfer_type,omitempty"` + Mvv string `json:"mvv,omitempty"` + } + + PaymentResponseBalances struct { + TotalAuthorized int `json:"total_authorized,omitempty"` + TotalVoided int `json:"total_voided,omitempty"` + AvailableToVoid int `json:"available_to_void"` + TotalCaptured int `json:"total_captured,omitempty"` + AvailableToCapture int `json:"available_to_capture,omitempty"` + TotalRefunded int `json:"total_refunded,omitempty"` + AvailableToRefund int `json:"available_to_refund,omitempty"` + } + + PaymentInstructionResponse struct { + ValueDate *time.Time `json:"value_date,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + Identification struct { + Type IdentificationType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + IssuingCountry common.Country `json:"issuing_country,omitempty"` + DateOfExpiry string `json:"date_of_expiry,omitempty"` + } +) + +//Request +type ( + PaymentRequest struct { + Source interface{} `json:"source,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + PaymentType payments.PaymentType `json:"payment_type,omitempty"` + MerchantInitiated bool `json:"merchant_initiated"` + Reference string `json:"reference,omitempty"` + Description string `json:"description,omitempty"` + AuthorizationType AuthorizationType `json:"authorization_type,omitempty"` + Capture bool `json:"capture"` + CaptureOn time.Time `json:"capture_on,omitempty"` + Customer *common.CustomerRequest `json:"customer,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + ShippingDetails *payments.ShippingDetails `json:"shipping,omitempty"` + ThreeDsRequest *payments.ThreeDsRequest `json:"3ds,omitempty"` + PreviousPaymentId string `json:"previous_payment_id,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + Risk *payments.RiskRequest `json:"risk,omitempty"` + SuccessUrl string `json:"success_url,omitempty"` + FailureUrl string `json:"failure_url,omitempty"` + PaymentIp string `json:"payment_ip,omitempty"` + Sender interface{} `json:"sender,omitempty"` + Recipient *payments.PaymentRecipient `json:"recipient,omitempty"` + Marketplace *common.MarketplaceData `json:"marketplace,omitempty"` + Processing *payments.ProcessingSettings `json:"processing,omitempty"` + Items []Product `json:"items,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } + + PayoutRequest struct { + Source interface{} `json:"source,omitempty"` + Destination interface{} `json:"destination,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Reference string `json:"reference,omitempty"` + BillingDescriptor *PayoutBillingDescriptor `json:"billing_descriptor,omitempty"` + Sender interface{} `json:"sender,omitempty"` + Instruction *PaymentInstruction `json:"instruction,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } + + CaptureRequest struct { + Amount int `json:"amount,omitempty"` + CaptureType CaptureType `json:"capture_type,omitempty"` + Reference string `json:"reference,omitempty"` + Customer *common.CustomerRequest `json:"customer,omitempty"` + Description string `json:"description,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + Shipping *payments.ShippingDetails `json:"shipping,omitempty"` + Items []Product `json:"items,omitempty"` + Marketplace *common.MarketplaceData `json:"marketplace,omitempty"` + AmountAllocations []common.AmountAllocations `json:"amount_allocations,omitempty"` + Processing *payments.ProcessingSettings `json:"processing,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } +) + +//Response +type ( + PaymentResponse struct { + HttpMetadata common.HttpMetadata + ActionId string `json:"action_id,omitempty"` + Amount int `json:"amount,omitempty"` + Approved bool `json:"approved,omitempty"` + AuthCode string `json:"auth_code,omitempty"` + Id string `json:"id,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + Source *SourceResponse `json:"source,omitempty"` + Status payments.PaymentStatus `json:"status,omitempty"` + ThreeDs *payments.ThreeDsEnrollment `json:"3ds,omitempty"` + Reference string `json:"reference,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + ResponseSummary string `json:"response_summary,omitempty"` + Risk *payments.RiskAssessment `json:"risk,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + ExpiresOn time.Time `json:"expires_on,omitempty"` + Balances *PaymentResponseBalances `json:"balances,omitempty"` + Processing *payments.PaymentProcessing `json:"processing,omitempty"` + Eci string `json:"eci,omitempty"` + SchemeId string `json:"scheme_id,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + PayoutResponse struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + Status payments.PaymentStatus `json:"status,omitempty"` + Reference string `json:"reference,omitempty"` + Instruction *PaymentInstructionResponse `json:"instruction,omitempty"` + } + + GetPaymentResponse struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + RequestedOn time.Time `json:"requested_on,omitempty"` + Source *SourceResponse `json:"source,omitempty"` + Destination *DestinationResponse `json:"destination,omitempty"` + Sender *SenderResponse `json:"sender,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + PaymentType payments.PaymentType `json:"payment_type,omitempty"` + Reference string `json:"reference,omitempty"` + Description string `json:"description,omitempty"` + Approved bool `json:"approved,omitempty"` + ExpiresOn time.Time `json:"expires_on,omitempty"` + Status payments.PaymentStatus `json:"status,omitempty"` + Balances *PaymentResponseBalances `json:"balances,omitempty"` + ThreeDs *payments.ThreeDsData `json:"3ds,omitempty"` + Risk *payments.RiskAssessment `json:"risk,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + BillingDescriptor *payments.BillingDescriptor `json:"billing_descriptor,omitempty"` + ShippingDetails *payments.ShippingDetails `json:"shipping,omitempty"` + PaymentIp string `json:"payment_ip,omitempty"` + Marketplace *common.MarketplaceData `json:"marketplace,omitempty"` + AmountAllocations []common.AmountAllocations `json:"amount_allocations,omitempty"` + Recipient *payments.PaymentRecipient `json:"recipient,omitempty"` + ProcessingData *payments.ProcessingData `json:"processing,omitempty"` + Items []Product `json:"items,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Eci string `json:"eci,omitempty"` + SchemeId string `json:"scheme_id,omitempty"` + Actions []payments.PaymentActionSummary `json:"actions,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + GetPaymentActionsResponse struct { + HttpMetadata common.HttpMetadata + Actions []PaymentAction `json:"actions,omitempty"` + } + + PaymentAction struct { + Id string `json:"id,omitempty"` + Type payments.ActionType `json:"type,omitempty"` + ProcessedOn time.Time `json:"processed_on,omitempty"` + Amount int `json:"amount,omitempty"` + Approved bool `json:"approved,omitempty"` + AuthCode string `json:"auth_code,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + ResponseSummary string `json:"response_summary,omitempty"` + AuthorizationType AuthorizationType `json:"authorization_type,omitempty"` + Reference string `json:"reference,omitempty"` + Processing *payments.ProcessingSettings `json:"processing,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + AmountAllocations []common.AmountAllocations `json:"amount_allocations,omitempty"` + Links map[string]common.Link `json:"_links"` + } +) + +func (p *GetPaymentActionsResponse) UnmarshalJSON(data []byte) error { + var actions []PaymentAction + if err := json.Unmarshal(data, &actions); err != nil { + return err + } + p.Actions = actions + return nil +} diff --git a/payments/nas/senders.go b/payments/nas/senders.go new file mode 100644 index 0000000..4ffc293 --- /dev/null +++ b/payments/nas/senders.go @@ -0,0 +1,105 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type PaymentSenderType string + +const ( + IndividualSender PaymentSenderType = "individual" + CorporateSender PaymentSenderType = "corporate" + InstrumentSender PaymentSenderType = "instrument" + Government PaymentSenderType = "government" +) + +type ( + PaymentCorporateSender struct { + Type PaymentSenderType `json:"type,omitempty"` + CompanyName string `json:"company_name,omitempty"` + Address *common.Address `json:"address,omitempty"` + ReferenceType string `json:"reference_type,omitempty"` + SourceOfFunds string `json:"source_of_funds,omitempty"` + Identification *common.AccountHolderIdentification `json:"identification,omitempty"` + } + + PaymentIndividualSender struct { + Type PaymentSenderType `json:"type,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Address *common.Address `json:"address,omitempty"` + Identification *Identification `json:"identification,omitempty"` + } + + PaymentInstrumentSender struct { + Type PaymentSenderType `json:"type,omitempty"` + } +) + +func NewPaymentCorporateSender() *PaymentCorporateSender { + return &PaymentCorporateSender{Type: CorporateSender} +} + +func NewPaymentIndividualSender() *PaymentIndividualSender { + return &PaymentIndividualSender{Type: IndividualSender} +} + +func NewPaymentInstrumentSender() *PaymentInstrumentSender { + return &PaymentInstrumentSender{Type: InstrumentSender} +} + +type ( + SenderResponse struct { + HttpMetadata common.HttpMetadata + PaymentCorporateSender *PaymentCorporateSender + PaymentGovernmentSender *PaymentCorporateSender + PaymentIndividualSender *PaymentIndividualSender + PaymentInstrumentSender *PaymentInstrumentSender + AlternativeResponse *common.AlternativeResponse + } +) + +func (s *SenderResponse) UnmarshalJSON(data []byte) error { + var typeMapping payments.SenderTypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Sender { + case string(IndividualSender): + var typeMapping PaymentIndividualSender + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.PaymentIndividualSender = &typeMapping + case string(CorporateSender): + var typeMapping PaymentCorporateSender + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.PaymentCorporateSender = &typeMapping + case string(Government): + var typeMapping PaymentCorporateSender + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.PaymentGovernmentSender = &typeMapping + case string(InstrumentSender): + var typeMapping PaymentInstrumentSender + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.PaymentInstrumentSender = &typeMapping + default: + var typeMapping common.AlternativeResponse + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.AlternativeResponse = &typeMapping + } + + return nil +} diff --git a/payments/nas/source.go b/payments/nas/source.go new file mode 100644 index 0000000..25cbfc4 --- /dev/null +++ b/payments/nas/source.go @@ -0,0 +1,76 @@ +package nas + +import ( + "encoding/json" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + SourceResponse struct { + HttpMetadata common.HttpMetadata + ResponseCardSource *ResponseCardSource + ResponseCurrencyAccountSource *ResponseCurrencyAccountSource + AlternativeResponse *common.AlternativeResponse + } + + ResponseCardSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Scheme string `json:"scheme,omitempty"` + SchemeLocal string `json:"scheme_local,omitempty"` + Last4 string `json:"last4,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Bin string `json:"bin,omitempty"` + CardType common.CardType `json:"card_type,omitempty"` + CardCategory common.CardCategory `json:"card_category,omitempty"` + Issuer string `json:"issuer,omitempty"` + IssuerCountry common.Country `json:"issuer_country,omitempty"` + ProductId string `json:"product_id,omitempty"` + ProductType string `json:"product_type,omitempty"` + AvsCheck string `json:"avs_check,omitempty"` + CvvCheck string `json:"cvv_check,omitempty"` + PaymentAccountReference string `json:"payment_account_reference,omitempty"` + } + + ResponseCurrencyAccountSource struct { + Type payments.SourceType `json:"type,omitempty"` + Amount int `json:"amount,omitempty"` + } +) + +func (s *SourceResponse) UnmarshalJSON(data []byte) error { + var typeMapping common.TypeMapping + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + + switch typeMapping.Type { + case string(payments.CardSource): + var typeMapping ResponseCardSource + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.ResponseCardSource = &typeMapping + case string(payments.CurrencyAccountSource): + var typeMapping ResponseCurrencyAccountSource + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.ResponseCurrencyAccountSource = &typeMapping + default: + var typeMapping common.AlternativeResponse + if err := json.Unmarshal(data, &typeMapping); err != nil { + return err + } + s.AlternativeResponse = &typeMapping + } + + return nil +} diff --git a/payments/nas/sources/apm/apm.go b/payments/nas/sources/apm/apm.go new file mode 100644 index 0000000..86654b6 --- /dev/null +++ b/payments/nas/sources/apm/apm.go @@ -0,0 +1,284 @@ +package apm + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" + "time" +) + +type BillingPlanType string + +const ( + MerchantInitiatedBilling BillingPlanType = "MERCHANT_INITIATED_BILLING" + MerchantInitiatedBillingSingleAgreement BillingPlanType = "MERCHANT_INITIATED_BILLING_SINGLE_AGREEMENT" + ChannelInitiatedBilling BillingPlanType = "CHANNEL_INITIATED_BILLING" + ChannelInitiatedBillingSingleAgreement BillingPlanType = "CHANNEL_INITIATED_BILLING_SINGLE_AGREEMENT" + RecurringPayments BillingPlanType = "RECURRING_PAYMENTS" + PreApprovedPayments BillingPlanType = "PRE_APPROVED_PAYMENTS" +) + +// Properties +type ( + FawryProduct struct { + ProductId string `json:"product_id,omitempty"` + Quantity float64 `json:"quantity,omitempty"` + Price float64 `json:"price,omitempty"` + Description string `json:"description,omitempty"` + } + + InfoFields struct { + Label string `json:"label,omitempty"` + Text string `json:"text,omitempty"` + } + + BillingPlan struct { + Type BillingPlanType `json:"type,omitempty"` + SkipShippingAddress bool `json:"skip_shipping_address,omitempty"` + ImmutableShippingAddress bool `json:"immutable_shipping_address,omitempty"` + } +) + +// Requests +type ( + RequestAfterPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + } + + RequestAlipayPlusSource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestAlmaSource struct { + Type payments.SourceType `json:"type,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + } + + RequestBancontactSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + Language string `json:"language,omitempty"` + } + + RequestBenefitSource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestEpsSource struct { + Type payments.SourceType `json:"type,omitempty"` + Purpose string `json:"purpose,omitempty"` + } + + RequestFawrySource struct { + Type payments.SourceType `json:"type,omitempty"` + Description string `json:"description,omitempty"` + CustomerProfileId string `json:"customer_profile_id,omitempty"` + CustomerEmail string `json:"customer_email,omitempty"` + CustomerMobile string `json:"customer_mobile,omitempty"` + ExpiresOn time.Time `json:"expires_on,omitempty"` + Products []FawryProduct `json:"Products,omitempty"` + } + + RequestGiropaySource struct { + Type payments.SourceType `json:"type,omitempty"` + Purpose string `json:"purpose,omitempty"` + InfoFields []InfoFields `json:"info_fields,omitempty"` + } + + RequestIdealSource struct { + Type payments.SourceType `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Bic string `json:"bic,omitempty"` + Language string `json:"language,omitempty"` + } + + RequestKlarnaSource struct { + Type payments.SourceType `json:"type,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + } + + RequestKnetSource struct { + Type payments.SourceType `json:"type,omitempty"` + Language string `json:"language,omitempty"` + UserDefinedField1 string `json:"user_defined_field1,omitempty"` + UserDefinedField2 string `json:"user_defined_field2,omitempty"` + UserDefinedField3 string `json:"user_defined_field3,omitempty"` + UserDefinedField4 string `json:"user_defined_field4,omitempty"` + UserDefinedField5 string `json:"user_defined_field5,omitempty"` + CardToken string `json:"card_token,omitempty"` + Ptlf string `json:"ptlf,omitempty"` + } + + RequestMbwaySource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestMultiBancoSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + } + + RequestP24Source struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + AccountHolderEmail string `json:"account_holder_email,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + } + + RequestPayPalSource struct { + Type payments.SourceType `json:"type,omitempty"` + Plan *BillingPlan `json:"plan,omitempty"` + } + + RequestPostFinanceSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentCountry common.Country `json:"payment_country,omitempty"` + AccountHolderName string `json:"account_holder_name,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + } + + RequestQPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + Quantity int `json:"quantity,omitempty"` + Description string `json:"description,omitempty"` + Language string `json:"language,omitempty"` + NationalId string `json:"national_id,omitempty"` + } + + RequestSofortSource struct { + Type payments.SourceType `json:"type,omitempty"` + CountryCode common.Country `json:"countryCode,omitempty"` + LanguageCode string `json:"languageCode,omitempty"` + } + + RequestStcPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + } + + RequestTamaraSource struct { + Type payments.SourceType `json:"type,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + } + + RequestWeChatPaySource struct { + Type payments.SourceType `json:"type,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + } +) + +func NewRequestAfterPaySource() *RequestAfterPaySource { + return &RequestAfterPaySource{Type: payments.Afterpay} +} + +func NewRequestAlipayPlusSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.AlipayPlus} +} + +func NewRequestAlipayPlusCNSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.AlipayCn} +} + +func NewRequestAlipayPlusGCashSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.Gcash} +} + +func NewRequestAlipayPlusHKSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.AlipayHk} +} + +func NewRequestAlipayPlusDanaSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.Dana} +} + +func NewRequestAlipayPlusKakaoPaySource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.Kakaopay} +} + +func NewRequestAlipayPlusTrueMoneySource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.Truemoney} +} + +func NewRequestAlipayPlusTNGSource() *RequestAlipayPlusSource { + return &RequestAlipayPlusSource{Type: payments.Tng} +} + +func NewRequestAlmaSource() *RequestAlmaSource { + return &RequestAlmaSource{Type: payments.Alma} +} + +func NewRequestBancontactSource() *RequestBancontactSource { + return &RequestBancontactSource{Type: payments.BancontactSource} +} + +func NewRequestBenefitSource() *RequestBenefitSource { + return &RequestBenefitSource{Type: payments.Benefit} +} + +func NewRequestEpsSource() *RequestEpsSource { + return &RequestEpsSource{Type: payments.EpsSource} +} + +func NewRequestFawrySource() *RequestFawrySource { + return &RequestFawrySource{Type: payments.FawrySource} +} + +func NewRequestGiropaySource() *RequestGiropaySource { + return &RequestGiropaySource{Type: payments.GiropaySource} +} + +func NewRequestIdealSource() *RequestIdealSource { + return &RequestIdealSource{Type: payments.IdealSource} +} + +func NewRequestKlarnaSource() *RequestKlarnaSource { + return &RequestKlarnaSource{Type: payments.KlarnaSource} +} + +func NewRequestKnetSource() *RequestKnetSource { + return &RequestKnetSource{Type: payments.KnetSource} +} + +func NewRequestMbwaySource() *RequestMbwaySource { + return &RequestMbwaySource{Type: payments.Mbway} +} + +func NewRequestMultiBancoSource() *RequestMultiBancoSource { + return &RequestMultiBancoSource{Type: payments.MultiBancoSource} +} + +func NewRequestP24Source() *RequestP24Source { + return &RequestP24Source{Type: payments.P24Source} +} + +func NewRequestPayPalSource() *RequestPayPalSource { + return &RequestPayPalSource{Type: payments.PayPalSource} +} + +func NewRequestPostFinanceSource() *RequestPostFinanceSource { + return &RequestPostFinanceSource{Type: payments.Postfinance} +} + +func NewRequestQPaySource() *RequestQPaySource { + return &RequestQPaySource{Type: payments.QPaySource} +} + +func NewRequestSofortSource() *RequestSofortSource { + return &RequestSofortSource{Type: payments.SofortSource} +} + +func NewRequestStcPaySource() *RequestStcPaySource { + return &RequestStcPaySource{Type: payments.SofortSource} +} + +func NewRequestTamaraSource() *RequestTamaraSource { + return &RequestTamaraSource{Type: payments.TamaraSource} +} + +func NewRequestWeChatPaySource() *RequestWeChatPaySource { + return &RequestWeChatPaySource{Type: payments.Wechatpay} +} diff --git a/payments/nas/sources/sources.go b/payments/nas/sources/sources.go new file mode 100644 index 0000000..7e82b0c --- /dev/null +++ b/payments/nas/sources/sources.go @@ -0,0 +1,112 @@ +package sources + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments" +) + +type ( + RequestCardSource struct { + Type payments.SourceType `json:"type,omitempty"` + Number string `json:"number,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Cvv string `json:"cvv,omitempty"` + Stored bool `json:"stored"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } + + RequestIdSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Cvv string `json:"cvv,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + } + + RequestTokenSource struct { + Type payments.SourceType `json:"type,omitempty"` + Token string `json:"token,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + } + + RequestProviderTokenSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + Token string `json:"token,omitempty"` + AccountHolder *common.AccountHolder `json:"account_holder,omitempty"` + } + + RequestNetworkTokenSource struct { + Type payments.SourceType `json:"type,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + TokenType payments.NetworkTokenType `json:"token_type,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Eci string `json:"eci,omitempty"` + Stored bool `json:"stored"` + Name string `json:"name,omitempty"` + Cvv string `json:"cvv,omitempty"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + } + + RequestBankAccountSource struct { + Type payments.SourceType `json:"type,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + AccountType string `json:"account_type,omitempty"` + Country common.Country `json:"country,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankCode string `json:"bank_code,omitempty"` + AccountHolder common.AccountHolder `json:"account_holder,omitempty"` + } + + RequestCustomerSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"number,omitempty"` + } +) + +func NewRequestCardSource() *RequestCardSource { + return &RequestCardSource{Type: payments.CardSource} +} + +func NewRequestIdSource() *RequestIdSource { + return &RequestIdSource{Type: payments.IdSource} +} + +func NewRequestTokenSource() *RequestTokenSource { + return &RequestTokenSource{Type: payments.TokenSource} +} + +func NewRequestProviderTokenSource() *RequestProviderTokenSource { + return &RequestProviderTokenSource{Type: payments.ProviderTokenSource} +} + +func NewRequestNetworkTokenSource() *RequestNetworkTokenSource { + return &RequestNetworkTokenSource{Type: payments.NetworkTokenSource} +} + +func NewRequestBankAccountSource() *RequestBankAccountSource { + return &RequestBankAccountSource{Type: payments.BankAccountSource} +} + +func NewRequestCustomerSource() *RequestCustomerSource { + return &RequestCustomerSource{Type: payments.CustomerSource} +} + +type ( + PayoutRequestSource struct { + Type payments.SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Amount int `json:"amount,omitempty"` + } +) + +func NewPayoutRequestSource() *PayoutRequestSource { + return &PayoutRequestSource{Type: payments.CurrencyAccountSource} +} diff --git a/payments/payer.go b/payments/payer.go new file mode 100644 index 0000000..a9e22b7 --- /dev/null +++ b/payments/payer.go @@ -0,0 +1,9 @@ +package payments + +type ( + Payer struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Document string `json:"document,omitempty"` + } +) diff --git a/payments/payment.go b/payments/payment.go deleted file mode 100644 index 6fcd675..0000000 --- a/payments/payment.go +++ /dev/null @@ -1,691 +0,0 @@ -package payments - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -type ( - // Request ... - Request struct { - Source interface{} `json:"source,omitempty"` - Destination interface{} `json:"destination,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency"` - Reference string `json:"reference,omitempty"` - PaymentType common.PaymentType `json:"payment_type,omitempty"` - MerchantInitiated *bool `json:"merchant_initiated,omitempty"` - Description string `json:"description,omitempty"` - Capture *bool `json:"capture,omitempty"` - CaptureOn *time.Time `json:"capture_on,omitempty"` - Customer *Customer `json:"customer,omitempty"` - BillingDescriptor *BillingDescriptor `json:"billing_descriptor,omitempty"` - Shipping *Shipping `json:"shipping,omitempty"` - ThreeDS *ThreeDS `json:"3ds,omitempty"` - PreviousPaymentID string `json:"previous_payment_id,omitempty"` - Risk *Risk `json:"risk,omitempty"` - SuccessURL string `json:"success_url,omitempty,omitempty"` - FailureURL string `json:"failure_url,omitempty,omitempty"` - PaymentIP string `json:"payment_ip,omitempty"` - Recipient *Recipient `json:"recipient,omitempty"` - Destinations []*Destination `json:"destinations,omitempty"` - Processing *Processing `json:"processing,omitempty"` - FundTransferType string `json:"fund_transfer_type,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - // FOUR only - AuthorizationType string `json:"authorization_type,omitempty"` - ProcessingChannelId string `json:"processing_channel_id,omitempty"` - } - - // IDSource ... - IDSource struct { - Type string `json:"type" binding:"required"` - ID string `json:"id" binding:"required"` - CVV string `json:"cvv,omitempty"` - } - - // CardSource ... - CardSource struct { - Type string `json:"type" binding:"required"` - Number string `json:"number" binding:"required"` - ExpiryMonth uint64 `json:"expiry_month" binding:"required"` - ExpiryYear uint64 `json:"expiry_year" binding:"required"` - Name string `json:"name,omitempty"` - CVV string `json:"cvv,omitempty"` - Stored *bool `json:"stored,omitempty"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // TokenSource ... - TokenSource struct { - Type string `json:"type" binding:"required"` - Token string `json:"token" binding:"required"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // CustomerSource ... - CustomerSource struct { - Type string `json:"type" binding:"required"` - ID string `json:"id,omitempty"` - Email string `json:"email,omitempty"` - } - - // NetworkTokenSource ... - NetworkTokenSource struct { - Type string `json:"type" binding:"required"` - Token string `json:"token" binding:"required"` - ExpiryMonth uint64 `json:"expiry_month" binding:"required"` - ExpiryYear uint64 `json:"expiry_year" binding:"required"` - TokenType string `json:"token_type" binding:"required"` - Cryptogram string `json:"cryptogram" binding:"required"` - ECI string `json:"eci" binding:"required"` - Stored *bool `json:"stored,omitempty"` - Name string `json:"name,omitempty"` - CVV string `json:"cvv,omitempty"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // AlipaySource ... - AlipaySource struct { - Type string `json:"type" binding:"required"` - } - - // BenefitpaySource ... - BenefitpaySource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - } - - // BalotoSource ... - BalotoSource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - Country string `json:"country" binding:"required"` - Description string `json:"description,omitempty"` - Payer *Payer `json:"payer,omitempty"` - } - - // BoletoSource ... - BoletoSource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - Country string `json:"country" binding:"required"` - Description string `json:"description,omitempty"` - Payer *Payer `json:"payer,omitempty"` - } - - // Payer - - Payer struct { - Name string `json:"name" binding:"required"` - Email string `json:"email" binding:"required"` - Document string `json:"document" binding:"required"` - } - - // EPSSource - - EPSSource struct { - Type string `json:"type" binding:"required"` - Purpose string `json:"purpose" binding:"required"` - BIC string `json:"bic" binding:"required"` - } - - // GiropaySource ... - GiropaySource struct { - Type string `json:"type" binding:"required"` - Purpose string `json:"purpose" binding:"required"` - BIC string `json:"bic,omitempty"` - InfoFields []InfoField `json:"info_fields,omitempty"` - } - - // InfoField ... - InfoField struct { - Label string `json:"label,omitempty"` - Text string `json:"text,omitempty"` - } - - // IDealSource ... - IDealSource struct { - Type string `json:"type" binding:"required"` - Description string `json:"description,omitempty"` - BIC string `json:"bic" binding:"required"` - Language string `json:"language,omitempty"` - } - - // KlarnaSource ... - KlarnaSource struct { - Type string `json:"type" binding:"required"` - AuthorizationToken string `json:"authorization_token" binding:"required"` - Locale string `json:"locale" binding:"required"` - PurchaseCountry string `json:"purchase_country" binding:"required"` - AutoCapture string `json:"auto_capture,omitempty"` - BillingAddress *KlarnaAddress `json:"billing_address,omitempty"` - ShippingAddress *KlarnaAddress `json:"shipping_address,omitempty"` - TaxAmount uint64 `json:"tax_amount,omitempty"` - Product *KlarnaProduct `json:"products,omitempty"` - Customer *KlarnaCustomer `json:"customer,omitempty"` - MerchantReference1 string `json:"merchant_reference1,omitempty"` - MerchantReference2 string `json:"merchant_reference2,omitempty"` - MerchantData string `json:"merchant_data,omitempty"` - Attachment *KlarnaAttachment `json:"attachment,omitempty"` - } - - // KlarnaAddress ... - KlarnaAddress struct { - Attention string `json:"attention,omitempty"` - City string `json:"city,omitempty"` - Country string `json:"country,omitempty"` - Email string `json:"email,omitempty"` - FamilyName string `json:"family_name,omitempty"` - GivenName string `json:"given_name,omitempty"` - OrganizationName string `json:"organization_name,omitempty"` - Phone string `json:"phone,omitempty"` - PostalCode string `json:"postal_code,omitempty"` - Region string `json:"region,omitempty"` - StreetAddress string `json:"street_address,omitempty"` - StreetAddress2 string `json:"street_address2,omitempty"` - Title string `json:"title,omitempty"` - } - - // KlarnaCustomer ... - KlarnaCustomer struct { - DateOfBirth string `json:"date_of_birth,omitempty"` - Gender string `json:"gender,omitempty"` - LastFourSSN string `json:"last_four_ssn,omitempty"` - NationalIdentificationNumber string `json:"national_identification_number,omitempty"` - OrganizationEntityType string `json:"organization_entity_type,omitempty"` - OrganizationRegistrationID string `json:"organization_registration_id,omitempty"` - Title string `json:"title,omitempty"` - Type string `json:"type,omitempty"` - VatID string `json:"vat_id,omitempty"` - } - - // KlarnaProduct ... - KlarnaProduct struct { - ImageURL string `json:"image_url,omitempty"` - MerchantData string `json:"merchant_data,omitempty"` - Name string `json:"name,omitempty"` - ProductIdentifiers *KlarnaProductIdentifiers `json:"product_identifiers,omitempty"` - ProductURL string `json:"product_url,omitempty"` - Quantity uint64 `json:"quantity,omitempty"` - QuantityUnit string `json:"quantity_unit,omitempty"` - Reference string `json:"reference,omitempty"` - TaxRate uint64 `json:"tax_rate,omitempty"` - TotalAmount uint64 `json:"total_amount,omitempty"` - TotalDiscountAmount uint64 `json:"total_discount_amount,omitempty"` - TotalTaxAmount uint64 `json:"total_tax_amount,omitempty"` - Type string `json:"type,omitempty"` - UnitPrice uint64 `json:"unit_price,omitempty"` - } - - // KlarnaProductIdentifiers ... - KlarnaProductIdentifiers struct { - Brand string `json:"brand,omitempty"` - CategoryPath string `json:"category_path,omitempty"` - GlobalTradeItemNumber string `json:"global_trade_item_number,omitempty"` - ManufacturerPartNumber string `json:"manufacturer_part_number,omitempty"` - } - - // KlarnaAttachment ... - KlarnaAttachment struct { - Body string `json:"body,omitempty"` - ContentType string `json:"content_type,omitempty"` - } - // KNetSource ... - KNetSource struct { - Type string `json:"type" binding:"required"` - Language string `json:"language" binding:"required"` - UserDefinedField1 string `json:"user_defined_field1,omitempty"` - UserDefinedField2 string `json:"user_defined_field2,omitempty"` - UserDefinedField3 string `json:"user_defined_field3,omitempty"` - UserDefinedField4 string `json:"user_defined_field4,omitempty"` - UserDefinedField5 string `json:"user_defined_field5,omitempty"` - CardToken string `json:"card_token,omitempty"` - PTLF string `json:"ptlf,omitempty"` - } - - // OxxoSource ... - OxxoSource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - Country string `json:"country,omitempty"` - Description string `json:"description,omitempty"` - Payer *Payer `json:"payer,omitempty"` - } - - // P24Source ... - P24Source struct { - Type string `json:"type" binding:"required"` - PaymentCountry string `json:"payment_country" binding:"required"` - AccountHolderName string `json:"account_holder_name,omitempty"` - AccountHolderEmail string `json:"account_holder_email,omitempty"` - BillingDescriptor string `json:"billing_descriptor,omitempty"` - } - - // PagofacilSource ... - PagofacilSource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - Country string `json:"country,omitempty"` - Description string `json:"description,omitempty"` - Payer *Payer `json:"payer,omitempty"` - } - - // PayPalSource ... - PayPalSource struct { - Type string `json:"type" binding:"required"` - InvoiceNumber string `json:"invoice_number" binding:"required"` - RecipientName string `json:"recipient_name,omitempty"` - LogoURL string `json:"logo_url,omitempty"` - STC map[string]string `json:"stc,omitempty"` - } - - // PoliSource ... - PoliSource struct { - Type string `json:"type" binding:"required"` - } - - // RapipagoSource ... - RapipagoSource struct { - Type string `json:"type" binding:"required"` - IntegrationType string `json:"integration_type" binding:"required"` - Country string `json:"country,omitempty"` - Description string `json:"description,omitempty"` - Payer *Payer `json:"payer,omitempty"` - } - - // SofortSource ... - SofortSource struct { - Type string `json:"type" binding:"required"` - } - - // BancontactSource ... - BancontactSource struct { - Type string `json:"type" binding:"required"` - PaymentCountry string `json:"payment_country,omitempty"` - AccountHolderName string `json:"account_holder_name,omitempty"` - BillingDescriptor string `json:"billing_descriptor,omitempty"` - Language string `json:"language,omitempty"` - } - - // FawrySource ... - FawrySource struct { - Type string `json:"type" binding:"required"` - Description string `json:"description,omitempty"` - CustomerProfileID string `json:"customer_profile_id,omitempty"` - CustomerEmail string `json:"customer_email,omitempty"` - CustomerMobile string `json:"customer_mobile,omitempty"` - ExpiresOn time.Time `json:"expires_on,omitempty"` - Products *[]FawryProduct `json:"products,omitempty"` - } - - // FawryProduct ... - FawryProduct struct { - ProductID string `json:"product_id,omitempty"` - Quantity uint64 `json:"quantity,omitempty"` - Price uint64 `json:"price,omitempty"` - Description string `json:"description,omitempty"` - } - - // QPaySource ... - QPaySource struct { - Type string `json:"type" binding:"required"` - Quantity uint64 `json:"quantity,omitempty"` - Description string `json:"description,omitempty"` - Language string `json:"language,omitempty"` - NationalID string `json:"national_id,omitempty"` - } - - // MultibancoSource ... - MultibancoSource struct { - Type string `json:"type" binding:"required"` - PaymentCountry string `json:"payment_country,omitempty"` - AccountHolderName string `json:"account_holder_name,omitempty"` - BillingDescriptor string `json:"billing_descriptor,omitempty"` - } - - // TokenDestination ... - TokenDestination struct { - Type string `json:"type" binding:"required"` - Token string `json:"token" binding:"required"` - FirstName string `json:"first_name" binding:"required"` - LastName string `json:"last_name" binding:"required"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // IDDestination ... - IDDestination struct { - Type string `json:"type" binding:"required"` - ID string `json:"id" binding:"required"` - FirstName string `json:"first_name,required"` - LastName string `json:"last_name,required"` - } - - // CardDestination ... - CardDestination struct { - Type string `json:"type" binding:"required"` - Number string `json:"number" binding:"required"` - ExpiryMonth uint64 `json:"expiry_month" binding:"required"` - ExpiryYear uint64 `json:"expiry_year" binding:"required"` - FirstName string `json:"first_name,required"` - LastName string `json:"last_name,required"` - Name string `json:"name,omitempty"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // Customer ... - Customer struct { - Document string `json:"document,omitempty"` - Email string `json:"email,omitempty"` - Name string `json:"name,omitempty"` - } - - // BillingDescriptor ... - BillingDescriptor struct { - Name string `json:"name,omitempty"` - City string `json:"city,omitempty"` - } - - // Shipping ... - Shipping struct { - Address *common.Address `json:"address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // Risk ... - Risk struct { - Enabled *bool `json:"enabled,omitempty"` - } - - // RiskAssessment ... - RiskAssessment struct { - Flagged *bool `json:"flagged,omitempty"` - } - - // Recipient ... - Recipient struct { - DOB string `json:"dob"` - AccountNumber string `json:"account_number"` - ZIP string `json:"zip"` - LastName string `json:"last_name"` - } - - // Destination ... - Destination struct { - ID string `json:"id"` - Amount uint64 `json:"amount"` - } - - // Processing - Use the processing object to influence or - // override the data sent during card processing - Processing struct { - Mid string `json:"mid,omitempty"` - Aft *bool `json:"aft,omitempty"` - DLocal *DLocal `json:"dlocal,omitempty"` - AcquirerTransactionID string `json:"acquirer_transaction_id,omitempty"` - AcquirerReferenceNumber string `json:"acquirer_reference_number,omitempty"` - RetrievalReferenceNumber string `json:"retrieval_reference_number,omitempty"` - SenderInformation *SenderInformation `json:"senderInformation,omitempty"` - } - - // SenderInformation - - SenderInformation struct { - FirstName string `json:"firstName" binding:"required"` - LastName string `json:"lastName" binding:"required"` - Address string `json:"address" binding:"required"` - City string `json:"city,omitempty"` - State string `json:"state,omitempty"` - PostalCode string `json:"postalCode" binding:"required"` - Country string `json:"country" binding:"required"` - SourceOfFunds string `json:"sourceOfFunds" binding:"required"` - AccountNumber string `json:"accountNumber" binding:"required"` - Reference string `json:"reference" binding:"required"` - } - - // DLocal - Processing information required for dLocal payments. - DLocal struct { - Country string `json:"country,omitempty"` - Payer *Customer `json:"payer,omitempty"` - Installments *Installments `json:"installments,omitempty"` - } - - // Installments - Details about the installments. - Installments struct { - Count string `json:"count,omitempty"` - } -) - -// SetSource ... -func (r *Request) SetSource(s interface{}) error { - var err error - switch p := s.(type) { - case *IDSource: - case *CardSource: - case *TokenSource: - case *NetworkTokenSource: - case *AlipaySource: - case *BenefitpaySource: - case *BalotoSource: - case *BoletoSource: - case *EPSSource: - case *GiropaySource: - case *IDealSource: - case *KlarnaSource: - case *KNetSource: - case *OxxoSource: - case *P24Source: - case *PagofacilSource: - case *PayPalSource: - case *PoliSource: - case *RapipagoSource: - case SofortSource: - case BancontactSource: - case FawrySource: - case QPaySource: - case MultibancoSource: - case map[string]string: - default: - err = fmt.Errorf("Unsupported source type %T", p) - } - if err == nil { - r.Source = s - } - return err -} - -// SetDestination ... -func (r *Request) SetDestination(d interface{}) error { - var err error - switch p := d.(type) { - case *IDDestination: - case *CardDestination: - case *TokenDestination: - case map[string]string: - default: - err = fmt.Errorf("Unsupported source type %T", p) - } - if err == nil { - r.Destination = d - } - return err -} - -type ( - // Response ... - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Processed *Processed `json:"processed,omitempty"` - Pending *PaymentPending `json:"pending,omitempty"` - } - // Processed ... - Processed struct { - ID string `json:"id,omitempty"` - ActionID string `json:"action_id,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency,omitempty"` - Approved *bool `json:"approved,omitempty"` - Status common.PaymentAction `json:"status,omitempty"` - AuthCode string `json:"auth_code,omitempty"` - ResponseCode string `json:"response_code,omitempty"` - ResponseSummary string `json:"response_summary,omitempty"` - ThreeDSEnrollment *ThreeDSEnrollment `json:"3ds,omitempty"` - Flagged *bool `json:"flagged,omitempty"` - RiskAssessment *RiskAssessment `json:"risk,omitempty"` - Source *SourceResponse `json:"source,omitempty"` - Destination *DestinationResponse `json:"destination,omitempty"` - Customer *Customer `json:"customer,omitempty"` - ProcessedOn time.Time `json:"processed_on,omitempty"` - Reference string `json:"reference,omitempty"` - Processing *Processing `json:"processing,omitempty"` - ECI string `json:"eci,omitempty"` - SchemeID string `json:"scheme_id,omitempty"` - Links map[string]common.Link `json:"_links,omitempty"` - } - // PaymentPending ... - PaymentPending struct { - ID string `json:"id,omitempty"` - Status common.PaymentAction `json:"status,omitempty"` - Reference string `json:"reference,omitempty"` - Customer *Customer `json:"customer,omitempty"` - ThreeDS *ThreeDSEnrollment `json:"3ds,omitempty"` - Links map[string]common.Link `json:"_links,omitempty"` - } - // PaymentResponse ... - PaymentResponse struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Payment *Payment `json:"payment,omitempty"` - } - // Payment ... - Payment struct { - ID string `json:"id,omitempty"` - RequestedOn time.Time `json:"requested_on,omitempty"` - Source *SourceResponse `json:"source,omitempty"` - Amount uint64 `json:"amount,omitempty"` - Currency string `json:"currency,omitempty"` - PaymentType common.PaymentType `json:"payment_type,omitempty"` - Reference string `json:"reference,omitempty"` - Description string `json:"description,omitempty"` - Approved *bool `json:"approved,omitempty"` - Status common.PaymentAction `json:"status,omitempty"` - ThreeDS *ThreeDSEnrollment `json:"3ds,omitempty"` - Risk *RiskAssessment `json:"risk,omitempty"` - Customer *Customer `json:"customer,omitempty"` - BillingDescriptor *BillingDescriptor `json:"billing_descriptor,omitempty"` - Shipping *Shipping `json:"shipping,omitempty"` - PaymentIP string `json:"payment_ip,omitempty"` - Recipient *Recipient `json:"recipient,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - ECI string `json:"eci,omitempty"` - Actions []ActionSummary `json:"actions,omitempty"` - SchemeID string `json:"scheme_id,omitempty"` - } - // SourceResponse ... - SourceResponse struct { - *CardSourceResponse - *AlternativePaymentSourceResponse - } - // CardSourceResponse ... - CardSourceResponse struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - ExpiryMonth uint64 `json:"expiry_month,omitempty"` - ExpiryYear uint64 `json:"expiry_year,omitempty"` - Name string `json:"name,omitempty"` - Scheme string `json:"scheme,omitempty"` - Last4 string `json:"last4,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` - Bin string `json:"bin,omitempty"` - CardType common.CardType `json:"card_type,omitempty"` - CardCategory common.CardCategory `json:"card_category,omitempty"` - Issuer string `json:"issuer,omitempty"` - IssuerCountry string `json:"issuer_country,omitempty"` - ProductID string `json:"product_id,omitempty"` - ProductType string `json:"product_type,omitempty"` - AVSCheck string `json:"avs_check,omitempty"` - CVVCheck string `json:"cvv_check,omitempty"` - PaymentAccountReference string `json:"payment_account_reference,omitempty"` - Payouts *bool `json:"payouts,omitempty"` - FastFunds string `json:"fast_funds,omitempty"` - } - - // AlternativePaymentSourceResponse ... - AlternativePaymentSourceResponse struct { - ID string `json:"id"` - Type string `json:"type"` - BillingAddress *common.Address `json:"billing_address,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - } - - // DestinationResponse - - DestinationResponse struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - ExpiryMonth uint64 `json:"expiry_month,omitempty"` - ExpiryYear uint64 `json:"expiry_year,omitempty"` - Scheme string `json:"scheme,omitempty"` - Last4 string `json:"last4,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` - Bin string `json:"bin,omitempty"` - CardType common.CardType `json:"card_type,omitempty"` - CardCategory common.CardCategory `json:"card_category,omitempty"` - Issuer string `json:"issuer,omitempty"` - IssuerCountry string `json:"issuer_country,omitempty"` - ProductID string `json:"product_id,omitempty"` - ProductType string `json:"product_type,omitempty"` - } -) - -// MarshalJSON ... -func (s *SourceResponse) MarshalJSON() ([]byte, error) { - if s.CardSourceResponse != nil { - return json.Marshal(s.CardSourceResponse) - } else if s.AlternativePaymentSourceResponse != nil { - return json.Marshal(s.AlternativePaymentSourceResponse) - } - return json.Marshal(nil) -} - -// UnmarshalJSON ... -func (s *SourceResponse) UnmarshalJSON(data []byte) error { - temp := &struct { - Type string `json:"type"` - }{} - if err := json.Unmarshal(data, &temp); err != nil { - return err - } - switch temp.Type { - case "card": - var source CardSourceResponse - if err := json.Unmarshal(data, &source); err != nil { - return err - } - s.CardSourceResponse = &source - s.AlternativePaymentSourceResponse = nil - default: - var source AlternativePaymentSourceResponse - if err := json.Unmarshal(data, &source); err != nil { - return err - } - s.AlternativePaymentSourceResponse = &source - s.CardSourceResponse = nil - } - return nil -} - -type ( - // Accepted ... - Accepted struct { - ActionID string `json:"action_id"` - Reference string `json:"reference"` - Links map[string]common.Link `json:"_links"` - } -) diff --git a/payments/payments.go b/payments/payments.go new file mode 100644 index 0000000..342acd3 --- /dev/null +++ b/payments/payments.go @@ -0,0 +1,445 @@ +package payments + +import ( + "time" + + "github.com/checkout/checkout-sdk-go/common" +) + +const PathPayments = "payments" + +type SourceType string + +const ( + CardSource SourceType = "card" + IdSource SourceType = "id" + CustomerSource SourceType = "customer" + NetworkTokenSource SourceType = "network_token" + TokenSource SourceType = "token" + DLocalSource SourceType = "dLocal" + AlipaySource SourceType = "alipay" + BenefitPaySource SourceType = "benefitpay" + BoletoSource SourceType = "boleto" + EpsSource SourceType = "eps" + GiropaySource SourceType = "giropay" + IdealSource SourceType = "ideal" + KlarnaSource SourceType = "klarna" + KnetSource SourceType = "knet" + OxxoSource SourceType = "oxxo" + P24Source SourceType = "p24" + PagoFacilSource SourceType = "pagofacil" + PayPalSource SourceType = "paypal" + PoliSource SourceType = "poli" + RapiPagoSource SourceType = "rapipago" + BancontactSource SourceType = "bancontact" + FawrySource SourceType = "fawry" + QPaySource SourceType = "qpay" + MultiBancoSource SourceType = "multibanco" + SepaSource SourceType = "sepa" + SofortSource SourceType = "sofort" + AlipayHk SourceType = "alipay_hk" + AlipayCn SourceType = "alipay_cn" + AlipayPlus SourceType = "alipay_plus" + Gcash SourceType = "gcash" + Wechatpay SourceType = "wechatpay" + Dana SourceType = "dana" + Kakaopay SourceType = "kakaopay" + Truemoney SourceType = "truemoney" + Tng = "tng" + Afterpay = "afterpay" + Benefit = "benefit" + Mbway = "mbway" + Postfinance = "postfinance" + Stcpay = "stcpay" + Alma = "alma" + + BankAccountSource SourceType = "bank_account" + ProviderTokenSource SourceType = "provider_token" + CurrencyAccountSource SourceType = "currency_account" + TamaraSource SourceType = "tamara" +) + +type PaymentDestinationType string + +const ( + BankAccountDestination PaymentDestinationType = "bank_account" + CardDestination PaymentDestinationType = "card" + IdDestination PaymentDestinationType = "id" + TokenDestination PaymentDestinationType = "token" +) + +type PaymentType string + +const ( + Regular PaymentType = "Regular" + Recurring PaymentType = "Recurring" + MOTO PaymentType = "MOTO" + Installment PaymentType = "Installment" +) + +type PaymentStatus string + +const ( + Active PaymentStatus = "Active" + Pending PaymentStatus = "Pending" + Authorized PaymentStatus = "Authorized" + CardVerified PaymentStatus = "Card Verified" + Voided PaymentStatus = "Voided" + PartiallyCaptured PaymentStatus = "Partially Captured" + Captured PaymentStatus = "Captured" + PartiallyRefunded PaymentStatus = "Partially Refunded" + Refunded PaymentStatus = "Refunded" + Declined PaymentStatus = "Declined" + Canceled PaymentStatus = "Canceled" + Expired PaymentStatus = "Expired" + Requested PaymentStatus = "Requested" + Paid PaymentStatus = "Paid" +) + +type Exemption string + +const ( + None Exemption = "none" + LowValue Exemption = "low_value" + RecurringOperation Exemption = "recurring_operation" + TransactionRiskAssessment Exemption = "transaction_risk_assessment" + SecureCorporatePayment Exemption = "secure_corporate_payment" + TrustedListing Exemption = "trusted_listing" + ThreeDsOutage Exemption = "3ds_outage" + ScaDelegation Exemption = "sca_delegation" + OutOfScaScope Exemption = "out_of_sca_scope" + Other Exemption = "other" + LowRiskProgram Exemption = "low_risk_program" +) + +type ThreeDsEnrollmentStatus string + +const ( + Yes ThreeDsEnrollmentStatus = "Y" + No ThreeDsEnrollmentStatus = "N" + U ThreeDsEnrollmentStatus = "U" +) + +type ActionType string + +const ( + AuthorizationYes ActionType = "Authorization" + CardVerification ActionType = "Card Verification" + Void ActionType = "Void" + Capture ActionType = "Capture" + Refund ActionType = "Refund" + Payout ActionType = "Payout" + Return ActionType = "Return" +) + +type NetworkTokenType string + +const ( + Vts NetworkTokenType = "vts" + Mdes NetworkTokenType = "mdes" + ApplePay NetworkTokenType = "applepay" + GooglePay NetworkTokenType = "googlepay" +) + +type PreferredSchema string + +const ( + Visa PreferredSchema = "visa" + Mastercard PreferredSchema = "mastercard" + CartesBancaires PreferredSchema = "cartes_bancaires" +) + +type ProductType string + +const ( + QrCode ProductType = "QR Code" + InApp ProductType = "In-App" + OfficialAccount ProductType = "Official Account" + MiniProgram ProductType = "Mini Program" +) + +type MerchantInitiatedReason string + +const ( + DelayedCharge MerchantInitiatedReason = "Delayed_charge" + Resubmission MerchantInitiatedReason = "Resubmission" + NoShow MerchantInitiatedReason = "No_show" + Reauthorization MerchantInitiatedReason = "Reauthorization" +) + +type TerminalType string + +const ( + App TerminalType = "APP" + Wap TerminalType = "WAP" + Web TerminalType = "WEB" +) + +type OsType string + +const ( + Android OsType = "ANDROID" + Ios OsType = "IOS" +) + +type ShippingPreference string + +const ( + NoShipping ShippingPreference = "NO_SHIPPING" + SetProvidedAddress ShippingPreference = "SET_PROVIDED_ADDRESS" + GetFromFile ShippingPreference = "GET_FROM_FILE" +) + +type UserAction string + +const ( + PayNow UserAction = "PAY_NOW" + Continue UserAction = "CONTINUE" +) + +type ( + AirlineData struct { + Ticket *Ticket `json:"ticket,omitempty"` + Passenger *Passenger `json:"passenger,omitempty"` + FlightLegDetails []FlightLegDetails `json:"flight_leg_details,omitempty"` + } + Ticket struct { + Number string `json:"number,omitempty"` + IssueDate string `json:"issue_date,omitempty"` + IssuingCarrierCode string `json:"issuing_carrier_code,omitempty"` + TravelAgencyName string `json:"travel_agency_name,omitempty"` + TravelAgencyCode string `json:"travel_agency_code,omitempty"` + } + Passenger struct { + Name *PassengerName `json:"name,omitempty"` + DateOfBirth string `json:"date_of_birth,omitempty"` + CountryCode common.Country `json:"country_code,omitempty"` + } + PassengerName struct { + FullName string `json:"full_name,omitempty"` + } + FlightLegDetails struct { + FlightNumber int64 `json:"flight_number,omitempty"` + CarrierCode string `json:"carrier_code,omitempty"` + ServiceClass string `json:"service_class,omitempty"` + DepartureDate string `json:"departure_date,omitempty"` + DepartureTime string `json:"departure_time,omitempty"` + DepartureAirport string `json:"departure_airport,omitempty"` + ArrivalAirport string `json:"arrival_airport,omitempty"` + StopoverCode string `json:"stopover_code,omitempty"` + FareBasisCode string `json:"fare_basis_code,omitempty"` + } + ShippingInfo struct { + ShippingCompany string `json:"shipping_company,omitempty"` + ShippingMethod string `json:"shipping_method,omitempty"` + TrackingNumber string `json:"tracking_number,omitempty"` + TrackingUri string `json:"tracking_uri,omitempty"` + ReturnShippingCompany string `json:"return_shipping_company,omitempty"` + ReturnTrackingNumber string `json:"return_tracking_number,omitempty"` + ReturnTrackingUri string `json:"return_tracking_uri,omitempty"` + } + ShippingDetails struct { + Address *common.Address `json:"address,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + FromAddressZip string `json:"from_address_zip,omitempty"` + } + + BillingDescriptor struct { + Name string `json:"name,omitempty"` + City string `json:"city,omitempty"` + // Not available on Previous + Reference string `json:"reference,omitempty"` + } + + ThreeDsRequest struct { + Enabled bool `json:"enabled"` + AttemptN3D bool `json:"attempt_n3d"` + Eci string `json:"eci,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Xid string `json:"xid,omitempty"` + Version string `json:"version,omitempty"` + Exemption Exemption `json:"exemption,omitempty"` + ChallengeIndicator common.ChallengeIndicator `json:"challenge_indicator,omitempty"` + AllowUpgrade bool `json:"allow_upgrade,omitempty"` + // Not available on Previous + Status string `json:"status,omitempty"` + AuthenticationDate time.Time `json:"authentication_date,omitempty"` + AuthenticationAmount float64 `json:"authentication_amount,omitempty"` + FlowType common.ThreeDsFlowType `json:"flow_type,omitempty"` + StatusReasonCode string `json:"status_reason_code,omitempty"` + ChallengeCancelReason string `json:"challenge_cancel_reason,omitempty"` + Score string `json:"score,omitempty"` + CryptogramAlgorithm string `json:"cryptogram_algorithm,omitempty"` + AuthenticationId string `json:"authentication_id,omitempty"` + } + + RiskRequest struct { + Enabled bool `json:"enabled"` + } + + PaymentRecipient struct { + DateOfBirth string `json:"dob,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + Zip string `json:"zip,omitempty"` + LastName string `json:"last_name,omitempty"` + } + + ProcessingSettings struct { + OrderId string `json:"order_id,omitempty"` + TaxAmount int64 `json:"tax_amount,omitempty"` + DiscountAmount int64 `json:"discount_amount,omitempty"` + DutyAmount int64 `json:"duty_amount,omitempty"` + ShippingAmount int64 `json:"shipping_amount,omitempty"` + ShippingTaxAmount int64 `json:"shipping_tax_amount,omitempty"` + Aft bool `json:"aft,omitempty"` + PreferredScheme PreferredSchema `json:"preferred_scheme,omitempty"` + MerchantInitiatedReason MerchantInitiatedReason `json:"merchant_initiated_reason,omitempty"` + CampaignId int64 `json:"campaign_id,omitempty"` + ProductType ProductType `json:"product_type,omitempty"` + OpenId string `json:"open_id,omitempty"` + OriginalOrderAmount int64 `json:"original_order_amount,omitempty"` + ReceiptId string `json:"receipt_id,omitempty"` + TerminalType TerminalType `json:"terminal_type,omitempty"` + OsType OsType `json:"os_type,omitempty"` + InvoiceId string `json:"invoice_id,omitempty"` + BrandName string `json:"brand_name,omitempty"` + Locale string `json:"locale,omitempty"` + ShippingPreference ShippingPreference `json:"shipping_preference,omitempty"` + UserAction UserAction `json:"user_action,omitempty"` + SetTransactionContext []map[string]string `json:"set_transaction_context,omitempty"` + AirlineData []AirlineData `json:"airline_data,omitempty"` + OtpValue string `json:"otp_value,omitempty"` + PurchaseCountry common.Country `json:"purchase_country,omitempty"` + CustomPaymentMethodIds []string `json:"custom_payment_method_ids,omitempty"` + ShippingDelay int64 `json:"shipping_delay,omitempty"` + ShippingInfo string `json:"shipping_info,omitempty"` + Dlocal *DLocalProcessingSettings `json:"dlocal,omitempty"` + } + + ThreeDsEnrollment struct { + Downgraded bool `json:"downgraded,omitempty"` + Enrolled ThreeDsEnrollmentStatus `json:"enrolled,omitempty"` + UpgradeReason string `json:"upgrade_reason,omitempty"` + } + + RiskAssessment struct { + Flagged bool `json:"flagged,omitempty"` + } + + PaymentProcessing struct { + RetrievalReferenceNumber string `json:"retrieval_reference_number,omitempty"` + AcquirerTransactionId string `json:"acquirer_transaction_id,omitempty"` + RecommendationCode string `json:"recommendation_code,omitempty"` + PartnerOrderId string `json:"partner_order_id,omitempty"` + PartnerSessionId string `json:"partner_session_id,omitempty"` + PartnerClientToken string `json:"partner_client_token,omitempty"` + PartnerPaymentId string `json:"partner_payment_id,omitempty"` + ContinuationPayload string `json:"continuation_payload,omitempty"` + Pun string `json:"pun,omitempty"` + } + + ThreeDsData struct { + Downgraded bool `json:"downgraded,omitempty"` + Enrolled string `json:"enrolled,omitempty"` + UpgradeReason string `json:"upgrade_reason,omitempty"` + SignatureValid string `json:"signature_valid,omitempty"` + AuthenticationResponse string `json:"authentication_response,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Xid string `json:"xid,omitempty"` + Version string `json:"version,omitempty"` + Exemption Exemption `json:"exemption,omitempty"` + Challenged bool `json:"challenged,omitempty"` + } + + PaymentActionSummary struct { + Id string `json:"id,omitempty"` + Type ActionType `json:"type,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + ResponseSummary string `json:"response_summary,omitempty"` + } + + Processing struct { + AcquirerReferenceNumber string `json:"acquirer_reference_number,omitempty"` + RetrievalReferenceNumber ActionType `json:"retrieval_reference_number,omitempty"` + AcquirerTransactionId string `json:"acquirer_transaction_id,omitempty"` + } + + Installments struct { + Count string `json:"count,omitempty"` + } + + DLocalProcessingSettings struct { + Country common.Country `json:"country,omitempty"` + Payer Payer `json:"payer,omitempty"` + Installments *Installments `json:"installments,omitempty"` + } +) + +//Request +type ( + RefundRequest struct { + Amount int `json:"amount,omitempty"` + Reference string `json:"reference,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } + + VoidRequest struct { + Reference string `json:"reference,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + } +) + +//Response +type ( + CaptureResponse struct { + HttpMetadata common.HttpMetadata + ActionId string `json:"action_id,omitempty"` + Reference string `json:"reference,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + RefundResponse struct { + HttpMetadata common.HttpMetadata + ActionId string `json:"action_id,omitempty"` + Reference string `json:"reference,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + VoidResponse struct { + HttpMetadata common.HttpMetadata + ActionId string `json:"action_id,omitempty"` + Reference string `json:"reference,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + ProcessingData struct { + PreferredScheme PreferredSchema `json:"preferred_scheme,omitempty"` + AppId string `json:"app_id,omitempty"` + PartnerCustomerId string `json:"partner_customer_id,omitempty"` + PartnerPaymentId string `json:"partner_payment_id,omitempty"` + TaxAmount int64 `json:"tax_amount,omitempty"` + PurchaseCountry common.Country `json:"purchase_country,omitempty"` + Locale string `json:"locale,omitempty"` + PartnerOrderId string `json:"partner_order_id,omitempty"` + FraudStatus string `json:"fraud_status,omitempty"` + ProviderAuthorizedPaymentMethod *ProviderAuthorizedPaymentMethod `json:"provider_authorized_payment_method,omitempty"` + CustomPaymentMethodIds []string `json:"custom_payment_method_ids,omitempty"` + } + + ProviderAuthorizedPaymentMethod struct { + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + NumberOfInstallments int64 `json:"number_of_installments,omitempty"` + NumberOfDays int64 `json:"number_of_days,omitempty"` + } +) + +type ( + DestinationTypeMapping struct { + Destination string `json:"destination"` + } + + SenderTypeMapping struct { + Sender string `json:"sender"` + } +) diff --git a/payments/refunds.go b/payments/refunds.go deleted file mode 100644 index 77b60d5..0000000 --- a/payments/refunds.go +++ /dev/null @@ -1,16 +0,0 @@ -package payments - -import "github.com/checkout/checkout-sdk-go" - -// RefundsRequest .. -type RefundsRequest struct { - Amount uint64 `json:"amount,omitempty"` - Reference string `json:"reference,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// RefundsResponse ... -type RefundsResponse struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Accepted *Accepted `json:"accepted,omitempty"` -} diff --git a/payments/threeDS.go b/payments/threeDS.go deleted file mode 100644 index 4d11475..0000000 --- a/payments/threeDS.go +++ /dev/null @@ -1,63 +0,0 @@ -package payments - -type Exemption string - -const ( - // LowValue ... - LowValue Exemption = "low_value" - // SecureCorporatePayment ... - SecureCorporatePayment Exemption = "secure_corporate_payment" - // TrustedListing ... - TrustedListing Exemption = "trusted_listing" - // TransactionRiskAssessment ... - TransactionRiskAssessment Exemption = "transaction_risk_assessment" - // ThreeDSOutage ... - ThreeDSOutage Exemption = "3ds_outage" - // SCADelegation ... - SCADelegation Exemption = "sca_delegation" - // OutOfSCAScope ... - OutOfSCAScope Exemption = "out_of_sca_scope" - // Other ... - Other Exemption = "other" - // LowRiskProgram ... - LowRiskProgram Exemption = "low_risk_program" -) - -type ChallengeIndicator string - -const ( - // NoPreference ... - NoPreference ChallengeIndicator = "no_preference" - // NoChallengeRequested ... - NoChallengeRequested ChallengeIndicator = "no_challenge_requested" - // ChallengeRequested ... - ChallengeRequested ChallengeIndicator = "challenge_requested" - // ChallengeRequestedMandate ... - ChallengeRequestedMandate ChallengeIndicator = "challenge_requested_mandate" -) - -func (c ChallengeIndicator) String() string { - return string(c) -} - -// ThreeDS ... -type ThreeDS struct { - Enabled *bool `json:"enabled,omitempty"` - AttemptN3d *bool `json:"attempt_n3d,omitempty"` - ECI string `json:"eci,omitempty"` - Cryptogram string `json:"cryptogram,omitempty"` - XID string `json:"xid,omitempty"` - Version string `json:"version,omitempty"` - Exemption Exemption `json:"exemption,omitempty"` - ChallengeIndicator ChallengeIndicator `json:"challenge_indicator,omitempty"` -} - -// ThreeDSEnrollment : 3D-Secure Enrollment Data -type ThreeDSEnrollment struct { - Downgraded *bool `json:"downgraded,omitempty"` - Enrolled string `json:"enrolled,omitempty"` - SignatureValid string `json:"signature_valid,omitempty"` - AuthenticationResponse string `json:"authentication_response,omitempty"` - Cryptogram string `json:"cryptogram,omitempty"` - XID string `json:"xid,omitempty"` -} diff --git a/payments/voids.go b/payments/voids.go deleted file mode 100644 index 3545128..0000000 --- a/payments/voids.go +++ /dev/null @@ -1,15 +0,0 @@ -package payments - -import "github.com/checkout/checkout-sdk-go" - -// VoidsRequest ... -type VoidsRequest struct { - Reference string `json:"reference,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// VoidsResponse ... -type VoidsResponse struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Accepted *Accepted `json:"accepted,omitempty"` -} diff --git a/reconciliation/client.go b/reconciliation/client.go deleted file mode 100644 index 3a30c9e..0000000 --- a/reconciliation/client.go +++ /dev/null @@ -1,207 +0,0 @@ -package reconciliation - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" - "github.com/google/go-querystring/query" -) - -const paymentsPath = "reporting/payments" -const statementsPath = "reporting/statements" - -// Client ... -type Client struct { - API checkout.HTTPClient -} - -// NewClient ... -func NewClient(config checkout.Config) *Client { - return &Client{ - API: httpclient.NewClient(config), - } -} - -// PaymentsReport - -func (c *Client) PaymentsReport(request *Request) (*Response, error) { - - value, _ := query.Values(request.PaymentsParameter) - var query string = value.Encode() - var urlPath string = "/" + paymentsPath + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var paymentsReport PaymentsReport - err = json.Unmarshal(resp.ResponseBody, &paymentsReport) - response.PaymentsReport = &paymentsReport - return response, err - } - return response, err -} - -// PaymentsReportCSV - -func (c *Client) PaymentsReportCSV(request *Request) (*Response, error) { - - value, _ := query.Values(request.PaymentsParameter) - var query string = value.Encode() - var urlPath string = "/" + paymentsPath + "/" + "download" + "?" - resp, err := c.API.Download(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - response.CSV = resp.ResponseCSV - return response, err - } - return response, err -} - -// StatementsReportCSV - -func (c *Client) StatementsReportCSV(request *Request) (*Response, error) { - - value, _ := query.Values(request.StatementsParameter) - var query string = value.Encode() - var urlPath string = "/" + statementsPath + "/" + "download" + "?" - resp, err := c.API.Download(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - response.CSV = resp.ResponseCSV - return response, err - } - return response, err -} - -// StatementPaymentReportCSV - -func (c *Client) StatementPaymentReportCSV(statementID *string) (*Response, error) { - - resp, err := c.API.Download(fmt.Sprintf("/%v/%v/payments/download", statementsPath, checkout.StringValue(statementID))) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - response.CSV = resp.ResponseCSV - return response, err - } - return response, err -} - -// PaymentReport - -func (c *Client) PaymentReport(paymentID *string, request *Request) (*Response, error) { - - if paymentID != nil { - - resp, err := c.API.Get(fmt.Sprintf("/%v/%v", paymentsPath, checkout.StringValue(paymentID))) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var paymentsReport PaymentsReport - err = json.Unmarshal(resp.ResponseBody, &paymentsReport) - response.PaymentsReport = &paymentsReport - return response, err - } - return response, err - } - if request != nil { - value, _ := query.Values(request.PaymentParameter) - var query string = value.Encode() - var urlPath string = "/" + paymentsPath + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var paymentsReport PaymentsReport - err = json.Unmarshal(resp.ResponseBody, &paymentsReport) - response.PaymentsReport = &paymentsReport - return response, err - } - return response, err - } - return nil, nil -} - -// StatementsReport - -func (c *Client) StatementsReport(request *Request) (*Response, error) { - - value, _ := query.Values(request.StatementsParameter) - var query string = value.Encode() - var urlPath string = "/" + statementsPath + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var statementsReport StatementsReport - err = json.Unmarshal(resp.ResponseBody, &statementsReport) - response.StatementsReport = &statementsReport - return response, err - } - return response, err -} - -// StatementPaymentReport - -func (c *Client) StatementPaymentReport(statementID string, request *Request) (*Response, error) { - - if request != nil { - value, _ := query.Values(request.StatementParameter) - var query string = value.Encode() - var urlPath string = "/" + statementsPath + "/" + statementID + "/" + "payments" + "?" - resp, err := c.API.Get(urlPath + query) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var paymentsReport PaymentsReport - err = json.Unmarshal(resp.ResponseBody, &paymentsReport) - response.PaymentsReport = &paymentsReport - return response, err - } - return response, err - } - resp, err := c.API.Get(fmt.Sprintf("/%v/%v/payments", statementsPath, statementID)) - response := &Response{ - StatusResponse: resp, - } - if err != nil { - return response, err - } - if resp.StatusCode == http.StatusOK { - var paymentsReport PaymentsReport - err = json.Unmarshal(resp.ResponseBody, &paymentsReport) - response.PaymentsReport = &paymentsReport - return response, err - } - return response, err -} diff --git a/reconciliation/reconciliation.go b/reconciliation/reconciliation.go deleted file mode 100644 index d00d6a0..0000000 --- a/reconciliation/reconciliation.go +++ /dev/null @@ -1,153 +0,0 @@ -package reconciliation - -import ( - "time" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -type ( - // Request - - Request struct { - *PaymentsParameter - *PaymentParameter - *StatementsParameter - *StatementParameter - } - - // PaymentsParameter - - PaymentsParameter struct { - From time.Time `url:"from,omitempty"` - To time.Time `url:"to,omitempty"` - Reference string `url:"reference,omitempty"` - Limit uint64 `url:"limit,omitempty"` - } - // PaymentParameter - - PaymentParameter struct { - Reference string `url:"reference,omitempty"` - } - - // StatementsParameter - - StatementsParameter struct { - From time.Time `url:"from,omitempty"` - To time.Time `url:"to,omitempty"` - Include string `url:"include,omitempty"` - } - - // StatementParameter - - StatementParameter struct { - PayoutID string `url:"payout_id,omitempty"` - PayoutCurrency string `url:"payout_currency,omitempty"` - Limit uint64 `url:"limit,omitempty"` - } -) - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - PaymentsReport *PaymentsReport `json:"payments_report,omitempty"` - StatementsReport *StatementsReport `json:"statements_report,omitempty"` - CSV [][]string `json:"csv,omitempty"` - } - - // PaymentsReport - - PaymentsReport struct { - Count uint64 `json:"count,omitempty"` - Data []Payment `json:"data,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // Payment - - Payment struct { - ID string `json:"id,omitempty"` - ProcessingCurrency string `json:"processing_currency,omitempty"` - PayoutCurrency string `json:"payout_currency,omitempty"` - RequestedOn string `json:"requested_on,omitempty"` - ChannelName string `json:"channel_name,omitempty"` - Reference string `json:"reference,omitempty"` - PaymentMethod string `json:"payment_method,omitempty"` - CardType common.CardType `json:"card_type,omitempty"` - CardCategory common.CardCategory `json:"card_category,omitempty"` - IssuerCountry string `json:"issuer_country,omitempty"` - MerchantCountry string `json:"merchant_country,omitempty"` - MID string `json:"mid,omitempty"` - Actions []Action `json:"actions,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // Action - - Action struct { - Type string `json:"type,omitempty"` - ID string `json:"id,omitempty"` - ProcessedOn string `json:"processed_on,omitempty"` - ResponseCode uint64 `json:"response_code,omitempty"` - ResponseDescription string `json:"response_description,omitempty"` - Breakdown []Breakdown `json:"breakdown,omitempty"` - } - - // Breakdown - - Breakdown struct { - Type string `json:"type,omitempty"` - Date string `json:"date,omitempty"` - ProcessingCurrencyAmount *float64 `json:"processing_currency_amount,omitempty"` - PayoutCurrencyAmount *float64 `json:"payout_currency_amount,omitempty"` - } - - // StatementsReport - - StatementsReport struct { - Count uint64 `json:"count,omitempty"` - Data []Statement `json:"data,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // Statement - - Statement struct { - ID string `json:"id,omitempty"` - PeriodStart string `json:"period_start,omitempty"` - PeriodEnd string `json:"period_end,omitempty"` - Date string `json:"date,omitempty"` - Payouts *[]Payout `json:"payouts,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // Payout - - Payout struct { - Currency string `json:"currency,omitempty"` - CarriedForwardAmount *float64 `json:"carried_forward_amount,omitempty"` - CurrentPeriodAmount *float64 `json:"current_period_amount,omitempty"` - NetAmount *float64 `json:"net_amount,omitempty"` - Date string `json:"date,omitempty"` - PeriodStart string `json:"period_start,omitempty"` - PeriodEnd string `json:"period_end,omitempty"` - ID string `json:"id,omitempty"` - Status string `json:"status,omitempty"` - PayoutFee *float64 `json:"payout_fee,omitempty"` - CurrentPeriodBreakdown *CurrentPeriodBreakdown `json:"current_period_breakdown,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // CurrentPeriodBreakdown - - CurrentPeriodBreakdown struct { - ProcessedAmount *float64 `json:"processed_amount,omitempty"` - RefundAmount *float64 `json:"refund_amount,omitempty"` - ChargebackAmount *float64 `json:"chargeback_amount,omitempty"` - ProcessingFees *float64 `json:"processing_fees,omitempty"` - ProcessingFeesBreakdown *ProcessingFeesBreakdown `json:"processing_fees_breakdown,omitempty"` - RollingReserveAmount *float64 `json:"rolling_reserve_amount,omitempty"` - Tax *float64 `json:"tax,omitempty"` - AdminFees *float64 `json:"admin_fees,omitempty"` - GeneralAdjustments *float64 `json:"general_adjustments,omitempty"` - } - - // ProcessingFeesBreakdown - - ProcessingFeesBreakdown struct { - InterchangeFees *float64 `json:"interchange_fees,omitempty"` - SchemeAndOtherNetworkFees *float64 `json:"scheme_and_other_network_fees,omitempty"` - PremiumAndAPMFees *float64 `json:"premium_and_apm_fees,omitempty"` - ChargebackFees *float64 `json:"chargeback_fees,omitempty"` - PaymentGatewayFees *float64 `json:"payment_gateway_fees,omitempty"` - AccountUpdaterFees *float64 `json:"account_updater_fees,omitempty"` - } -) diff --git a/sessions/channels/channels.go b/sessions/channels/channels.go new file mode 100644 index 0000000..c560070 --- /dev/null +++ b/sessions/channels/channels.go @@ -0,0 +1,89 @@ +package channels + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +type ChannelType string + +const ( + Browser ChannelType = "browser" + App ChannelType = "app" +) + +type SdkInterfaceType string + +const ( + Native SdkInterfaceType = "native" + Html SdkInterfaceType = "html" + Both SdkInterfaceType = "both" +) + +type UIElements string + +const ( + Text UIElements = "text" + SingleSelect UIElements = "single_select" + MultiSelect UIElements = "multi_select" + Oob UIElements = "oob" + HtmlOther UIElements = "html_other" +) + +type ( + Channel interface { + GetType() ChannelType + } + + ChannelData struct { + Channel ChannelType `json:"channel,omitempty"` + } + + appSession struct { + ChannelData + SdkAppId string `json:"sdk_app_id,omitempty"` + SdkMaxTimeout int `json:"sdk_max_timeout,omitempty"` + SdkEphemPubKey *SdkEphemeralPublicKey `json:"sdk_ephem_pub_key,omitempty"` + SdkReferenceNumber string `json:"sdk_reference_number,omitempty"` + SdkEncryptedData string `json:"sdk_encrypted_data,omitempty"` + SdkTransactionId string `json:"sdk_transaction_id,omitempty"` + SdkInterfaceType SdkInterfaceType `json:"sdk_interface_type,omitempty"` + SdkUiElements []UIElements `json:"sdk_ui_elements,omitempty"` + } + + browserSession struct { + ChannelData + ThreeDsMethodCompletion common.ThreeDsMethodCompletion `json:"three_ds_method_completion,omitempty"` + AcceptHeader string `json:"accept_header,omitempty"` + JavaEnabled bool `json:"java_enabled,omitempty"` + Language string `json:"language,omitempty"` + ColorDepth string `json:"color_depth,omitempty"` + ScreenHeight string `json:"screen_height,omitempty"` + ScreenWidth string `json:"screen_width,omitempty"` + Timezone string `json:"timezone,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + IpAddress string `json:"ip_address,omitempty"` + } +) + +func NewAppSession() *appSession { + return &appSession{ChannelData: ChannelData{Channel: App}} +} + +func NewBrowserSession() *browserSession { + return &browserSession{ChannelData: ChannelData{Channel: Browser}} +} + +func (s *appSession) GetType() ChannelType { + return s.Channel +} + +func (s *browserSession) GetType() ChannelType { + return s.Channel +} + +type SdkEphemeralPublicKey struct { + Kty string `json:"kty,omitempty"` + Crv string `json:"crv,omitempty"` + X string `json:"x,omitempty"` + Y string `json:"y,omitempty"` +} diff --git a/sessions/client.go b/sessions/client.go new file mode 100644 index 0000000..1c8dba5 --- /dev/null +++ b/sessions/client.go @@ -0,0 +1,137 @@ +package sessions + +import ( + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/sessions/channels" +) + +type Client struct { + configuration *configuration.Configuration + apiClient client.HttpClient +} + +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { + return &Client{ + configuration: configuration, + apiClient: apiClient, + } +} + +func (c *Client) RequestSession(request SessionRequest) (*SessionResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.OAuth) + if err != nil { + return nil, err + } + + var response SessionDetails + err = c.apiClient.Post( + common.BuildPath(SessionsPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + var sessionResponse SessionResponse + sessionResponse.MapResponse(&response) + return &sessionResponse, nil +} + +func (c *Client) GetSessionDetails(sessionId string, sessionSecret string) (*GetSessionResponse, error) { + auth, err := c.customSdkAuthorization(sessionSecret) + if err != nil { + return nil, err + } + + var response GetSessionResponse + err = c.apiClient.Get(common.BuildPath(SessionsPath, sessionId), auth, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) UpdateSession( + sessionId string, + request channels.Channel, + sessionSecret string, +) (*GetSessionResponse, error) { + auth, err := c.customSdkAuthorization(sessionSecret) + if err != nil { + return nil, err + } + + var response GetSessionResponse + err = c.apiClient.Put( + common.BuildPath(SessionsPath, sessionId, CollectDataPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) CompleteSession(sessionId, sessionSecret string) (*common.MetadataResponse, error) { + auth, err := c.customSdkAuthorization(sessionSecret) + if err != nil { + return nil, err + } + + var response common.MetadataResponse + err = c.apiClient.Post( + common.BuildPath(SessionsPath, sessionId, CompletePath), + auth, + nil, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) Update3dsMethodCompletion( + sessionId string, + request ThreeDsMethodCompletionRequest, + sessionSecret string, +) (*Update3dsMethodCompletionResponse, error) { + auth, err := c.customSdkAuthorization(sessionSecret) + if err != nil { + return nil, err + } + + var response Update3dsMethodCompletionResponse + err = c.apiClient.Put( + common.BuildPath(SessionsPath, sessionId, IssuerFingerprintPath), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (c *Client) customSdkAuthorization(sessionSecret string) (*configuration.SdkAuthorization, error) { + if sessionSecret == "" { + return c.configuration.Credentials.GetAuthorization(configuration.OAuth) + } + + return NewSessionSecretCredentials(sessionSecret).GetAuthorization(configuration.CustomAuth) +} diff --git a/sessions/client_test.go b/sessions/client_test.go new file mode 100644 index 0000000..fda1a98 --- /dev/null +++ b/sessions/client_test.go @@ -0,0 +1,604 @@ +package sessions + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" + "github.com/checkout/checkout-sdk-go/sessions/channels" + "github.com/checkout/checkout-sdk-go/sessions/sources" +) + +func TestRequestSession(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + sessionDetails = SessionDetails{ + HttpMetadata: httpMetadata, + Id: "ses_1234", + SessionSecret: "session_secret", + } + + sessionResponse = SessionResponse{ + Created: &sessionDetails, + } + ) + + cases := []struct { + name string + request SessionRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*SessionResponse, error) + }{ + { + name: "when request is correct then request session", + request: SessionRequest{ + Source: sources.NewSessionCardSource(), + Amount: 100, + Currency: common.USD, + ProcessingChannelId: "pc_5jp2az55l3cuths25t5p3xhwru", + AuthenticationType: RegularAuthType, + AuthenticationCategory: Payment, + }, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*SessionDetails) + *respMapping = sessionDetails + }) + }, + checker: func(response *SessionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.Created.HttpMetadata.StatusCode) + assert.Equal(t, sessionResponse.Created.Id, response.Created.Id) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *SessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: SessionRequest{}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "source required", + }, + }, + }) + }, + checker: func(response *SessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.RequestSession(tc.request)) + }) + } +} + +func TestGetSessionDetails(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + session = SessionDetails{ + HttpMetadata: httpMetadata, + Id: "ses_1234", + SessionSecret: "session_secret", + } + + sessionResponse = GetSessionResponse{ + SessionDetails: session, + } + ) + + cases := []struct { + name string + sessionId string + getAuthorization func(*mock.Mock) mock.Call + apiGet func(*mock.Mock) mock.Call + checker func(*GetSessionResponse, error) + }{ + { + name: "when sessionId is correct then return session details", + sessionId: "ses_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(2).(*GetSessionResponse) + *respMapping = sessionResponse + }) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, session.Id, response.Id) + assert.Equal(t, session.SessionSecret, response.SessionSecret) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when session not found then return error", + sessionId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiGet: func(m *mock.Mock) mock.Call { + return *m.On("Get", mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + assert.Equal(t, "404 Not Found", chkErr.Status) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiGet(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.GetSessionDetails(tc.sessionId, "")) + }) + } +} + +func TestUpdateSession(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + session = SessionDetails{ + HttpMetadata: httpMetadata, + Id: "ses_1234", + SessionSecret: "session_secret", + } + + sessionResponse = GetSessionResponse{ + SessionDetails: session, + } + ) + + cases := []struct { + name string + sessionId string + request channels.Channel + getAuthorization func(*mock.Mock) mock.Call + apiPut func(*mock.Mock) mock.Call + checker func(*GetSessionResponse, error) + }{ + { + name: "when request is correct then update session", + sessionId: "ses_1234", + request: channels.NewAppSession(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*GetSessionResponse) + *respMapping = sessionResponse + }) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + sessionId: "ses_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when session not found then return error", + sessionId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *GetSessionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPut(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.UpdateSession(tc.sessionId, tc.request, "")) + }) + } +} + +func TestCompleteSession(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "204 No Content", + StatusCode: http.StatusNoContent, + } + + response = common.MetadataResponse{HttpMetadata: httpMetadata} + ) + + cases := []struct { + name string + sessionId string + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*common.MetadataResponse, error) + }{ + { + name: "when sessionId is correct then accept dispute", + sessionId: "ses_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*common.MetadataResponse) + *respMapping = response + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusNoContent, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when session not found then return error", + sessionId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *common.MetadataResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CompleteSession(tc.sessionId, "")) + }) + } +} + +func TestUpdate3dsMethodCompletion(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "200 OK", + StatusCode: http.StatusOK, + } + + sessionResponse = Update3dsMethodCompletionResponse{ + HttpMetadata: httpMetadata, + Id: "ses_1234", + SessionSecret: "session_secret", + } + ) + + cases := []struct { + name string + sessionId string + request ThreeDsMethodCompletionRequest + getAuthorization func(*mock.Mock) mock.Call + apiPut func(*mock.Mock) mock.Call + checker func(*Update3dsMethodCompletionResponse, error) + }{ + { + name: "when request is correct then update session", + sessionId: "ses_1234", + request: ThreeDsMethodCompletionRequest{ThreeDsMethodCompletion: common.Y}, + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*Update3dsMethodCompletionResponse) + *respMapping = sessionResponse + }) + }, + checker: func(response *Update3dsMethodCompletionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when credentials invalid then return error", + sessionId: "ses_1234", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *Update3dsMethodCompletionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when session not found then return error", + sessionId: "not_found", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPut: func(m *mock.Mock) mock.Call { + return *m.On("Put", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusNotFound, + Status: "404 Not Found", + }) + }, + checker: func(response *Update3dsMethodCompletionResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPut(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.Update3dsMethodCompletion(tc.sessionId, tc.request, "")) + }) + } +} + +func TestCustomSdkAuthorization(t *testing.T) { + cases := []struct { + name string + sessionSecret string + getAuthorization func(*mock.Mock) mock.Call + checker func(*configuration.SdkAuthorization, error) + }{ + { + name: "when session secret is empty then use OAuth credentials", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return( + &configuration.SdkAuthorization{ + PlatformType: configuration.DefaultOAuth, + Credential: "credential", + }, nil) + }, + checker: func(authorization *configuration.SdkAuthorization, err error) { + assert.Nil(t, err) + assert.NotNil(t, authorization) + assert.Equal(t, configuration.DefaultOAuth, authorization.PlatformType) + assert.Equal(t, "credential", authorization.Credential) + }, + }, + { + name: "when session secret is valid then use Session Secret credentials", + sessionSecret: "secret", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, nil) + }, + checker: func(authorization *configuration.SdkAuthorization, err error) { + assert.Nil(t, err) + assert.NotNil(t, authorization) + assert.Equal(t, configuration.Custom, authorization.PlatformType) + assert.Equal(t, "secret", authorization.Credential) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.customSdkAuthorization(tc.sessionSecret)) + }) + } +} diff --git a/sessions/completion/completion.go b/sessions/completion/completion.go new file mode 100644 index 0000000..4460555 --- /dev/null +++ b/sessions/completion/completion.go @@ -0,0 +1,46 @@ +package completion + +type CompletionType string + +const ( + Hosted CompletionType = "hosted" + NonHosted CompletionType = "non_hosted" +) + +type ( + Completion interface { + GetType() CompletionType + } + + CompletionInfo struct { + Type CompletionType `json:"type,omitempty"` + } + + hostedCompletion struct { + CompletionInfo + CallbackUrl string `json:"callback_url,omitempty"` + SuccessUrl string `json:"success_url,omitempty"` + FailureUrl string `json:"failure_url,omitempty"` + } + + nonHostedCompletion struct { + CompletionInfo + CallbackUrl string `json:"callback_url,omitempty"` + } +) + +func NewHostedCompletion() *hostedCompletion { + return &hostedCompletion{CompletionInfo: CompletionInfo{Type: Hosted}} +} + +func NewNonHostedCompletion() *nonHostedCompletion { + return &nonHostedCompletion{CompletionInfo: CompletionInfo{Type: NonHosted}} +} + +func (c *hostedCompletion) GetType() CompletionType { + return c.Type +} + +func (c *nonHostedCompletion) GetType() CompletionType { + return c.Type +} diff --git a/sessions/session_secret_credentials.go b/sessions/session_secret_credentials.go new file mode 100644 index 0000000..52be929 --- /dev/null +++ b/sessions/session_secret_credentials.go @@ -0,0 +1,29 @@ +package sessions + +import ( + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" +) + +type SessionSecretCredentials struct { + SessionSecret string +} + +func NewSessionSecretCredentials(sessionSecret string) *SessionSecretCredentials { + return &SessionSecretCredentials{SessionSecret: sessionSecret} +} + +func (c *SessionSecretCredentials) GetAuthorization(authorizationType configuration.AuthorizationType) (*configuration.SdkAuthorization, error) { + switch authorizationType { + case configuration.CustomAuth: + if c.SessionSecret != "" { + return &configuration.SdkAuthorization{ + PlatformType: configuration.Custom, + Credential: c.SessionSecret, + }, nil + } + return nil, errors.InvalidKey("session_secret") + default: + return nil, errors.InvalidAuthorizationType(string(configuration.CustomAuth)) + } +} diff --git a/sessions/session_secret_credentials_test.go b/sessions/session_secret_credentials_test.go new file mode 100644 index 0000000..e372abc --- /dev/null +++ b/sessions/session_secret_credentials_test.go @@ -0,0 +1,60 @@ +package sessions + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" +) + +func TestGetAuthorization(t *testing.T) { + cases := []struct { + name string + sessionSecret string + authorizationType configuration.AuthorizationType + checker func(*configuration.SdkAuthorization, error) + }{ + { + name: "when session secret is empty then return error", + authorizationType: configuration.CustomAuth, + checker: func(authorization *configuration.SdkAuthorization, err error) { + assert.Nil(t, authorization) + assert.NotNil(t, err) + assert.IsType(t, reflect.TypeOf(errors.CheckoutAuthorizationError("")), reflect.TypeOf(err)) + assert.Equal(t, "session_secret is required for this operation", err.Error()) + }, + }, + { + name: "when authorization type is invalid then return error", + sessionSecret: "secret", + authorizationType: configuration.OAuth, + checker: func(authorization *configuration.SdkAuthorization, err error) { + assert.Nil(t, authorization) + assert.NotNil(t, err) + assert.IsType(t, reflect.TypeOf(errors.CheckoutAuthorizationError("")), reflect.TypeOf(err)) + assert.Equal(t, fmt.Sprintf("Operation requires %s authorization type", configuration.CustomAuth), err.Error()) + }, + }, + { + name: "when session secret and authorization type is valid then SDK Authorization", + sessionSecret: "secret", + authorizationType: configuration.CustomAuth, + checker: func(authorization *configuration.SdkAuthorization, err error) { + assert.Nil(t, err) + assert.NotNil(t, authorization) + assert.Equal(t, configuration.Custom, authorization.PlatformType) + assert.Equal(t, "secret", authorization.Credential) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(NewSessionSecretCredentials(tc.sessionSecret).GetAuthorization(tc.authorizationType)) + }) + } +} diff --git a/sessions/sessions.go b/sessions/sessions.go new file mode 100644 index 0000000..4b0f6e3 --- /dev/null +++ b/sessions/sessions.go @@ -0,0 +1,83 @@ +package sessions + +const ( + SessionsPath = "sessions" + CollectDataPath = "collect-data" + CompletePath = "complete" + IssuerFingerprintPath = "issuer-fingerprint" +) + +type AuthenticationType string + +const ( + RegularAuthType AuthenticationType = "regular" + RecurringAuthType AuthenticationType = "recurring" +) + +type Category string + +const ( + Payment Category = "payment" + NonPayment Category = "nonPayment" +) + +type TransactionType string + +const ( + GoodsService TransactionType = "goods_service" + CheckAcceptance TransactionType = "check_acceptance" + AccountFunding TransactionType = "account_funding" + QuashiCardTransaction TransactionType = "quashi_card_transaction" + PrepaidActivationAndLoad TransactionType = "prepaid_activation_and_load" +) + +type SessionStatus string + +const ( + Pending SessionStatus = "pending" + Processing SessionStatus = "processing" + Challenged SessionStatus = "challenged" + ChallengeAbandoned SessionStatus = "challenge_abandoned" + Expired SessionStatus = "expired" + Approved SessionStatus = "approved" + Attempted SessionStatus = "attempted" + Unavailable SessionStatus = "unavailable" + Declined SessionStatus = "declined" + Rejected SessionStatus = "rejected" +) + +type StatusReason string + +const ( + AresError StatusReason = "ares_error" + AresStatus StatusReason = "ares_status" + VeresError StatusReason = "veres_error" + VeresStatus StatusReason = "veres_status" + ParesError StatusReason = "pares_error" + ParesStatus StatusReason = "pares_status" + RreqError StatusReason = "rreq_error" + RreqStatus StatusReason = "rreq_status" + RiskDeclined StatusReason = "risk_declined" +) + +type NextAction string + +const ( + CollectChannelData NextAction = "collect_channel_data" + IssueFingerprint NextAction = "issuer_fingerprint" + ChallengeCardHolder NextAction = "challenge_cardholder" + RedirectCardholder NextAction = "redirect_cardholder" + Complete NextAction = "complete" + Authenticate NextAction = "authenticate" +) + +type Recurring struct { + DaysBetweenPayments int `json:"days_between_payments,omitempty"` + Expiry string `json:"expiry,omitempty"` +} + +type Installment struct { + NumberOfPayments int `json:"number_of_payments,omitempty"` + DaysBetweenPayments int `json:"days_between_payments,omitempty"` + Expiry string `json:"expiry,omitempty"` +} diff --git a/sessions/sessions_requests.go b/sessions/sessions_requests.go new file mode 100644 index 0000000..6aaf50d --- /dev/null +++ b/sessions/sessions_requests.go @@ -0,0 +1,90 @@ +package sessions + +import ( + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/sessions/channels" + "github.com/checkout/checkout-sdk-go/sessions/completion" + "github.com/checkout/checkout-sdk-go/sessions/sources" +) + +type AuthenticationMethod string + +const ( + NoAuthentication AuthenticationMethod = "no_authentication" + OwnCredentials AuthenticationMethod = "own_credentials" + FederatedId AuthenticationMethod = "federated_id" + IssuerCredentials AuthenticationMethod = "issuer_credentials" + ThirdPartyAuthentication AuthenticationMethod = "third_party_authentication" + Fido AuthenticationMethod = "fido" +) + +type DeliveryTimeframe string + +const ( + ElectronicDelivery DeliveryTimeframe = "electronic_delivery" + SameDay DeliveryTimeframe = "same_day" + Overnight DeliveryTimeframe = "overnight" + TwoDayOrMore DeliveryTimeframe = "two_day_or_more" +) + +type ShippingIndicator string + +const ( + Visa ShippingIndicator = "visa" +) + +type ( + SessionRequest struct { + Source sources.SessionSource `json:"source,omitempty"` + Amount int `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + ProcessingChannelId string `json:"processing_channel_id,omitempty"` + Marketplace *SessionMarketplaceData `json:"marketplace,omitempty"` + AuthenticationType AuthenticationType `json:"authentication_type,omitempty"` + AuthenticationCategory Category `json:"authentication_category,omitempty"` + AccountInfo *CardholderAccountInfo `json:"account_info,omitempty"` + ChallengeIndicator common.ChallengeIndicator `json:"challenge_indicator,omitempty"` + BillingDescriptor *SessionsBillingDescriptor `json:"billing_descriptor,omitempty"` + Reference string `json:"reference,omitempty"` + MerchantRiskInfo *MerchantRiskInfo `json:"merchant_risk_info,omitempty"` + PriorTransactionReference string `json:"prior_transaction_reference,omitempty"` + TransactionType TransactionType `json:"transaction_type,omitempty"` + ShippingAddress *sources.SessionAddress `json:"shipping_address,omitempty"` + ShippingAddressMatchesBilling bool `json:"shipping_address_matches_billing,omitempty"` + Completion completion.Completion `json:"completion,omitempty"` + ChannelData channels.Channel `json:"channel_data,omitempty"` + Recurring *Recurring `json:"recurring,omitempty"` + Installment *Installment `json:"installment,omitempty"` + } + + ThreeDsMethodCompletionRequest struct { + ThreeDsMethodCompletion common.ThreeDsMethodCompletion `json:"three_ds_method_completion,omitempty"` + } +) + +type SessionMarketplaceData struct { + SubEntityId string `json:"sub_entity_id,omitempty"` +} + +type CardholderAccountInfo struct { + PurchaseCount int `json:"purchase_count,omitempty"` + AccountAge string `json:"account_age,omitempty"` + AddCardAttempts int `json:"add_card_attempts,omitempty"` + ShippingAddressAge string `json:"shipping_address_age,omitempty"` + AccountNameMatchesShippingName bool `json:"account_name_matches_shipping_name,omitempty"` + SuspiciousAccountActivity bool `json:"suspicious_account_activity,omitempty"` + TransactionsToday int `json:"transactions_today,omitempty"` + AuthenticationMethod AuthenticationMethod `json:"authentication_method,omitempty"` +} + +type SessionsBillingDescriptor struct { + Name string `json:"name,omitempty"` +} + +type MerchantRiskInfo struct { + DeliveryEmail string `json:"delivery_email,omitempty"` + DeliveryTimeframe DeliveryTimeframe `json:"delivery_timeframe,omitempty"` + IsPreorder bool `json:"is_preorder,omitempty"` + IsReorder bool `json:"is_reorder,omitempty"` + ShippingIndicator ShippingIndicator `json:"shipping_indicator,omitempty"` +} diff --git a/sessions/sessions_responses.go b/sessions/sessions_responses.go new file mode 100644 index 0000000..c946eb5 --- /dev/null +++ b/sessions/sessions_responses.go @@ -0,0 +1,182 @@ +package sessions + +import ( + "time" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/sessions/channels" + "github.com/checkout/checkout-sdk-go/sessions/sources" +) + +type ChallengeCancelReason string + +const ( + CardHolderCancel ChallengeCancelReason = "cardholder_cancel" + TransactionTimedOut ChallengeCancelReason = "transaction_timed_out" + ChallengeTimedOut ChallengeCancelReason = "challenge_timed_out" + TransactionError ChallengeCancelReason = "transaction_error" + SdkTimedOut ChallengeCancelReason = "sdk_timed_out" + Unknown ChallengeCancelReason = "unknown" +) + +type SessionInterface string + +const ( + NativeUi SessionInterface = "native_ui" + Html SessionInterface = "html" +) + +type ResponseCode string + +const ( + Y ResponseCode = "Y" + N ResponseCode = "N" + U ResponseCode = "U" + A ResponseCode = "A" + C ResponseCode = "C" + D ResponseCode = "D" + R ResponseCode = "R" + I ResponseCode = "I" +) + +type ( + SessionResponse struct { + Accepted *SessionDetails `json:"accepted,omitempty"` + Created *SessionDetails `json:"created,omitempty"` + } + + SessionDetails struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + SessionSecret string `json:"session_secret,omitempty"` + TransactionId string `json:"transaction_id,omitempty"` + Scheme sources.SessionScheme `json:"scheme,omitempty"` + Amount int64 `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Completed bool `json:"completed,omitempty"` + Challenged bool `json:"challenged,omitempty"` + AuthenticationType AuthenticationType `json:"authentication_type,omitempty"` + AuthenticationCategory Category `json:"authentication_category,omitempty"` + Certificates *DsPublicKeys `json:"certificates,omitempty"` + Status SessionStatus `json:"status,omitempty"` + StatusReason StatusReason `json:"status_reason,omitempty"` + Approved bool `json:"approved,omitempty"` + ProtocolVersion string `json:"protocol_version,omitempty"` + Reference string `json:"reference,omitempty"` + TransactionType TransactionType `json:"transaction_type,omitempty"` + NextActions []NextAction `json:"next_actions,omitempty"` + Ds *Ds `json:"ds,omitempty"` + Acs *Acs `json:"acs,omitempty"` + ResponseCode ResponseCode `json:"response_code,omitempty"` + ResponseStatusReason string `json:"response_status_reason,omitempty"` + Pareq string `json:"pareq,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Eci string `json:"eci,omitempty"` + Xid string `json:"xid,omitempty"` + CardholderInfo string `json:"cardholder_info,omitempty"` + Card *CardInfo `json:"card,omitempty"` + Recurring *Recurring `json:"recurring,omitempty"` + Installment *Installment `json:"installment,omitempty"` + AuthenticationDate time.Time `json:"authentication_date,omitempty"` + Exemption *ThreeDsExemption `json:"exemption,omitempty"` + FlowType common.ThreeDsFlowType `json:"flow_type,omitempty"` + ChallengeIndicator common.ChallengeIndicator `json:"challenge_indicator,omitempty"` + SchemeInfo *SchemeInfo `json:"scheme_info,omitempty"` + Links map[string]common.Link `json:"_links"` + } + + GetSessionResponse struct { + SessionDetails + } + + Update3dsMethodCompletionResponse struct { + HttpMetadata common.HttpMetadata + Id string `json:"id,omitempty"` + SessionSecret string `json:"session_secret,omitempty"` + TransactionId string `json:"transaction_id,omitempty"` + Scheme sources.SessionScheme `json:"scheme,omitempty"` + Amount int64 `json:"amount,omitempty"` + Currency common.Currency `json:"currency,omitempty"` + Completed bool `json:"completed,omitempty"` + Challenged bool `json:"challenged,omitempty"` + AuthenticationType AuthenticationType `json:"authentication_type,omitempty"` + AuthenticationCategory Category `json:"authentication_category,omitempty"` + Certificates *DsPublicKeys `json:"certificates,omitempty"` + Status SessionStatus `json:"status,omitempty"` + StatusReason StatusReason `json:"status_reason,omitempty"` + Approved bool `json:"approved,omitempty"` + ProtocolVersion string `json:"protocol_version,omitempty"` + Reference string `json:"reference,omitempty"` + TransactionType TransactionType `json:"transaction_type,omitempty"` + NextActions []NextAction `json:"next_actions,omitempty"` + Ds *Ds `json:"ds,omitempty"` + Acs *Acs `json:"acs,omitempty"` + ResponseCode ResponseCode `json:"response_code,omitempty"` + ResponseStatusReason string `json:"response_status_reason,omitempty"` + Pareq string `json:"pareq,omitempty"` + Cryptogram string `json:"cryptogram,omitempty"` + Eci string `json:"eci,omitempty"` + Xid string `json:"xid,omitempty"` + Card *CardInfo `json:"card,omitempty"` + Links map[string]common.Link `json:"_links"` + } +) + +func (r *SessionResponse) MapResponse(response *SessionDetails) { + switch response.HttpMetadata.StatusCode { + case 201: + r.Created = response + case 202: + r.Accepted = response + } +} + +type ( + CardInfo struct { + InstrumentId string `json:"instrument_id,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + } + + DsPublicKeys struct { + DsPublic string `json:"ds_public,omitempty"` + CaPublic string `json:"ca_public,omitempty"` + } + + Ds struct { + DsId string `json:"ds_id,omitempty"` + ReferenceNumber string `json:"reference_number,omitempty"` + TransactionId string `json:"transaction_id,omitempty"` + } + + Acs struct { + ReferenceNumber string `json:"reference_number,omitempty"` + TransactionId string `json:"transaction_id,omitempty"` + OperatorId string `json:"operator_id,omitempty"` + SignedContent string `json:"signed_content,omitempty"` + ChallengeMandated bool `json:"challenge_mandated,omitempty"` + AuthenticationType string `json:"authentication_type,omitempty"` + ChallengeCancelReason ChallengeCancelReason `json:"challenge_cancel_reason,omitempty"` + SessionInterface SessionInterface `json:"session_interface,omitempty"` + UiTemplate channels.UIElements `json:"ui_template,omitempty"` + ChallengeCancelReasonCode string `json:"challenge_cancel_reason_code,omitempty"` + } + + ThreeDsExemption struct { + Requested string `json:"requested,omitempty"` + Applied common.Exemption `json:"applied,omitempty"` + Code string `json:"code,omitempty"` + TrustedBeneficiary *TrustedBeneficiary `json:"trusted_beneficiary,omitempty"` + } + + TrustedBeneficiary struct { + Status string `json:"status,omitempty"` + Source string `json:"source,omitempty"` + } + + SchemeInfo struct { + Name sources.SessionScheme `json:"name,omitempty"` + Score string `json:"score,omitempty"` + Avalgo string `json:"avalgo,omitempty"` + } +) diff --git a/sessions/sources/session_sources.go b/sessions/sources/session_sources.go new file mode 100644 index 0000000..138b030 --- /dev/null +++ b/sessions/sources/session_sources.go @@ -0,0 +1,108 @@ +package sources + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +type SessionSourceType string + +const ( + Card SessionSourceType = "card" + Id SessionSourceType = "id" + Token SessionSourceType = "token" + NetworkToken SessionSourceType = "network_token" +) + +type SessionScheme string + +const ( + Visa SessionScheme = "visa" + Mastercard SessionScheme = "mastercard" + Jcb SessionScheme = "jcb" + Amex SessionScheme = "amex" + Diners SessionScheme = "diners" + CartesBancaires SessionScheme = "cartes_bancaires" +) + +type ( + SessionSource interface { + GetType() SessionSourceType + } + + SessionSourceInfo struct { + Type SessionSourceType `json:"type,omitempty"` + Scheme SessionScheme `json:"scheme,omitempty"` + BillingAddress *SessionAddress `json:"billing_address,omitempty"` + HomePhone *common.Phone `json:"home_phone,omitempty"` + MobilePhone *common.Phone `json:"mobile_phone,omitempty"` + WorkPhone *common.Phone `json:"work_phone,omitempty"` + Email string `json:"email,omitempty"` + } + + sessionCardSource struct { + SessionSourceInfo + Number string `json:"number,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Stored bool `json:"stored,omitempty"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + } + + sessionIdSource struct { + SessionSourceInfo + Id string `json:"id,omitempty"` + } + + sessionTokenSource struct { + SessionSourceInfo + Token string `json:"token,omitempty"` + StoreForFutureUse bool `json:"store_for_future_use,omitempty"` + } + + sessionNetworkTokenSource struct { + SessionSourceInfo + Token string `json:"token,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year,omitempty"` + Name string `json:"name,omitempty"` + Stored bool `json:"stored,omitempty"` + } +) + +func NewSessionCardSource() *sessionCardSource { + return &sessionCardSource{SessionSourceInfo: SessionSourceInfo{Type: Card}} +} + +func NewSessionIdSource() *sessionIdSource { + return &sessionIdSource{SessionSourceInfo: SessionSourceInfo{Type: Id}} +} + +func NewSessionTokenSource() *sessionTokenSource { + return &sessionTokenSource{SessionSourceInfo: SessionSourceInfo{Type: Token}} +} + +func NewSessionNetworkTokenSource() *sessionNetworkTokenSource { + return &sessionNetworkTokenSource{SessionSourceInfo: SessionSourceInfo{Type: NetworkToken}} +} + +func (s *sessionCardSource) GetType() SessionSourceType { + return s.Type +} + +func (s *sessionIdSource) GetType() SessionSourceType { + return s.Type +} + +func (s *sessionTokenSource) GetType() SessionSourceType { + return s.Type +} + +func (s *sessionNetworkTokenSource) GetType() SessionSourceType { + return s.Type +} + +type SessionAddress struct { + common.Address + AddressLine3 string `json:"address_line3,omitempty"` +} diff --git a/sources/client.go b/sources/client.go index bee0591..65896b7 100644 --- a/sources/client.go +++ b/sources/client.go @@ -1,40 +1,40 @@ package sources import ( - "encoding/json" - "net/http" - - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/httpclient" + "github.com/checkout/checkout-sdk-go/client" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" ) -const path = "sources" - -// Client ... type Client struct { - API checkout.HTTPClient + configuration *configuration.Configuration + apiClient client.HttpClient } -// NewClient ... -func NewClient(config checkout.Config) *Client { +func NewClient(configuration *configuration.Configuration, apiClient client.HttpClient) *Client { return &Client{ - API: httpclient.NewClient(config), + configuration: configuration, + apiClient: apiClient, } } -// AddPaymentSource - -func (c *Client) AddPaymentSource(request *Request) (*Response, error) { - response, err := c.API.Post("/"+path, request, nil) - resp := &Response{ - StatusResponse: response, - } +func (c *Client) CreateSepaSource(request *sepaSourceRequest) (*CreateSepaSourceResponse, error) { + auth, err := c.configuration.Credentials.GetAuthorization(configuration.SecretKey) if err != nil { - return resp, err + return nil, err } - if response.StatusCode == http.StatusCreated { - var source Source - err = json.Unmarshal(response.ResponseBody, &source) - resp.Source = &source + + var response CreateSepaSourceResponse + err = c.apiClient.Post( + common.BuildPath(path), + auth, + request, + &response, + nil, + ) + if err != nil { + return nil, err } - return resp, err + + return &response, nil } diff --git a/sources/client_test.go b/sources/client_test.go new file mode 100644 index 0000000..9a5ce9d --- /dev/null +++ b/sources/client_test.go @@ -0,0 +1,153 @@ +package sources + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/mocks" +) + +var ( + sepa = Sepa + email = "bruce@wayne-enterprises.com" + name = "Bruce Wayne" + phone = common.Phone{ + CountryCode: "+1", + Number: "415 555 2671", + } + customer = common.CustomerRequest{ + Email: email, + Name: name, + Phone: &phone, + } +) + +func TestCreateSepaSource(t *testing.T) { + var ( + httpMetadata = common.HttpMetadata{ + Status: "201 Created", + StatusCode: http.StatusCreated, + } + + sepaResponse = CreateSepaSourceResponse{ + HttpResponse: httpMetadata, + SourceResponse: &SourceResponse{ + SourceType: sepa, + }, + } + ) + + cases := []struct { + name string + request *sepaSourceRequest + getAuthorization func(*mock.Mock) mock.Call + apiPost func(*mock.Mock) mock.Call + checker func(*CreateSepaSourceResponse, error) + }{ + { + name: "when request is correct then create sepa source", + request: getSepaSourceRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + respMapping := args.Get(3).(*CreateSepaSourceResponse) + *respMapping = sepaResponse + }) + }, + checker: func(response *CreateSepaSourceResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpResponse.StatusCode) + assert.Equal(t, sepaResponse.SourceResponse.SourceType, response.SourceResponse.SourceType) + }, + }, + { + name: "when credentials invalid then return error", + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(nil, errors.CheckoutAuthorizationError("Invalid authorization type")) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + }, + checker: func(response *CreateSepaSourceResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAuthorizationError) + assert.Equal(t, "Invalid authorization type", chkErr.Error()) + }, + }, + { + name: "when request invalid then return error", + request: NewSepaSourceRequest(), + getAuthorization: func(m *mock.Mock) mock.Call { + return *m.On("GetAuthorization", mock.Anything). + Return(&configuration.SdkAuthorization{}, nil) + }, + apiPost: func(m *mock.Mock) mock.Call { + return *m.On("Post", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return( + errors.CheckoutAPIError{ + StatusCode: http.StatusUnprocessableEntity, + Status: "422 Invalid Request", + Data: &errors.ErrorDetails{ + ErrorType: "request_invalid", + ErrorCodes: []string{ + "email_required", + }, + }, + }) + }, + checker: func(response *CreateSepaSourceResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + apiClient := new(mocks.ApiClientMock) + credentials := new(mocks.CredentialsMock) + environment := new(mocks.EnvironmentMock) + + tc.getAuthorization(&credentials.Mock) + tc.apiPost(&apiClient.Mock) + + configuration := configuration.NewConfiguration(credentials, environment, &http.Client{}) + client := NewClient(configuration, apiClient) + + tc.checker(client.CreateSepaSource(tc.request)) + }) + } +} + +func getSepaSourceRequest() *sepaSourceRequest { + sepaRequest := NewSepaSourceRequest() + sourceData := SourceData{ + FirstName: "Bruce", + LastName: "Wayne", + AccountIban: "1234", + } + + sepaRequest.SourceData = &sourceData + sepaRequest.Reference = "reference" + sepaRequest.CustomerRequest = &customer + + return sepaRequest +} diff --git a/sources/source.go b/sources/source.go deleted file mode 100644 index f0e4938..0000000 --- a/sources/source.go +++ /dev/null @@ -1,83 +0,0 @@ -package sources - -import ( - "github.com/checkout/checkout-sdk-go" - "github.com/checkout/checkout-sdk-go/common" -) - -type ( - // Request - - Request struct { - *SEPA - *ACH - } - - // SEPA - - SEPA struct { - Type string `json:"type" binding:"required"` - Reference string `json:"reference,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - Customer *Customer `json:"customer,omitempty"` - BillingAddress *common.Address `json:"billing_address" binding:"required"` - SourceData *SEPASourceData `json:"source_data,omitempty"` - } - - // ACH - - ACH struct { - Type string `json:"type" binding:"required"` - Reference string `json:"reference,omitempty"` - Phone *common.Phone `json:"phone,omitempty"` - Customer *Customer `json:"customer,omitempty"` - BillingAddress *common.Address `json:"billing_address" binding:"required"` - SourceData *ACHSourceData `json:"source_data,omitempty"` - } - - // Customer - - Customer struct { - ID string `json:"id,omitempty"` - Email string `json:"email,omitempty"` - Name string `json:"name,omitempty"` - } - - // SEPASourceData - - SEPASourceData struct { - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - AccountIBAN string `json:"account_iban,omitempty"` - BIC string `json:"bic,omitempty"` - BillingDescriptor string `json:"billing_descriptor,omitempty"` - MandateType string `json:"mandate_type,omitempty"` - } - - // ACHSourceData - - ACHSourceData struct { - AccountType string `json:"account_type,omitempty"` - AccountNumber string `json:"account_number,omitempty"` - RoutingNumber string `json:"routing_number,omitempty"` - AccountHolderName string `json:"account_holder_name,omitempty"` - BillingDescriptor string `json:"billing_descriptor,omitempty"` - CompanyName string `json:"company_name,omitempty"` - } -) - -type ( - // Response - - Response struct { - StatusResponse *checkout.StatusResponse `json:"api_response,omitempty"` - Source *Source `json:"source,omitempty"` - } - // Source - - Source struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - ResponseCode string `json:"response_code,omitempty"` - Customer *Customer `json:"response_data,omitempty"` - ResponseData *ResponseData `json:"uploaded_on,omitempty"` - Links map[string]common.Link `json:"_links"` - } - - // ResponseData - - ResponseData struct { - MandateReference string `json:"mandate_reference,omitempty"` - } -) diff --git a/sources/sources.go b/sources/sources.go new file mode 100644 index 0000000..09d8b74 --- /dev/null +++ b/sources/sources.go @@ -0,0 +1,63 @@ +package sources + +import ( + "github.com/checkout/checkout-sdk-go/common" +) + +const path = "sources" + +type SourceType string +type MandateType string + +const ( + Sepa SourceType = "Sepa" +) + +const ( + Single MandateType = "single" + Recurring MandateType = "recurring" +) + +// Request +type ( + SourceData struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + AccountIban string `json:"account_iban,omitempty"` + Bic string `json:"bic,omitempty"` + BillingDescriptor string `json:"billing_descriptor,omitempty"` + MandateType MandateType `json:"mandate_type,omitempty"` + } + + sepaSourceRequest struct { + Type SourceType `json:"type" binding:"required"` + BillingAddress *common.Address `json:"billing_address,omitempty"` + SourceData *SourceData `json:"source_data,omitempty"` + Reference string `json:"reference,omitempty"` + Phone *common.Phone `json:"phone,omitempty"` + CustomerRequest *common.CustomerRequest `json:"customer,omitempty"` + } +) + +func NewSepaSourceRequest() *sepaSourceRequest { + return &sepaSourceRequest{ + Type: Sepa, + } +} + +// Response +type ( + CreateSepaSourceResponse struct { + HttpResponse common.HttpMetadata + SourceResponse *SourceResponse + ResponseData map[string]string `json:"response_data,omitempty"` + } + + SourceResponse struct { + SourceType SourceType `json:"type,omitempty"` + Id string `json:"id,omitempty"` + ResponseCode string `json:"response_code,omitempty"` + Customer *common.CustomerResponse `json:"customer,omitempty"` + Links map[string]common.Link `json:"_links"` + } +) diff --git a/test/accounts_test.go b/test/accounts_test.go new file mode 100644 index 0000000..139bd27 --- /dev/null +++ b/test/accounts_test.go @@ -0,0 +1,465 @@ +package test + +import ( + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/checkout/checkout-sdk-go" + "github.com/checkout/checkout-sdk-go/accounts" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/errors" + "github.com/checkout/checkout-sdk-go/nas" +) + +func TestCreateEntity(t *testing.T) { + cases := []struct { + name string + request accounts.OnboardEntityRequest + checker func(*accounts.OnboardEntityResponse, error) + }{ + { + name: "when request is correct then create entity", + request: accounts.OnboardEntityRequest{ + Reference: GenerateRandomReference(), + ContactDetails: &accounts.ContactDetails{Phone: &accounts.Phone{Number: "2345678910"}}, + Profile: &accounts.Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &accounts.Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + RegisteredAddress: Address(), + DateOfBirth: &accounts.DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &accounts.Identification{NationalIdNumber: "AB123456C"}, + }, + }, + checker: func(response *accounts.OnboardEntityResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when request is not correct then return error", + request: accounts.OnboardEntityRequest{}, + checker: func(response *accounts.OnboardEntityResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "invalid_request", chkErr.Data.ErrorType) + assert.Contains(t, chkErr.Data.ErrorCodes, "company_or_individual_required") + }, + }, + } + + client := OAuthApi().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.CreateEntity(tc.request)) + }) + } +} + +func TestGetEntity(t *testing.T) { + var ( + entityId = createEntity() + ) + + cases := []struct { + name string + entityId string + checker func(*accounts.OnboardEntityDetails, error) + }{ + { + name: "when entity exists then return entity details", + entityId: entityId, + checker: func(response *accounts.OnboardEntityDetails, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.Equal(t, entityId, response.Id) + }, + }, + { + name: "when entity does not exist then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + checker: func(response *accounts.OnboardEntityDetails, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + client := OAuthApi().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.GetEntity(tc.entityId)) + }) + } +} + +func TestUpdateEntity(t *testing.T) { + var ( + entityId = createEntity() + ) + + cases := []struct { + name string + entityId string + request accounts.OnboardEntityRequest + checker func(*accounts.OnboardEntityResponse, error) + }{ + { + name: "when request is correct then update entity", + entityId: entityId, + request: accounts.OnboardEntityRequest{ + ContactDetails: &accounts.ContactDetails{Phone: &accounts.Phone{Number: "2345678910"}}, + Profile: &accounts.Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &accounts.Individual{ + FirstName: "New Name", + LastName: "New LastName", + TradingName: "New Trading Name", + NationalTaxId: "TAX8765432", + RegisteredAddress: Address(), + DateOfBirth: &accounts.DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &accounts.Identification{NationalIdNumber: "AB123456C"}, + }, + }, + checker: func(response *accounts.OnboardEntityResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + }, + { + name: "when entity not_found then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + request: accounts.OnboardEntityRequest{ + Individual: &accounts.Individual{ + FirstName: "New Name", + LastName: "New LastName", + TradingName: "New Trading Name", + NationalTaxId: "TAX8765432", + RegisteredAddress: Address(), + DateOfBirth: &accounts.DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &accounts.Identification{NationalIdNumber: "AB123456C"}, + }, + }, + checker: func(response *accounts.OnboardEntityResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + client := OAuthApi().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.UpdateEntity(tc.entityId, tc.request)) + }) + } +} + +func createEntity() string { + r := accounts.OnboardEntityRequest{ + Reference: GenerateRandomReference(), + ContactDetails: &accounts.ContactDetails{Phone: &accounts.Phone{Number: "2345678910"}}, + Profile: &accounts.Profile{ + Urls: []string{"https://www.superheroexample.com"}, + Mccs: []string{"0742"}, + }, + Individual: &accounts.Individual{ + FirstName: "Bruce", + LastName: "Wayne", + TradingName: "Batman's Super Hero Masks", + NationalTaxId: "TAX123456", + RegisteredAddress: Address(), + DateOfBirth: &accounts.DateOfBirth{Day: 5, Month: 6, Year: 1995}, + Identification: &accounts.Identification{NationalIdNumber: "AB123456C"}, + }, + } + + entity, _ := OAuthApi().Accounts.CreateEntity(r) + + return entity.Id +} + +func TestUpdatePayoutSchedule(t *testing.T) { + var ( + entityId = "ent_t2jwrwxhxdas5755cnctu7iwmm" + ) + + cases := []struct { + name string + entityId string + currency common.Currency + request accounts.CurrencySchedule + checkerRequest func(*common.IdResponse, error) + checkerInfo func(*accounts.PayoutSchedule, error) + }{ + { + name: "when request for daily frequency schedule is correct then update entity", + entityId: entityId, + currency: common.USD, + request: accounts.CurrencySchedule{ + Enabled: true, + Threshold: 500, + Recurrence: accounts.NewScheduleFrequencyDailyRequest(), + }, + checkerRequest: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + checkerInfo: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.NotNil(t, response.Currency) + assert.IsType(t, accounts.NewScheduleFrequencyDailyRequest(), response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when request for weekly frequency schedule is correct then update entity", + entityId: entityId, + currency: common.USD, + request: accounts.CurrencySchedule{ + Enabled: true, + Threshold: 1000, + Recurrence: accounts.NewScheduleFrequencyWeeklyRequest([]accounts.DaySchedule{accounts.Monday}), + }, + checkerRequest: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + checkerInfo: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.NotNil(t, response.Currency) + assert.IsType(t, + accounts.NewScheduleFrequencyWeeklyRequest([]accounts.DaySchedule{accounts.Monday}), + response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when request for monthly frequency schedule is correct then update entity", + entityId: entityId, + currency: common.USD, + request: accounts.CurrencySchedule{ + Enabled: true, + Threshold: 1500, + Recurrence: accounts.NewScheduleFrequencyMonthlyRequest([]int{5}), + }, + checkerRequest: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + }, + checkerInfo: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.NotNil(t, response.Currency) + assert.IsType(t, + accounts.NewScheduleFrequencyMonthlyRequest([]int{5}), + response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when entity not_found then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + currency: common.USD, + request: accounts.CurrencySchedule{}, + checkerRequest: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + checkerInfo: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + client := buildAccountsClient().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checkerRequest(client.UpdatePayoutSchedule(tc.entityId, tc.currency, tc.request)) + + tc.checkerInfo(client.RetrievePayoutSchedule(tc.entityId)) + }) + } +} + +func TestGetPayoutSchedule(t *testing.T) { + var ( + dailyEntity = "ent_sdioy6bajpzxyl3utftdp7legq" + weeklyEntity = "ent_yvt7y275h6iu4diq4s6gxxepfm" + monthlyEntity = "ent_224gcrnxtugb2hlqo62w625i6m" + ) + + cases := []struct { + name string + entityId string + checker func(*accounts.PayoutSchedule, error) + }{ + { + name: "when entity with daily schedule exists then return entity's payout schedule", + entityId: dailyEntity, + checker: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Currency[common.USD]) + assert.True(t, response.Currency[common.USD].Enabled) + assert.Equal(t, 1000, response.Currency[common.USD].Threshold) + assert.Equal(t, accounts.NewScheduleFrequencyDailyRequest(), response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when entity with weekly schedule exists then return entity's payout schedule", + entityId: weeklyEntity, + checker: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Currency[common.USD]) + assert.True(t, response.Currency[common.USD].Enabled) + assert.Equal(t, 1000, response.Currency[common.USD].Threshold) + assert.Equal(t, + accounts.NewScheduleFrequencyWeeklyRequest([]accounts.DaySchedule{accounts.Wednesday}), + response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when entity with monthly schedule exists then return entity's payout schedule", + entityId: monthlyEntity, + checker: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Currency[common.USD]) + assert.True(t, response.Currency[common.USD].Enabled) + assert.Equal(t, 1000, response.Currency[common.USD].Threshold) + assert.Equal(t, + accounts.NewScheduleFrequencyMonthlyRequest([]int{15}), + response.Currency[common.USD].Recurrence) + }, + }, + { + name: "when entity does not exist then return error", + entityId: "ent_zzzzzzzzzzzzzzzzzzzzzzzzzz", + checker: func(response *accounts.PayoutSchedule, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + client := buildAccountsClient().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.RetrievePayoutSchedule(tc.entityId)) + }) + } +} + +func TestUploadFileAccounts(t *testing.T) { + cases := []struct { + name string + fileRequest accounts.File + checker func(*common.IdResponse, error) + }{ + { + name: "when data is correct then return ID for uploaded file - IMAGE", + fileRequest: accounts.File{ + File: "./checkout.jpeg", + Purpose: common.BankVerification, + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Id) + assert.NotNil(t, response.Links) + }, + }, + { + name: "when data is correct then return ID for uploaded file - PDF", + fileRequest: accounts.File{ + File: "./checkout.pdf", + Purpose: common.BankVerification, + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Id) + assert.NotNil(t, response.Links) + }, + }, + { + name: "when file path is missing then return error", + fileRequest: accounts.File{}, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + assert.Equal(t, "Invalid file name", err.Error()) + }, + }, + { + name: "when purpose is missing then return error", + fileRequest: accounts.File{ + File: "./checkout.pdf", + }, + checker: func(response *common.IdResponse, err error) { + assert.Nil(t, response) + assert.NotNil(t, err) + assert.Equal(t, "Invalid purpose", err.Error()) + }, + }, + } + + client := buildAccountsClient().Accounts + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.UploadFile(tc.fileRequest)) + }) + } +} + +func buildAccountsClient() *nas.Api { + oauthAccountsApi, _ := checkout.Builder().OAuth(). + WithClientCredentials( + os.Getenv("CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID"), + os.Getenv("CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET")). + WithEnvironment(configuration.Sandbox()). + WithScopes([]string{configuration.Marketplace, configuration.Files}). + Build() + + return oauthAccountsApi +} diff --git a/test/apm_ideal_test.go b/test/apm_ideal_test.go new file mode 100644 index 0000000..e4e5ec6 --- /dev/null +++ b/test/apm_ideal_test.go @@ -0,0 +1,66 @@ +package test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/checkout/checkout-sdk-go/apm/ideal" +) + +/* TODO fix 404 not-found +func TestGetInfo(t *testing.T) { + cases := []struct { + name string + checker func(*ideal.IdealInfo, error) + }{ + { + name: "when auth is correct then return ideal info", + checker: func(response *ideal.IdealInfo, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.IdealInfoLinks) + assert.NotNil(t, response.IdealInfoLinks.Self) + assert.NotNil(t, response.IdealInfoLinks.Curies) + assert.NotNil(t, response.IdealInfoLinks.Issuers) + }, + }, + } + + client := PreviousApi().Ideal + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.GetInfo()) + }) + } +} +*/ + +func TestGetIssuers(t *testing.T) { + cases := []struct { + name string + checker func(*ideal.IssuerResponse, error) + }{ + { + name: "when auth is correct then return issuers info", + checker: func(response *ideal.IssuerResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.Countries) + assert.NotNil(t, response.Links) + }, + }, + } + + client := PreviousApi().Ideal + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.GetIssuers()) + }) + } +} diff --git a/test/apm_klarna_test.go b/test/apm_klarna_test.go new file mode 100644 index 0000000..cc41a78 --- /dev/null +++ b/test/apm_klarna_test.go @@ -0,0 +1,136 @@ +package test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/checkout/checkout-sdk-go/apm/klarna" + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/errors" +) + +func TestCreateCreditSession(t *testing.T) { + cases := []struct { + name string + request klarna.CreditSessionRequest + checker func(*klarna.CreditSessionResponse, error) + }{ + { + name: "when request is correct then create klarna session", + request: klarna.CreditSessionRequest{ + PurchaseCountry: common.GB, + Currency: common.GBP, + Locale: "en-GB", + Amount: 1000, + TaxAmount: 1, + Products: getKlarnaProduct(), + }, + checker: func(response *klarna.CreditSessionResponse, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusCreated, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.SessionId) + assert.NotNil(t, response.ClientToken) + assert.NotNil(t, response.PaymentMethodCategories) + }, + }, + { + name: "when request is missing information then return error", + request: klarna.CreditSessionRequest{}, + checker: func(response *klarna.CreditSessionResponse, err error) { + assert.NotNil(t, err) + assert.Nil(t, response) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusUnprocessableEntity, chkErr.StatusCode) + assert.Equal(t, "request_invalid", chkErr.Data.ErrorType) + }, + }, + } + + client := PreviousApi().Klarna + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.CreateCreditSession(tc.request)) + }) + } +} + +func TestGetCreditSession(t *testing.T) { + var ( + sessionId = createCreditSession(t).SessionId + ) + + cases := []struct { + name string + sessionId string + checker func(*klarna.CreditSession, error) + }{ + { + name: "when session exists then return session", + sessionId: sessionId, + checker: func(response *klarna.CreditSession, err error) { + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, http.StatusOK, response.HttpMetadata.StatusCode) + assert.NotNil(t, response.ClientToken) + assert.NotNil(t, response.PurchaseCountry) + assert.NotNil(t, response.Currency) + assert.NotNil(t, response.Amount) + assert.NotNil(t, response.TaxAmount) + }, + }, + { + name: "when session not found then return error", + sessionId: "invalid_session_id", + checker: func(response *klarna.CreditSession, err error) { + assert.NotNil(t, err) + assert.Nil(t, response) + chkErr := err.(errors.CheckoutAPIError) + assert.Equal(t, http.StatusNotFound, chkErr.StatusCode) + }, + }, + } + + client := PreviousApi().Klarna + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tc.checker(client.GetCreditSession(tc.sessionId)) + }) + } +} + +func getKlarnaProduct() []map[string]interface{} { + return []map[string]interface{}{ + { + "name": "test product", + "quantity": 1, + "unit_price": 1000, + "tax_rate": 0, + "total_amount": 1000, + "total_tax_amount": 0, + }, + } +} + +func createCreditSession(t *testing.T) *klarna.CreditSessionResponse { + request := klarna.CreditSessionRequest{ + PurchaseCountry: common.GB, + Currency: common.GBP, + Locale: "en-GB", + Amount: 1000, + TaxAmount: 1, + Products: getKlarnaProduct(), + } + + response, err := PreviousApi().Klarna.CreateCreditSession(request) + if err != nil { + assert.Fail(t, fmt.Sprintf("error creating klarna session - %s", err.Error())) + } + + return response +} diff --git a/test/checkout.jpeg b/test/checkout.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ca26cc3ab5d304dfb0589f783a62396e888627cb GIT binary patch literal 4752 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ℭS(@~%8%Hg|3 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%0D-Ph+Ai;KU`8epN6jwTkJ_3^AkYNP7> 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

F}c`!w#PaJ9HxS;PH?HM?>}>3)z2s<8MdT?>)Tk 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

    EJ;DL2G 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�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