├── .default.env ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── adyen.go ├── adyen_test.go ├── amount.go ├── amount_test.go ├── avs.go ├── boolean.go ├── boolean_test.go ├── checkout.go ├── checkout_gateway.go ├── checkout_gateway_test.go ├── checkout_test.go ├── credetials.go ├── cvv.go ├── environment.go ├── environment_test.go ├── go.mod ├── go.sum ├── modification.go ├── modification_gateway.go ├── notification.go ├── notification_test.go ├── payment.go ├── payment_gateway.go ├── payment_gateway_test.go ├── recurring.go ├── recurring_gateway.go ├── response.go ├── response_test.go ├── signature.go ├── signature_test.go └── types.go /.default.env: -------------------------------------------------------------------------------- 1 | # Test account provided for example, it's a valid account, please use it only for testing purposes 2 | # To access Adyen back office, please create another account and update this file for tests 3 | 4 | # Payment API 5 | export ADYEN_USERNAME="ws@Company.HomeAccount934" 6 | export ADYEN_PASSWORD="9{d5V4z={tb6Byv}>)Z&?Phu-" 7 | export ADYEN_CLIENT_TOKEN="8214907238780839" 8 | export ADYEN_ACCOUNT="HomeCOM148" 9 | 10 | # API settings for Adyen Hosted Payment pages 11 | export ADYEN_HMAC="46AE75207D01236D1DAE55AF004F09CD18EDC303FC7F459038B01CED70D8A595" 12 | export ADYEN_SKINCODE="sgOgVcKV" 13 | export ADYEN_SHOPPER_LOCALE="en_GB" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .idea 27 | .DS_Store 28 | .vscode 29 | 30 | *.coverprofile 31 | c.out 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | install: 6 | - source .default.env 7 | - go get ./... 8 | - go get golang.org/x/lint/golint 9 | - go get github.com/kisielk/errcheck 10 | - go get github.com/joho/godotenv 11 | 12 | stages: 13 | - verification 14 | - test 15 | 16 | jobs: 17 | include: 18 | - stage: verification 19 | go: "1.12" 20 | script: make verification 21 | 22 | - stage: verification 23 | go: "1.13" 24 | script: make verification 25 | 26 | - stage: verification 27 | go: "1.14" 28 | script: make verification 29 | 30 | - stage: test 31 | go: "1.12" 32 | script: make test 33 | 34 | - stage: test 35 | go: "1.13" 36 | script: make test 37 | 38 | - stage: test 39 | go: "1.14" 40 | before_script: 41 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 42 | - chmod +x ./cc-test-reporter 43 | - ./cc-test-reporter before-build 44 | script: 45 | - make test 46 | after_script: 47 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at zhutik@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Igor Zhutaiev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | test: 4 | go test -coverprofile c.out -parallel 5 -v ./... 5 | 6 | verification: 7 | go vet ./... 8 | golint -set_exit_status ./... 9 | CGO_ENABLED=0 errcheck ./... 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] Adyen API for Go 2 | 3 | This library is deprecated in favor of [official one from Adyen](https://github.com/Adyen/adyen-go-api-library), now it's finally released! 4 | 5 | Thanks a lot for everyone who contributed and used this library! 6 | 7 | ----------------------------------------------------------------- 8 | 9 | [![Build Status](https://travis-ci.org/zhutik/adyen-api-go.png)](https://travis-ci.org/zhutik/adyen-api-go) 10 | [![GoDoc](http://godoc.org/github.com/zhutik/adyen-api-go?status.png)](http://godoc.org/github.com/zhutik/adyen-api-go) 11 | [![Maintainability](https://api.codeclimate.com/v1/badges/47b60e74a4ee14812282/maintainability)](https://codeclimate.com/github/zhutik/adyen-api-go/maintainability) 12 | [![Test Coverage](https://api.codeclimate.com/v1/badges/47b60e74a4ee14812282/test_coverage)](https://codeclimate.com/github/zhutik/adyen-api-go/test_coverage) 13 | [![Go Report Card](https://goreportcard.com/badge/github.com/zhutik/adyen-api-go)](https://goreportcard.com/report/github.com/zhutik/adyen-api-go) 14 | 15 | A Go client library for [Adyen](https://www.adyen.com/en/) payments platform. 16 | 17 | This is *not* an official client library. Adyen has official libraries for multiple platforms [Github](https://github.com/adyen/), but not Go yet. 18 | 19 | This package provides core functionality to perform most common types of a Payment requests to an API. 20 | If you see some functionality is missing, please, open an issue (or better yet, a pull request). 21 | 22 | ## Installation 23 | 24 | ``` 25 | go get github.com/zhutik/adyen-api-go 26 | ``` 27 | 28 | ## Playground and examples 29 | 30 | Please check separate repository with Adyen API playground, where you can test API 31 | and get some usage examples for Adyen API library 32 | 33 | https://github.com/zhutik/adyen-api-go-example 34 | 35 | Or you can visit [Wiki page](https://github.com/zhutik/adyen-api-go/wiki) for more details and examples 36 | 37 | ## Supported API Calls 38 | 39 | * Authorise (Encrypted in recommended) 40 | * Authorise 3D 41 | * Recurring payments and retrieving stored payment methods 42 | * Capture 43 | * Cancel 44 | * Refund (CancelOrRefund) 45 | * Notifications 46 | 47 | ## Usage 48 | 49 | ```go 50 | import "github.com/zhutik/adyen-api-go" 51 | 52 | // Configure Adyen API 53 | instance := adyen.New( 54 | adyen.Testing, 55 | os.Getenv("ADYEN_USERNAME"), 56 | os.Getenv("ADYEN_PASSWORD"), 57 | ) 58 | 59 | amount := &adyen.Amount{ 60 | Value: 1000, // amount * 100, f.e. 10,30 EUR = 1030 61 | Currency: "EUR" // or use instance.Currency 62 | } 63 | 64 | // or amount := adyen.NewAmount("EUR", 10), in this case decimal points would be adjusted automatically 65 | 66 | req := &adyen.AuthoriseEncrypted{ 67 | Amount: amount, 68 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), // your merchant account in Adyen 69 | AdditionalData: &adyen.AdditionalData{Content: "encryptedData"}, // encrypted data from a form 70 | Reference: "your-order-number", 71 | } 72 | 73 | // Perform authorise transaction 74 | g, err := instance.Payment().AuthoriseEncrypted(req) 75 | 76 | ``` 77 | 78 | Load Client Side JS for form encryption to include on credit card form page 79 | 80 | ```go 81 | // Configure Adyen API 82 | instance := adyen.New( 83 | adyen.Testing, 84 | os.Getenv("ADYEN_USERNAME"), 85 | os.Getenv("ADYEN_PASSWORD"), 86 | ) 87 | 88 | url := &adyen.ClientURL(os.Getenv("ADYEN_CLIENT_TOKEN")) 89 | ``` 90 | 91 | Currently, MerchantAccount and Currency need to be set for every request manually 92 | 93 | To shortcut configuration, additional methods could be used to set and retrieve those settings. 94 | 95 | ```go 96 | // Configure Adyen API 97 | instance := adyen.New( 98 | adyen.Testing, 99 | os.Getenv("ADYEN_USERNAME"), 100 | os.Getenv("ADYEN_PASSWORD"), 101 | ) 102 | 103 | // set parameters once for current instance 104 | instance.Currency = "USD" 105 | instance.MerchantAccount = "TEST_MERCHANT_ACCOUNT" 106 | 107 | // futher, information could be retrieved to populate request 108 | req := &adyen.AuthoriseEncrypted{ 109 | Amount: adyen.NewAmount(instance.Currency, 10.00), 110 | MerchantAccount: instance.MerchantAccount, 111 | AdditionalData: &adyen.AdditionalData{Content: "encryptedData"}, // encrypted data from a form 112 | Reference: "your-order-number", 113 | } 114 | ``` 115 | 116 | ### Environment configuration 117 | 118 | Adyen's Production environment requires additional configuration to the Test environment for security reasons. Namely, this includes a random hexadecimal string that's generated for your account and the company account name. 119 | 120 | In the following examples, the environmenst have been hard-coded for clarity. They would typically come from environments variables instead. 121 | 122 | To target the Test environment: 123 | 124 | ``` go 125 | env := adyen.TestEnvironment() 126 | ``` 127 | 128 | To target the Production environment: 129 | 130 | ``` go 131 | env, err := adyen.ProductionEnvironment("5409c4fd1cc98a4e", "AcmeAccount123") 132 | ``` 133 | 134 | ## To run example 135 | 136 | ### Expose your settings for Adyen API configuration. 137 | 138 | ``` 139 | $ export ADYEN_CLIENT_TOKEN="YOUR_ADYEN_CLIENT_TOKEN" 140 | $ export ADYEN_USERNAME="YOUR_ADYEN_API_USERNAME" 141 | $ export ADYEN_PASSWORD="YOUR_API_PASSWORD" 142 | $ export ADYEN_ACCOUNT="YOUR_MERCHANT_ACCOUNT" 143 | ``` 144 | 145 | Settings explanation: 146 | * ADYEN_CLIENT_TOKEN - Library token in Adyen, used to load external JS file from Adyen to validate Credit Card information 147 | * ADYEN_USERNAME - Adyen API username, usually starts with ws@ 148 | * ADYEN_PASSWORD - Adyen API password for username 149 | * ADYEN_ACCOUNT - Selected Merchant Account 150 | 151 | ## Hosted Payment Pages 152 | 153 | Update your settings to include 154 | 155 | ``` 156 | $ export ADYEN_HMAC="YOUR_HMAC_KEY" 157 | $ export ADYEN_SKINCODE="YOUR_SKINCODE_ID" 158 | $ export ADYEN_SHOPPER_LOCALE="YOUR_SHOPPER_LOCALE" 159 | ``` 160 | 161 | Use HPP constructor to initialize new Adyen API instance 162 | 163 | ```go 164 | import "github.com/zhutik/adyen-api-go" 165 | 166 | // Configure Adyen API 167 | instance := adyen.NewWithHMAC( 168 | adyen.Testing, 169 | os.Getenv("ADYEN_USERNAME"), 170 | os.Getenv("ADYEN_PASSWORD"), 171 | os.Getenv("ADYEN_HMAC"), 172 | ) 173 | 174 | ``` 175 | 176 | Perform requests as usual: 177 | 178 | ```go 179 | timeIn := time.Now().Local().Add(time.Minute * time.Duration(60)) 180 | 181 | req := &adyen.DirectoryLookupRequest{ 182 | CurrencyCode: "EUR", 183 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 184 | PaymentAmount: 1000, 185 | SkinCode: os.Getenv("ADYEN_SKINCODE"), 186 | MerchantReference: "your-order-number", 187 | SessionsValidity: timeIn.Format(time.RFC3339), 188 | } 189 | 190 | g, err := instance.Payment().DirectoryLookup(req) 191 | 192 | ``` 193 | 194 | or generate redirect URL for selected payment method 195 | 196 | Example with iDEAL for Netherlands: 197 | 198 | ```go 199 | timeIn := time.Now().Local().Add(time.Minute * time.Duration(60)) 200 | 201 | req := &adyen.SkipHppRequest{ 202 | MerchantReference: "your-order-number", 203 | PaymentAmount: 1000, 204 | CurrencyCode: "EUR", 205 | ShipBeforeDate: timeIn.Format(time.RFC3339), 206 | SkinCode: os.Getenv("ADYEN_SKINCODE"), 207 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 208 | ShopperLocale: "nl", 209 | SessionsValidity: timeIn.Format(time.RFC3339), 210 | CountryCode: "NL", 211 | BrandCode: "ideal", 212 | IssuerID: "1121", 213 | } 214 | 215 | url, err := instance.Payment().GetHPPRedirectURL(req) 216 | 217 | http.Redirect(w, r, url, http.StatusTemporaryRedirect) 218 | ``` 219 | 220 | Supported Calls: 221 | * Directory Lookup 222 | * Locale payment methods redirect 223 | 224 | ### Setup playgroup 225 | 226 | Please check separate repository with Adyen API playgroup, where you can test API 227 | and get some usage example for Adyen API library 228 | 229 | https://github.com/zhutik/adyen-api-go-example 230 | 231 | ### Perform payments 232 | 233 | Open http://localhost:8080 in your browser and check implemented actions. 234 | 235 | Test credit cards could be found https://docs.adyen.com/support/integration#testcardnumbers 236 | 237 | ## TODOs 238 | 239 | * Move some constants into enum files. 240 | * Parse URLs for environment's BaseURL, ClientURL and HppURL methods instead of string concatenation (needs to return an error as well). 241 | * Reduced API surface by making most types and functions unexported. 242 | -------------------------------------------------------------------------------- /adyen.go: -------------------------------------------------------------------------------- 1 | // Package adyen is Adyen API Library for GO 2 | package adyen 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | const ( 12 | // DefaultCurrency is the default currency for transactions 13 | DefaultCurrency = "EUR" 14 | 15 | // DefaultClientTimeout is the default timeout used when making 16 | // HTTP requests to Adyen. 17 | DefaultClientTimeout = time.Second * 10 18 | 19 | // PaymentAPIVersion - API version of current payment API 20 | PaymentAPIVersion = "v52" 21 | 22 | // RecurringAPIVersion - API version of current recurring API 23 | RecurringAPIVersion = "v49" 24 | 25 | // PaymentService is used to identify the standard payment workflow. 26 | PaymentService = "Payment" 27 | 28 | // RecurringService is used to identify the recurring payment workflow. 29 | RecurringService = "Recurring" 30 | 31 | // CheckoutAPIVersion - API version of current checkout API 32 | CheckoutAPIVersion = "v52" 33 | ) 34 | 35 | // Adyen - base structure with configuration options 36 | // 37 | // - Credentials instance of API creditials to connect to Adyen API 38 | // - Currency is a default request currency. Request data overrides this setting 39 | // - MerchantAccount is default merchant account to be used. Request data overrides this setting 40 | // - client is http client instance 41 | // 42 | // Currency and MerchantAccount should be used only to store the data and be able to use it later. 43 | // Requests won't be automatically populated with given values 44 | type Adyen struct { 45 | Credentials apiCredentials 46 | Currency string 47 | MerchantAccount string 48 | 49 | client *http.Client 50 | } 51 | 52 | // New - creates Adyen instance 53 | // 54 | // Description: 55 | // 56 | // - env - Environment for next API calls 57 | // - username - API username for authentication 58 | // - password - API password for authentication 59 | // - opts - an optional collection of functions that allow you to tweak configurations. 60 | // 61 | // You can create new API user there: https://ca-test.adyen.com/ca/ca/config/users.shtml 62 | func New(env Environment, username, password string, opts ...Option) *Adyen { 63 | creds := makeCredentials(env, username, password) 64 | return NewWithCredentials(env, creds, opts...) 65 | } 66 | 67 | // NewWithHMAC - create new Adyen instance with HPP credentials 68 | // 69 | // Use this constructor when you need to use Adyen HPP API. 70 | // 71 | // Description: 72 | // 73 | // - env - Environment for next API calls 74 | // - username - API username for authentication 75 | // - password - API password for authentication 76 | // - hmac - is generated when new Skin is created in Adyen Customer Area 77 | // - opts - an optional collection of functions that allow you to tweak configurations. 78 | // 79 | // New skin can be created there https://ca-test.adyen.com/ca/ca/skin/skins.shtml 80 | func NewWithHMAC(env Environment, username, password, hmac string, opts ...Option) *Adyen { 81 | creds := makeCredentialsWithHMAC(env, username, password, hmac) 82 | return NewWithCredentials(env, creds, opts...) 83 | } 84 | 85 | // NewWithCredentials - create new Adyen instance with pre-configured credentials. 86 | // 87 | // Description: 88 | // 89 | // - env - Environment for next API calls 90 | // - credentials - configured apiCredentials to use when interacting with Adyen. 91 | // - opts - an optional collection of functions that allow you to tweak configurations. 92 | // 93 | // New skin can be created there https://ca-test.adyen.com/ca/ca/skin/skins.shtml 94 | func NewWithCredentials(env Environment, creds apiCredentials, opts ...Option) *Adyen { 95 | a := Adyen{ 96 | Credentials: creds, 97 | Currency: DefaultCurrency, 98 | client: &http.Client{}, 99 | } 100 | 101 | if opts != nil { 102 | for _, opt := range opts { 103 | opt(&a) 104 | } 105 | } 106 | 107 | return &a 108 | } 109 | 110 | // Option allows for custom configuration overrides. 111 | type Option func(*Adyen) 112 | 113 | // WithTimeout allows for a custom timeout to be provided to the underlying 114 | // HTTP client that's used to communicate with Adyen. 115 | func WithTimeout(d time.Duration) func(*Adyen) { 116 | return func(a *Adyen) { 117 | a.client.Timeout = d 118 | } 119 | } 120 | 121 | // WithTransport allows customer HTTP transports to be provider to the Adyen 122 | func WithTransport(transport http.RoundTripper) func(*Adyen) { 123 | return func(a *Adyen) { 124 | a.client.Transport = transport 125 | } 126 | } 127 | 128 | // WithCurrency allows for custom currencies to be provided to the Adyen. 129 | func WithCurrency(c string) func(*Adyen) { 130 | return func(a *Adyen) { 131 | a.Currency = c 132 | } 133 | } 134 | 135 | // ClientURL - returns URl, that need to loaded in UI, to encrypt Credit Card information 136 | // 137 | // - clientID - Used to load external JS files from Adyen, to encrypt client requests 138 | func (a *Adyen) ClientURL(clientID string) string { 139 | return a.Credentials.Env.ClientURL(clientID) 140 | } 141 | 142 | // adyenURL returns Adyen backend URL 143 | func (a *Adyen) adyenURL(service, requestType, apiVersion string) string { 144 | return a.Credentials.Env.BaseURL(service, apiVersion) + "/" + requestType + "/" 145 | } 146 | 147 | // createHPPUrl returns Adyen HPP url 148 | func (a *Adyen) createHPPUrl(requestType string) string { 149 | return a.Credentials.Env.HppURL(requestType) 150 | } 151 | 152 | // checkoutURL returns the Adyen checkout URL. 153 | func (a *Adyen) checkoutURL(requestType, apiVersion string) string { 154 | return a.Credentials.Env.CheckoutURL(requestType, apiVersion) 155 | } 156 | 157 | // execute request on Adyen side, transforms "requestEntity" into JSON representation 158 | // 159 | // internal method to do a request to Adyen API endpoint 160 | // request Type: POST, request body format - JSON 161 | func (a *Adyen) execute(url string, requestEntity interface{}) (r *Response, err error) { 162 | body, err := json.Marshal(requestEntity) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body)) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | req.Header.Set("Content-Type", "application/json") 173 | req.SetBasicAuth(a.Credentials.Username, a.Credentials.Password) 174 | 175 | resp, err := a.client.Do(req) 176 | if err != nil { 177 | return nil, err 178 | } 179 | defer func() { 180 | if cerr := resp.Body.Close(); cerr != nil { 181 | err = cerr 182 | } 183 | }() 184 | 185 | buf := new(bytes.Buffer) 186 | if _, err = buf.ReadFrom(resp.Body); err != nil { 187 | return nil, err 188 | } 189 | 190 | r = &Response{ 191 | Response: resp, 192 | Body: buf.Bytes(), 193 | } 194 | 195 | if err = r.handleHTTPError(); err != nil { 196 | return nil, err 197 | } 198 | 199 | return 200 | } 201 | 202 | // executeHpp - execute request without authorization to Adyen Hosted Payment API 203 | // 204 | // internal method to request Adyen HPP API via GET 205 | func (a *Adyen) executeHpp(url string, requestEntity interface{}) (r *Response, err error) { 206 | req, err := http.NewRequest(http.MethodGet, url, nil) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | resp, err := a.client.Do(req) 212 | 213 | if err != nil { 214 | return nil, err 215 | } 216 | defer func() { 217 | if cerr := resp.Body.Close(); cerr != nil { 218 | err = cerr 219 | } 220 | }() 221 | 222 | buf := new(bytes.Buffer) 223 | if _, err = buf.ReadFrom(resp.Body); err != nil { 224 | return nil, err 225 | } 226 | 227 | r = &Response{ 228 | Response: resp, 229 | Body: buf.Bytes(), 230 | } 231 | 232 | return 233 | } 234 | 235 | // Payment - returns PaymentGateway 236 | func (a *Adyen) Payment() *PaymentGateway { 237 | return &PaymentGateway{a} 238 | } 239 | 240 | // Modification - returns ModificationGateway 241 | func (a *Adyen) Modification() *ModificationGateway { 242 | return &ModificationGateway{a} 243 | } 244 | 245 | // Recurring - returns RecurringGateway 246 | func (a *Adyen) Recurring() *RecurringGateway { 247 | return &RecurringGateway{a} 248 | } 249 | 250 | // Checkout - returns CheckoutGateway 251 | func (a *Adyen) Checkout() *CheckoutGateway { 252 | return &CheckoutGateway{a} 253 | } 254 | -------------------------------------------------------------------------------- /adyen_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "runtime" 14 | "strings" 15 | "testing" 16 | "time" 17 | 18 | "github.com/joho/godotenv" 19 | ) 20 | 21 | func TestMain(m *testing.M) { 22 | // Set environment variables for subsequent tests. 23 | if err := godotenv.Load(".default.env"); err != nil { 24 | log.Fatalf("error loading .env file: %v", err) 25 | } 26 | 27 | os.Exit(m.Run()) 28 | } 29 | 30 | func TestNewWithTimeout(t *testing.T) { 31 | const timeout = time.Second * 123 32 | 33 | act := New(Testing, "un", "pw", WithTimeout(timeout)) 34 | equals(t, timeout, act.client.Timeout) 35 | } 36 | 37 | func TestNewWithCurrency(t *testing.T) { 38 | const currency = "USD" 39 | 40 | act := New(Testing, "un", "pw", WithCurrency(currency)) 41 | equals(t, currency, act.Currency) 42 | } 43 | 44 | func TestNewWithCustomOptions(t *testing.T) { 45 | const merchant, currency, timeout = "merch", "JPY", time.Second * 21 46 | 47 | f1 := func(a *Adyen) { 48 | a.Currency = currency 49 | a.client.Timeout = timeout 50 | } 51 | 52 | f2 := func(a *Adyen) { 53 | a.MerchantAccount = merchant 54 | } 55 | 56 | act := New(Testing, "un", "pw", f1, f2) 57 | equals(t, merchant, act.MerchantAccount) 58 | equals(t, currency, act.Currency) 59 | equals(t, timeout, act.client.Timeout) 60 | } 61 | 62 | func equals(tb *testing.T, exp interface{}, act interface{}) { 63 | _, fullPath, line, _ := runtime.Caller(1) 64 | file := filepath.Base(fullPath) 65 | 66 | if !reflect.DeepEqual(exp, act) { 67 | fmt.Printf("%s:%d:\n\texp: %[3]v (%[3]T)\n\tgot: %[4]v (%[4]T)\n", file, line, exp, act) 68 | tb.FailNow() 69 | } 70 | } 71 | 72 | func assert(tb *testing.T, cond bool, message string) { 73 | _, fullPath, line, _ := runtime.Caller(1) 74 | file := filepath.Base(fullPath) 75 | 76 | if !cond { 77 | fmt.Printf("%s:%d:\n\t%s\n", file, line, message) 78 | tb.FailNow() 79 | } 80 | } 81 | 82 | // getTestInstance - instanciate adyen for tests 83 | func getTestInstance() *Adyen { 84 | instance := New( 85 | Testing, 86 | os.Getenv("ADYEN_USERNAME"), 87 | os.Getenv("ADYEN_PASSWORD")) 88 | 89 | return instance 90 | } 91 | 92 | // getTestInstanceWithHPP - instanciate adyen for tests 93 | func getTestInstanceWithHPP() *Adyen { 94 | instance := NewWithHMAC( 95 | Testing, 96 | os.Getenv("ADYEN_USERNAME"), 97 | os.Getenv("ADYEN_PASSWORD"), 98 | os.Getenv("ADYEN_HMAC")) 99 | 100 | return instance 101 | } 102 | 103 | // randInt - get random integer from a given range 104 | func randInt(min int, max int) int { 105 | return min + rand.Intn(max-min) 106 | } 107 | 108 | // randomString - generate randorm string of given length 109 | // note: not for use in live code 110 | func randomString(l int) string { 111 | rand.Seed(time.Now().UTC().UnixNano()) 112 | 113 | bytes := make([]byte, l) 114 | for i := 0; i < l; i++ { 115 | bytes[i] = byte(randInt(65, 90)) 116 | } 117 | return string(bytes) 118 | } 119 | 120 | // createTestResponse - create response object for tests 121 | func createTestResponse(input, status string, code int) (*Response, error) { 122 | body := strings.NewReader(input) 123 | 124 | resp := &http.Response{ 125 | Status: status, 126 | StatusCode: code, 127 | ContentLength: int64(body.Len()), 128 | Body: ioutil.NopCloser(body), 129 | } 130 | 131 | buf := new(bytes.Buffer) 132 | _, err := buf.ReadFrom(resp.Body) 133 | 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | response := &Response{ 139 | Response: resp, 140 | Body: buf.Bytes(), 141 | } 142 | 143 | return response, nil 144 | } 145 | -------------------------------------------------------------------------------- /amount.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import "math" 4 | 5 | // Amount value/currency representation 6 | type Amount struct { 7 | Value float32 `json:"value"` 8 | Currency string `json:"currency"` 9 | } 10 | 11 | var ( 12 | // DefaultCurrencyDecimals - default currency decimals 13 | DefaultCurrencyDecimals uint = 2 14 | 15 | // CurrencyDecimals - https://docs.adyen.com/developers/currency-codes 16 | // currencies with 2 decimals stripped out 17 | CurrencyDecimals = map[string]uint{ 18 | "BHD": 3, 19 | "CVE": 0, 20 | "DJF": 0, 21 | "GNF": 0, 22 | "IDR": 0, 23 | "JOD": 3, 24 | "JPY": 0, 25 | "KMF": 0, 26 | "KRW": 0, 27 | "KWD": 3, 28 | "LYD": 3, 29 | "OMR": 3, 30 | "PYG": 0, 31 | "RWF": 0, 32 | "TND": 3, 33 | "UGX": 0, 34 | "VND": 0, 35 | "VUV": 0, 36 | "XAF": 0, 37 | "XOF": 0, 38 | "XPF": 0, 39 | } 40 | ) 41 | 42 | // NewAmount - creates Amount instance 43 | // 44 | // Automatically adjust decimal points for the float value 45 | // Link - https://docs.adyen.com/developers/development-resources/currency-codes 46 | func NewAmount(currency string, amount float32) *Amount { 47 | decimals, ok := CurrencyDecimals[currency] 48 | if !ok { 49 | decimals = DefaultCurrencyDecimals 50 | } 51 | 52 | if decimals == 0 { 53 | return &Amount{ 54 | Currency: currency, 55 | Value: amount, 56 | } 57 | } 58 | 59 | coef := float32(math.Pow(10, float64(decimals))) 60 | 61 | return &Amount{ 62 | Currency: currency, 63 | Value: float32(math.Round(float64(amount * coef))), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /amount_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestNewAmount(t *testing.T) { 9 | cases := []struct { 10 | name string 11 | currency string 12 | amount float32 13 | expected Amount 14 | }{ 15 | { 16 | name: "Test EUR currency", 17 | currency: "EUR", 18 | amount: 10.50, 19 | expected: Amount{Currency: "EUR", Value: 1050}, 20 | }, 21 | { 22 | name: "Test EUR currency, zero case", 23 | currency: "EUR", 24 | amount: 0, 25 | expected: Amount{Currency: "EUR", Value: 0}, 26 | }, 27 | { 28 | name: "Test unknown (UKN) currency, default should be used", 29 | currency: "UKN", 30 | amount: 10.60, 31 | expected: Amount{Currency: "UKN", Value: 1060}, 32 | }, 33 | { 34 | name: "Test CVE currency with zero decimal adjustment", 35 | currency: "CVE", 36 | amount: 150, 37 | expected: Amount{Currency: "CVE", Value: 150}, 38 | }, 39 | { 40 | name: "Test BHD currency with 3 decimal adjustment points", 41 | currency: "BHD", 42 | amount: 150.050, 43 | expected: Amount{Currency: "BHD", Value: 150050}, 44 | }, 45 | { 46 | name: "Test correct float32 conversion", 47 | currency: "EUR", 48 | amount: 8.40, 49 | expected: Amount{Currency: "EUR", Value: 840}, 50 | }, 51 | } 52 | 53 | for _, c := range cases { 54 | t.Run(c.name, func(t *testing.T) { 55 | a := NewAmount(c.currency, c.amount) 56 | 57 | equals(t, c.expected, *a) 58 | }) 59 | } 60 | } 61 | 62 | func TestAmount_UnmarshalJson(t *testing.T) { 63 | t.Parallel() 64 | 65 | structJSON := ` 66 | { 67 | "currency" : "KWD", 68 | "value" : 87230 69 | } 70 | ` 71 | var amount Amount 72 | err := json.Unmarshal([]byte(structJSON), &amount) 73 | if err != nil { 74 | t.Fatalf("unmarshal error: %v", err) 75 | } 76 | if amount.Currency != "KWD" { 77 | t.Fatalf("expected currency KWD, but got %s in unmarshaled struct %+v", amount.Currency, amount) 78 | } 79 | if amount.Value != 87230.0 { 80 | t.Fatalf("expected value 87230.0, but got %f in unmarshaled struct %+v", amount.Value, amount) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /avs.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // AVSResponse is a type definition for all possible responses from Adyen's AVS system 4 | // 5 | // https://docs.adyen.com/risk-management/avs-checks 6 | type AVSResponse string 7 | 8 | // AVSResponse hard-coded for easy comparison checking later 9 | const ( 10 | AVSResponse0 AVSResponse = "0 Unknown" 11 | AVSResponse1 AVSResponse = "1 Address matches, postal code doesn't" 12 | AVSResponse2 AVSResponse = "2 Neither postal code nor address match" 13 | AVSResponse3 AVSResponse = "3 AVS unavailable" 14 | AVSResponse4 AVSResponse = "4 AVS not supported for this card type" 15 | AVSResponse5 AVSResponse = "5 No AVS data provided" 16 | AVSResponse6 AVSResponse = "6 Postal code matches, but the address does not match" 17 | AVSResponse7 AVSResponse = "7 Both postal code and address match" 18 | AVSResponse8 AVSResponse = "8 Address not checked, postal code unknown" 19 | AVSResponse9 AVSResponse = "9 Address matches, postal code unknown" 20 | AVSResponse10 AVSResponse = "10 Address doesn't match, postal code unknown" 21 | AVSResponse11 AVSResponse = "11 Postal code not checked, address unknown" 22 | AVSResponse12 AVSResponse = "12 Address matches, postal code not checked" 23 | AVSResponse13 AVSResponse = "13 Address doesn't match, postal code not checked" 24 | AVSResponse14 AVSResponse = "14 Postal code matches, address unknown" 25 | AVSResponse15 AVSResponse = "15 Postal code matches, address not checked" 26 | AVSResponse16 AVSResponse = "16 Postal code doesn't match, address unknown" 27 | AVSResponse17 AVSResponse = "17 Postal code doesn't match, address not checked." 28 | AVSResponse18 AVSResponse = "18 Neither postal code nor address were checked" 29 | AVSResponse19 AVSResponse = "19 Name and postal code matches" 30 | AVSResponse20 AVSResponse = "20 Name, address and postal code matches" 31 | AVSResponse21 AVSResponse = "21 Name and address matches" 32 | AVSResponse22 AVSResponse = "22 Name matches" 33 | AVSResponse23 AVSResponse = "23 Postal code matches, name doesn't match" 34 | AVSResponse24 AVSResponse = "24 Both postal code and address matches, name doesn't match" 35 | AVSResponse25 AVSResponse = "25 Address matches, name doesn't match" 36 | AVSResponse26 AVSResponse = "26 Neither postal code, address nor name matches" 37 | ) 38 | -------------------------------------------------------------------------------- /boolean.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // StringBool allows us to unmarhsal Adyen Boolean values 9 | // which appear as strings instead of bools. 10 | type StringBool bool 11 | 12 | // NewStringBool returns an instance of StringBool representing a given bool 13 | func NewStringBool(b bool) *StringBool { 14 | sb := StringBool(b) 15 | return &sb 16 | } 17 | 18 | // UnmarshalJSON unmarshalls to a StringBool from a slice of bytes 19 | func (b *StringBool) UnmarshalJSON(data []byte) (err error) { 20 | str := strings.TrimFunc(strings.ToLower(string(data)), func(c rune) bool { 21 | return c == ' ' || c == '"' 22 | }) 23 | 24 | parsed, err := strconv.ParseBool(str) 25 | if err != nil { 26 | return 27 | } 28 | 29 | *b = StringBool(parsed) 30 | return 31 | } 32 | 33 | // MarshalJSON marshalls a StringBool to a slice of bytes 34 | func (b StringBool) MarshalJSON() ([]byte, error) { 35 | boolResult := bool(b) 36 | var boolString string 37 | 38 | if boolResult { 39 | boolString = `"true"` 40 | } else { 41 | boolString = `"false"` 42 | } 43 | 44 | return []byte(boolString), nil 45 | } 46 | -------------------------------------------------------------------------------- /boolean_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | type thing struct { 9 | Bool StringBool `json:"value"` 10 | } 11 | 12 | type thingWithEmpty struct { 13 | Bool *StringBool `json:"value,omitempty"` 14 | } 15 | 16 | func TestStringBool_Unmarshal(t *testing.T) { 17 | cases := []struct { 18 | name string 19 | json string 20 | exp bool 21 | expErr bool 22 | }{ 23 | { 24 | name: "empty", 25 | json: `{ "value": "" }`, 26 | exp: false, 27 | expErr: true, 28 | }, 29 | { 30 | name: "true", 31 | json: `{ "value": "true" }`, 32 | exp: true, 33 | }, 34 | { 35 | name: "TRUE", 36 | json: `{ "value": "TRUE" }`, 37 | exp: true, 38 | }, 39 | { 40 | name: "1", 41 | json: `{ "value": "1" }`, 42 | exp: true, 43 | }, 44 | { 45 | name: "spaces", 46 | json: `{ "value": " true " }`, 47 | exp: true, 48 | }, 49 | { 50 | name: "false", 51 | json: `{ "value": "false" }`, 52 | exp: false, 53 | }, 54 | { 55 | name: "FALSE", 56 | json: `{ "value": "FALSE" }`, 57 | exp: false, 58 | }, 59 | { 60 | name: "0", 61 | json: `{ "value": "0" }`, 62 | exp: false, 63 | }, 64 | { 65 | name: "spaces", 66 | json: `{ "value": " false " }`, 67 | exp: false, 68 | }, 69 | { 70 | name: "spaces", 71 | json: `{ "value": " false " }`, 72 | exp: false, 73 | }, 74 | } 75 | 76 | for _, c := range cases { 77 | t.Run(c.name, func(t *testing.T) { 78 | var th thing 79 | err := json.Unmarshal([]byte(c.json), &th) 80 | if !c.expErr && err != nil { 81 | t.Fatalf("unexpected error: %v", err) 82 | } 83 | if c.expErr && err == nil { 84 | t.Fatalf("expected error but didn't get one") 85 | } 86 | 87 | if StringBool(c.exp) != th.Bool { 88 | t.Fatalf("my exp: %v but got %v", c.exp, th.Bool) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func TestStringBool_Marshal(t *testing.T) { 95 | cases := []struct { 96 | name string 97 | object thing 98 | expected string 99 | }{ 100 | { 101 | name: "true", 102 | object: thing{Bool: StringBool(true)}, 103 | expected: `{"value":"true"}`, 104 | }, 105 | { 106 | name: "false", 107 | object: thing{Bool: StringBool(false)}, 108 | expected: `{"value":"false"}`, 109 | }, 110 | } 111 | 112 | for _, c := range cases { 113 | t.Run(c.name, func(t *testing.T) { 114 | res, _ := json.Marshal(c.object) 115 | str := string(res) 116 | 117 | if c.expected != str { 118 | t.Fatalf("my exp: %v but got %v", c.expected, str) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestStringBool_MarshalWithOmitempty(t *testing.T) { 125 | valueTrue := StringBool(true) 126 | valueFalse := StringBool(false) 127 | 128 | cases := []struct { 129 | name string 130 | object thingWithEmpty 131 | expected string 132 | }{ 133 | { 134 | name: "true", 135 | object: thingWithEmpty{Bool: &valueTrue}, 136 | expected: `{"value":"true"}`, 137 | }, 138 | { 139 | name: "false", 140 | object: thingWithEmpty{Bool: &valueFalse}, 141 | expected: `{"value":"false"}`, 142 | }, 143 | { 144 | name: "false", 145 | object: thingWithEmpty{}, 146 | expected: `{}`, 147 | }, 148 | } 149 | 150 | for _, c := range cases { 151 | t.Run(c.name, func(t *testing.T) { 152 | res, _ := json.Marshal(c.object) 153 | str := string(res) 154 | 155 | if c.expected != str { 156 | t.Fatalf("my exp: %v but got %v", c.expected, str) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /checkout.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // PaymentMethods contains the fields required by the checkout 4 | // API's /paymentMethods endpoint. See the following for more 5 | // information: 6 | // 7 | // https://docs.adyen.com/api-explorer/#/PaymentSetupAndVerificationService/v32/paymentMethods 8 | type PaymentMethods struct { 9 | Amount *Amount `json:"amount"` 10 | Channel string `json:"channel"` 11 | CountryCode string `json:"countryCode"` 12 | MerchantAccount string `json:"merchantAccount"` 13 | ShopperLocale string `json:"shopperLocale"` 14 | ShopperReference string `json:"shopperReference"` 15 | } 16 | 17 | // PaymentMethodsResponse is returned by Adyen in response to 18 | // a PaymentMethods request. 19 | type PaymentMethodsResponse struct { 20 | PaymentMethods []PaymentMethodDetails `json:"paymentMethods"` 21 | OneClickPaymentMethods []OneClickPaymentMethodDetails `json:"oneClickPaymentMethods,omitempty"` 22 | } 23 | 24 | // PaymentMethodDetails describes the PaymentMethods part of 25 | // a PaymentMethodsResponse. 26 | type PaymentMethodDetails struct { 27 | Details []PaymentMethodDetailsInfo `json:"details,omitempty"` 28 | Name string `json:"name"` 29 | Type string `json:"type"` 30 | } 31 | 32 | // PaymentMethodDetailsInfo describes the collection of all 33 | // payment methods. 34 | type PaymentMethodDetailsInfo struct { 35 | Items []PaymentMethodItems `json:"items"` 36 | Key string `json:"key"` 37 | Type string `json:"type"` 38 | } 39 | 40 | // PaymentMethodItems describes a single payment method. 41 | type PaymentMethodItems struct { 42 | ID string `json:"id"` 43 | Name string `json:"name"` 44 | } 45 | 46 | // OneClickPaymentMethodDetails describes the OneClickPayment part of 47 | // a PaymentMethods response. 48 | type OneClickPaymentMethodDetails struct { 49 | Details []PaymentMethodTypes `json:"details"` 50 | Name string `json:"name"` 51 | Type string `json:"type"` 52 | StoredDetails PaymentMethodStoredDetails `json:"storedDetails"` 53 | } 54 | 55 | // PaymentMethodTypes describes any additional information associated 56 | // with a OneClick payment. 57 | type PaymentMethodTypes struct { 58 | Key string `json:"key"` 59 | Type string `json:"type"` 60 | } 61 | 62 | // PaymentMethodStoredDetails describes the information stored for a 63 | // OneClick payment. 64 | type PaymentMethodStoredDetails struct { 65 | Card PaymentMethodCard `json:"card"` 66 | } 67 | 68 | // PaymentMethodCard describes the card information associated with a 69 | // OneClick payment. 70 | type PaymentMethodCard struct { 71 | ExpiryMonth string `json:"expiryMonth"` 72 | ExpiryYear string `json:"expiryYear"` 73 | HolderName string `json:"holderName"` 74 | Number string `json:"number"` 75 | } 76 | -------------------------------------------------------------------------------- /checkout_gateway.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // CheckoutGateway - allows you to accept all of Adyen's payment 4 | // methods and flows. 5 | type CheckoutGateway struct { 6 | *Adyen 7 | } 8 | 9 | const ( 10 | paymentMethodsURL = "paymentMethods" 11 | ) 12 | 13 | // PaymentMethods - Perform paymentMethods request in Adyen. 14 | // 15 | // Used to get a collection of available payment methods for a merchant. 16 | func (a *CheckoutGateway) PaymentMethods(req *PaymentMethods) (*PaymentMethodsResponse, error) { 17 | url := a.checkoutURL(paymentMethodsURL, CheckoutAPIVersion) 18 | 19 | resp, err := a.execute(url, req) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return resp.paymentMethods() 25 | } 26 | -------------------------------------------------------------------------------- /checkout_gateway_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // TestPaymentMethods - test for https://docs.adyen.com/developers/checkout/api-integration 9 | // 10 | // This test requires CheckoutAPI access. To obtain, visit https://docs.adyen.com/developers/user-management/how-to-get-the-checkout-api-key. 11 | func TestPaymentMethods(t *testing.T) { 12 | t.Parallel() 13 | 14 | instance := getTestInstance() 15 | 16 | request := &PaymentMethods{ 17 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 18 | } 19 | 20 | _, err := instance.Checkout().PaymentMethods(request) 21 | 22 | knownError, ok := err.(APIError) 23 | if ok { 24 | t.Errorf("Response should be succesfull. Known API Error: Code - %s, Message - %s, Type - %s", knownError.ErrorCode, knownError.Message, knownError.ErrorType) 25 | } 26 | 27 | if err != nil { 28 | t.Errorf("Response should be succesfull, error - %s", err.Error()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /checkout_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestPaymentMethodsResponse_ParseMerchantAccount(t *testing.T) { 9 | rawResponse := `{"paymentMethods":[{"details":[{"key":"additionalData.card.encrypted.json","type":"cardToken"}],"name":"Credit Card","type":"scheme"},{"details":[{"items":[{"id":"1121","name":"Test Issuer"},{"id":"1154","name":"Test Issuer 5"},{"id":"1153","name":"Test Issuer 4"},{"id":"1152","name":"Test Issuer 3"},{"id":"1151","name":"Test Issuer 2"},{"id":"1162","name":"Test Issuer Cancelled"},{"id":"1161","name":"Test Issuer Pending"},{"id":"1160","name":"Test Issuer Refused"},{"id":"1159","name":"Test Issuer 10"},{"id":"1158","name":"Test Issuer 9"},{"id":"1157","name":"Test Issuer 8"},{"id":"1156","name":"Test Issuer 7"},{"id":"1155","name":"Test Issuer 6"}],"key":"idealIssuer","type":"select"}],"name":"iDEAL","type":"ideal"},{"name":"Pay later with Klarna.","type":"klarna"},{"details":[{"key":"sepa.ownerName","type":"text"},{"key":"sepa.ibanNumber","type":"text"}],"name":"SEPA Direct Debit","type":"sepadirectdebit"},{"name":"UnionPay","type":"unionpay"}]}` 10 | 11 | var response PaymentMethodsResponse 12 | if err := json.Unmarshal([]byte(rawResponse), &response); err != nil { 13 | t.Fatalf("error unmarshalling json: %v", err) 14 | } 15 | 16 | exp := PaymentMethodsResponse{ 17 | PaymentMethods: []PaymentMethodDetails{ 18 | PaymentMethodDetails{ 19 | Details: []PaymentMethodDetailsInfo{ 20 | PaymentMethodDetailsInfo{ 21 | Key: "additionalData.card.encrypted.json", 22 | Type: "cardToken"}}, 23 | Name: "Credit Card", 24 | Type: "scheme"}, 25 | PaymentMethodDetails{ 26 | Details: []PaymentMethodDetailsInfo{ 27 | PaymentMethodDetailsInfo{ 28 | Items: []PaymentMethodItems{ 29 | PaymentMethodItems{ID: "1121", Name: "Test Issuer"}, 30 | PaymentMethodItems{ID: "1154", Name: "Test Issuer 5"}, 31 | PaymentMethodItems{ID: "1153", Name: "Test Issuer 4"}, 32 | PaymentMethodItems{ID: "1152", Name: "Test Issuer 3"}, 33 | PaymentMethodItems{ID: "1151", Name: "Test Issuer 2"}, 34 | PaymentMethodItems{ID: "1162", Name: "Test Issuer Cancelled"}, 35 | PaymentMethodItems{ID: "1161", Name: "Test Issuer Pending"}, 36 | PaymentMethodItems{ID: "1160", Name: "Test Issuer Refused"}, 37 | PaymentMethodItems{ID: "1159", Name: "Test Issuer 10"}, 38 | PaymentMethodItems{ID: "1158", Name: "Test Issuer 9"}, 39 | PaymentMethodItems{ID: "1157", Name: "Test Issuer 8"}, 40 | PaymentMethodItems{ID: "1156", Name: "Test Issuer 7"}, 41 | PaymentMethodItems{ID: "1155", Name: "Test Issuer 6"}}, 42 | Key: "idealIssuer", Type: "select"}, 43 | }, 44 | Name: "iDEAL", 45 | Type: "ideal", 46 | }, 47 | PaymentMethodDetails{ 48 | Name: "Pay later with Klarna.", 49 | Type: "klarna"}, 50 | PaymentMethodDetails{ 51 | Details: []PaymentMethodDetailsInfo{ 52 | PaymentMethodDetailsInfo{ 53 | Key: "sepa.ownerName", 54 | Type: "text", 55 | }, 56 | PaymentMethodDetailsInfo{ 57 | Key: "sepa.ibanNumber", 58 | Type: "text", 59 | }, 60 | }, 61 | Name: "SEPA Direct Debit", 62 | Type: "sepadirectdebit", 63 | }, 64 | PaymentMethodDetails{ 65 | Name: "UnionPay", 66 | Type: "unionpay", 67 | }, 68 | }, 69 | } 70 | 71 | equals(t, exp, response) 72 | } 73 | 74 | func TestPaymentMethodsResponse_ParseCountryAmount(t *testing.T) { 75 | rawResponse := `{"paymentMethods":[{"details":[{"items":[{"id":"1121","name":"Test Issuer"},{"id":"1154","name":"Test Issuer 5"},{"id":"1153","name":"Test Issuer 4"},{"id":"1152","name":"Test Issuer 3"},{"id":"1151","name":"Test Issuer 2"},{"id":"1162","name":"Test Issuer Cancelled"},{"id":"1161","name":"Test Issuer Pending"},{"id":"1160","name":"Test Issuer Refused"},{"id":"1159","name":"Test Issuer 10"},{"id":"1158","name":"Test Issuer 9"},{"id":"1157","name":"Test Issuer 8"},{"id":"1156","name":"Test Issuer 7"},{"id":"1155","name":"Test Issuer 6"}],"key":"idealIssuer","type":"select"}],"name":"iDEAL","type":"ideal"},{"details":[{"key":"additionalData.card.encrypted.json","type":"cardToken"}],"name":"Credit Card","type":"scheme"},{"name":"Pay later with Klarna.","type":"klarna"},{"details":[{"key":"sepa.ownerName","type":"text"},{"key":"sepa.ibanNumber","type":"text"}],"name":"SEPA Direct Debit","type":"sepadirectdebit"},{"name":"UnionPay","type":"unionpay"}]}` 76 | 77 | var response PaymentMethodsResponse 78 | if err := json.Unmarshal([]byte(rawResponse), &response); err != nil { 79 | t.Fatalf("error unmarshalling json: %v", err) 80 | } 81 | 82 | exp := PaymentMethodsResponse{ 83 | PaymentMethods: []PaymentMethodDetails{ 84 | PaymentMethodDetails{ 85 | Details: []PaymentMethodDetailsInfo{ 86 | PaymentMethodDetailsInfo{ 87 | Items: []PaymentMethodItems{ 88 | PaymentMethodItems{ID: "1121", Name: "Test Issuer"}, 89 | PaymentMethodItems{ID: "1154", Name: "Test Issuer 5"}, 90 | PaymentMethodItems{ID: "1153", Name: "Test Issuer 4"}, 91 | PaymentMethodItems{ID: "1152", Name: "Test Issuer 3"}, 92 | PaymentMethodItems{ID: "1151", Name: "Test Issuer 2"}, 93 | PaymentMethodItems{ID: "1162", Name: "Test Issuer Cancelled"}, 94 | PaymentMethodItems{ID: "1161", Name: "Test Issuer Pending"}, 95 | PaymentMethodItems{ID: "1160", Name: "Test Issuer Refused"}, 96 | PaymentMethodItems{ID: "1159", Name: "Test Issuer 10"}, 97 | PaymentMethodItems{ID: "1158", Name: "Test Issuer 9"}, 98 | PaymentMethodItems{ID: "1157", Name: "Test Issuer 8"}, 99 | PaymentMethodItems{ID: "1156", Name: "Test Issuer 7"}, 100 | PaymentMethodItems{ID: "1155", Name: "Test Issuer 6"}, 101 | }, 102 | Key: "idealIssuer", 103 | Type: "select", 104 | }, 105 | }, 106 | Name: "iDEAL", 107 | Type: "ideal", 108 | }, 109 | PaymentMethodDetails{ 110 | Details: []PaymentMethodDetailsInfo{ 111 | PaymentMethodDetailsInfo{ 112 | Key: "additionalData.card.encrypted.json", 113 | Type: "cardToken"}, 114 | }, 115 | Name: "Credit Card", 116 | Type: "scheme", 117 | }, 118 | PaymentMethodDetails{ 119 | Name: "Pay later with Klarna.", 120 | Type: "klarna", 121 | }, 122 | PaymentMethodDetails{ 123 | Details: []PaymentMethodDetailsInfo{ 124 | PaymentMethodDetailsInfo{ 125 | Key: "sepa.ownerName", 126 | Type: "text", 127 | }, 128 | PaymentMethodDetailsInfo{ 129 | Key: "sepa.ibanNumber", 130 | Type: "text", 131 | }, 132 | }, 133 | Name: "SEPA Direct Debit", 134 | Type: "sepadirectdebit", 135 | }, 136 | PaymentMethodDetails{ 137 | Name: "UnionPay", 138 | Type: "unionpay", 139 | }, 140 | }, 141 | } 142 | 143 | equals(t, exp, response) 144 | } 145 | 146 | func TestPaymentMethodsResponse_ParseOneClick(t *testing.T) { 147 | rawResponse := `{"oneClickPaymentMethods":[{"details":[{"key":"cardDetails.cvc","type":"cvc"}],"name":"VISA","type":"visa","storedDetails":{"card":{"expiryMonth":"8","expiryYear":"2018","holderName":"John Smith","number":"1111"}}}],"paymentMethods":[{"details":[{"items":[{"id":"1121","name":"Test Issuer"},{"id":"1154","name":"Test Issuer 5"},{"id":"1153","name":"Test Issuer 4"},{"id":"1152","name":"Test Issuer 3"},{"id":"1151","name":"Test Issuer 2"},{"id":"1162","name":"Test Issuer Cancelled"},{"id":"1161","name":"Test Issuer Pending"},{"id":"1160","name":"Test Issuer Refused"},{"id":"1159","name":"Test Issuer 10"},{"id":"1158","name":"Test Issuer 9"},{"id":"1157","name":"Test Issuer 8"},{"id":"1156","name":"Test Issuer 7"},{"id":"1155","name":"Test Issuer 6"}],"key":"idealIssuer","type":"select"}],"name":"iDEAL","type":"ideal"},{"details":[{"key":"additionalData.card.encrypted.json","type":"cardToken"},{"key":"storeDetails","optional":"true","type":"boolean"}],"name":"Credit Card","type":"scheme"},{"name":"Pay later with Klarna.","type":"klarna"},{"details":[{"key":"sepa.ownerName","type":"text"},{"key":"sepa.ibanNumber","type":"text"}],"name":"SEPA Direct Debit","type":"sepadirectdebit"},{"name":"UnionPay","type":"unionpay"}]}` 148 | 149 | var response PaymentMethodsResponse 150 | if err := json.Unmarshal([]byte(rawResponse), &response); err != nil { 151 | t.Fatalf("error unmarshalling json: %v", err) 152 | } 153 | 154 | exp := PaymentMethodsResponse{ 155 | PaymentMethods: []PaymentMethodDetails{ 156 | PaymentMethodDetails{ 157 | Details: []PaymentMethodDetailsInfo{ 158 | PaymentMethodDetailsInfo{ 159 | Items: []PaymentMethodItems{ 160 | PaymentMethodItems{ID: "1121", Name: "Test Issuer"}, 161 | PaymentMethodItems{ID: "1154", Name: "Test Issuer 5"}, 162 | PaymentMethodItems{ID: "1153", Name: "Test Issuer 4"}, 163 | PaymentMethodItems{ID: "1152", Name: "Test Issuer 3"}, 164 | PaymentMethodItems{ID: "1151", Name: "Test Issuer 2"}, 165 | PaymentMethodItems{ID: "1162", Name: "Test Issuer Cancelled"}, 166 | PaymentMethodItems{ID: "1161", Name: "Test Issuer Pending"}, 167 | PaymentMethodItems{ID: "1160", Name: "Test Issuer Refused"}, 168 | PaymentMethodItems{ID: "1159", Name: "Test Issuer 10"}, 169 | PaymentMethodItems{ID: "1158", Name: "Test Issuer 9"}, 170 | PaymentMethodItems{ID: "1157", Name: "Test Issuer 8"}, 171 | PaymentMethodItems{ID: "1156", Name: "Test Issuer 7"}, 172 | PaymentMethodItems{ID: "1155", Name: "Test Issuer 6"}, 173 | }, 174 | Key: "idealIssuer", 175 | Type: "select", 176 | }, 177 | }, 178 | Name: "iDEAL", 179 | Type: "ideal"}, 180 | PaymentMethodDetails{ 181 | Details: []PaymentMethodDetailsInfo{ 182 | PaymentMethodDetailsInfo{ 183 | Key: "additionalData.card.encrypted.json", 184 | Type: "cardToken", 185 | }, 186 | PaymentMethodDetailsInfo{ 187 | Key: "storeDetails", 188 | Type: "boolean", 189 | }, 190 | }, 191 | Name: "Credit Card", 192 | Type: "scheme", 193 | }, 194 | PaymentMethodDetails{ 195 | Name: "Pay later with Klarna.", 196 | Type: "klarna", 197 | }, 198 | PaymentMethodDetails{ 199 | Details: []PaymentMethodDetailsInfo{ 200 | PaymentMethodDetailsInfo{ 201 | Key: "sepa.ownerName", 202 | Type: "text", 203 | }, 204 | PaymentMethodDetailsInfo{ 205 | Key: "sepa.ibanNumber", 206 | Type: "text", 207 | }, 208 | }, 209 | Name: "SEPA Direct Debit", 210 | Type: "sepadirectdebit", 211 | }, 212 | PaymentMethodDetails{ 213 | Name: "UnionPay", 214 | Type: "unionpay", 215 | }, 216 | }, 217 | OneClickPaymentMethods: []OneClickPaymentMethodDetails{ 218 | OneClickPaymentMethodDetails{ 219 | Details: []PaymentMethodTypes{ 220 | PaymentMethodTypes{ 221 | Key: "cardDetails.cvc", 222 | Type: "cvc", 223 | }, 224 | }, 225 | Name: "VISA", 226 | Type: "visa", 227 | StoredDetails: PaymentMethodStoredDetails{ 228 | Card: PaymentMethodCard{ 229 | ExpiryMonth: "8", 230 | ExpiryYear: "2018", 231 | HolderName: "John Smith", 232 | Number: "1111", 233 | }, 234 | }, 235 | }, 236 | }, 237 | } 238 | 239 | equals(t, exp, response) 240 | } 241 | -------------------------------------------------------------------------------- /credetials.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // apiCredentials basic API settings 4 | // 5 | // Description: 6 | // 7 | // - Env - Environment for next API calls 8 | // - Username - API username for authentication 9 | // - Password - API password for authentication 10 | // - Hmac - Hash-based Message Authentication Code (HMAC) setting 11 | // 12 | // You can create new API user there: https://ca-test.adyen.com/ca/ca/config/users.shtml 13 | // New skin can be created there https://ca-test.adyen.com/ca/ca/skin/skins.shtml 14 | type apiCredentials struct { 15 | Env Environment 16 | Username string 17 | Password string 18 | Hmac string 19 | } 20 | 21 | // makeCredentials create new APICredentials 22 | func makeCredentials(env Environment, username, password string) apiCredentials { 23 | return apiCredentials{ 24 | Env: env, 25 | Username: username, 26 | Password: password, 27 | } 28 | } 29 | 30 | // makeCredentialsWithHMAC create new APICredentials with HMAC signature 31 | func makeCredentialsWithHMAC(env Environment, username, password, hmac string) apiCredentials { 32 | return apiCredentials{ 33 | Env: env, 34 | Username: username, 35 | Password: password, 36 | Hmac: hmac, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cvv.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // CVCResult represents the Adyen translation of CVC codes from issuer 4 | // https://docs.adyen.com/development-resources/test-cards/cvc-cvv-result-testing 5 | type CVCResult string 6 | 7 | // Constants represented by numerical code they are assigned 8 | const ( 9 | CVCResult0 CVCResult = "0 Unknown" 10 | CVCResult1 CVCResult = "1 Matches" 11 | CVCResult2 CVCResult = "2 Doesn't Match" 12 | CVCResult3 CVCResult = "3 Not Checked" 13 | CVCResult4 CVCResult = "4 No CVC/CVV provided, but was required" 14 | CVCResult5 CVCResult = "5 Issuer not certified for CVC/CVV" 15 | CVCResult6 CVCResult = "6 No CVC/CVV provided" 16 | ) 17 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // Environment allows clients to be configured for Testing 9 | // and Production environments. 10 | type Environment struct { 11 | apiURL string 12 | clientURL string 13 | hppURL string 14 | checkoutURL string 15 | } 16 | 17 | var ( 18 | errProdEnvValidation = errors.New("production requires random and company name fields as per https://docs.adyen.com/developers/api-reference/live-endpoints") 19 | ) 20 | 21 | // Testing - instance of testing environment 22 | var Testing = Environment{ 23 | apiURL: "https://pal-test.adyen.com/pal/servlet", 24 | clientURL: "https://test.adyen.com/hpp/cse/js/", 25 | hppURL: "https://test.adyen.com/hpp/", 26 | checkoutURL: "https://checkout-test.adyen.com/services/PaymentSetupAndVerification", 27 | } 28 | 29 | // Production - instance of production environment 30 | var Production = Environment{ 31 | apiURL: "https://%s-%s-pal-live.adyenpayments.com/pal/servlet", 32 | clientURL: "https://live.adyen.com/hpp/cse/js/", 33 | hppURL: "https://live.adyen.com/hpp/", 34 | checkoutURL: "https://%s-%s-checkout-live.adyenpayments.com/services/PaymentSetupAndVerification", 35 | } 36 | 37 | // TestEnvironment returns test environment configuration. 38 | func TestEnvironment() (e Environment) { 39 | return Testing 40 | } 41 | 42 | // ProductionEnvironment returns production environment configuration. 43 | func ProductionEnvironment(random, companyName string) (e Environment, err error) { 44 | if random == "" || companyName == "" { 45 | err = errProdEnvValidation 46 | return 47 | } 48 | e = Production 49 | e.apiURL = fmt.Sprintf(e.apiURL, random, companyName) 50 | e.checkoutURL = fmt.Sprintf(e.checkoutURL, random, companyName) 51 | return e, nil 52 | } 53 | 54 | // BaseURL returns api base url 55 | func (e Environment) BaseURL(service string, version string) string { 56 | return e.apiURL + "/" + service + "/" + version 57 | } 58 | 59 | // ClientURL returns Adyen Client URL to load external scripts 60 | func (e Environment) ClientURL(clientID string) string { 61 | return e.clientURL + clientID + ".shtml" 62 | } 63 | 64 | // HppURL returns Adyen HPP url to execute Hosted Payment Paged API requests 65 | func (e Environment) HppURL(request string) string { 66 | return e.hppURL + request + ".shtml" 67 | } 68 | 69 | // CheckoutURL returns the full URL to a Checkout API endpoint. 70 | func (e Environment) CheckoutURL(service string, version string) string { 71 | return e.checkoutURL + "/" + version + "/" + service 72 | } 73 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import "testing" 4 | 5 | func TestTestEnvironment(t *testing.T) { 6 | act := TestEnvironment() 7 | equals(t, Testing.apiURL, act.apiURL) 8 | equals(t, Testing.clientURL, act.clientURL) 9 | equals(t, Testing.hppURL, act.hppURL) 10 | } 11 | 12 | func TestBaseURLEnvironmentTesting(t *testing.T) { 13 | env := TestEnvironment() 14 | act := env.BaseURL("service", "version") 15 | exp := "https://pal-test.adyen.com/pal/servlet/service/version" 16 | 17 | equals(t, exp, act) 18 | } 19 | 20 | func TestClientURLEnvironmentTesting(t *testing.T) { 21 | env := TestEnvironment() 22 | act := env.ClientURL("clientID") 23 | exp := "https://test.adyen.com/hpp/cse/js/clientID.shtml" 24 | 25 | equals(t, exp, act) 26 | } 27 | 28 | func TestHppURLEnvironmentTest(t *testing.T) { 29 | env := TestEnvironment() 30 | act := env.HppURL("request") 31 | exp := "https://test.adyen.com/hpp/request.shtml" 32 | 33 | equals(t, exp, act) 34 | } 35 | 36 | func TestCheckoutURLEnvironmentTesting(t *testing.T) { 37 | env := TestEnvironment() 38 | act := env.CheckoutURL("service", "version") 39 | exp := "https://checkout-test.adyen.com/services/PaymentSetupAndVerification/version/service" 40 | 41 | equals(t, exp, act) 42 | } 43 | 44 | func TestEnvironmentProductionValidation(t *testing.T) { 45 | cases := []struct { 46 | name string 47 | random string 48 | companyName string 49 | }{ 50 | { 51 | name: "missing random", 52 | random: "", 53 | companyName: "AcmeAccount123", 54 | }, 55 | { 56 | name: "missing company name", 57 | random: "5409c4fd1cc98a4e", 58 | companyName: "", 59 | }, 60 | { 61 | name: "missing random and company name", 62 | random: "", 63 | companyName: "", 64 | }, 65 | } 66 | 67 | for _, c := range cases { 68 | t.Run(c.name, func(t *testing.T) { 69 | _, err := ProductionEnvironment(c.random, c.companyName) 70 | equals(t, errProdEnvValidation, err) 71 | }) 72 | } 73 | } 74 | 75 | func TestBaseURLEnvironmentProduction(t *testing.T) { 76 | env, err := ProductionEnvironment("5409c4fd1cc98a4e", "AcmeAccount123") 77 | if err != nil { 78 | t.Fatalf("error creating production environment: %v", err) 79 | } 80 | 81 | act := env.BaseURL("service", "version") 82 | exp := "https://5409c4fd1cc98a4e-AcmeAccount123-pal-live.adyenpayments.com/pal/servlet/service/version" 83 | 84 | equals(t, exp, act) 85 | } 86 | 87 | func TestClientURLEnvironmentProduction(t *testing.T) { 88 | env, err := ProductionEnvironment("5409c4fd1cc98a4e", "AcmeAccount123") 89 | if err != nil { 90 | t.Fatalf("error creating production environment: %v", err) 91 | } 92 | 93 | act := env.ClientURL("clientID") 94 | exp := "https://live.adyen.com/hpp/cse/js/clientID.shtml" 95 | 96 | equals(t, exp, act) 97 | } 98 | 99 | func TestHppURLEnvironmentProduction(t *testing.T) { 100 | env, err := ProductionEnvironment("5409c4fd1cc98a4e", "AcmeAccount123") 101 | if err != nil { 102 | t.Fatalf("error creating production environment: %v", err) 103 | } 104 | 105 | act := env.HppURL("request") 106 | exp := "https://live.adyen.com/hpp/request.shtml" 107 | 108 | equals(t, exp, act) 109 | } 110 | 111 | func TestCheckoutURLEnvironmentProduction(t *testing.T) { 112 | env, err := ProductionEnvironment("5409c4fd1cc98a4e", "AcmeAccount123") 113 | if err != nil { 114 | t.Fatalf("error creating production environment: %v", err) 115 | } 116 | 117 | act := env.CheckoutURL("service", "version") 118 | exp := "https://5409c4fd1cc98a4e-AcmeAccount123-checkout-live.adyenpayments.com/services/PaymentSetupAndVerification/version/service" 119 | 120 | equals(t, exp, act) 121 | } 122 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zhutik/adyen-api-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/go-querystring v1.0.0 7 | github.com/joho/godotenv v1.3.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 2 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 3 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 4 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 5 | -------------------------------------------------------------------------------- /modification.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | /********* 4 | * Cancel * 5 | *********/ 6 | 7 | // Adjust authorisation reasons 8 | // 9 | // Link https://docs.adyen.com/developers/api-reference/payments-api/modificationrequest/adjustauthorisationmodificationrequest 10 | const ( 11 | DelayedCharge = "DelayedCharge" 12 | NoShow = "NoShow" 13 | ) 14 | 15 | // Cancel structure for Cancel request 16 | type Cancel struct { 17 | Reference string `json:"reference"` 18 | MerchantAccount string `json:"merchantAccount"` 19 | OriginalReference string `json:"originalReference"` 20 | } 21 | 22 | // CancelResponse is a response structure for Adyen cancellation 23 | type CancelResponse struct { 24 | PspReference string `json:"pspReference"` 25 | Response string `json:"response"` 26 | } 27 | 28 | // CancelOrRefundResponse is a response structure for Adyen cancelOrRefund 29 | type CancelOrRefundResponse struct { 30 | PspReference string `json:"pspReference"` 31 | Response string `json:"response"` 32 | } 33 | 34 | /********** 35 | * Capture * 36 | **********/ 37 | 38 | // Capture structure for Capture request 39 | type Capture struct { 40 | ModificationAmount *Amount `json:"modificationAmount"` 41 | Reference string `json:"reference"` 42 | MerchantAccount string `json:"merchantAccount"` 43 | OriginalReference string `json:"originalReference"` 44 | } 45 | 46 | // CaptureResponse is a response structure for Adyen capture 47 | type CaptureResponse struct { 48 | PspReference string `json:"pspReference"` 49 | Response string `json:"response"` 50 | } 51 | 52 | /********* 53 | * Refund * 54 | *********/ 55 | 56 | // Refund structure for refund request 57 | type Refund struct { 58 | ModificationAmount *Amount `json:"modificationAmount"` 59 | Reference string `json:"reference"` 60 | MerchantAccount string `json:"merchantAccount"` 61 | OriginalReference string `json:"originalReference"` 62 | } 63 | 64 | // RefundResponse is a response structure for Adyen refund request 65 | type RefundResponse struct { 66 | PspReference string `json:"pspReference"` 67 | Response string `json:"response"` 68 | } 69 | 70 | /*********************** 71 | * Adjust Authorisation * 72 | ***********************/ 73 | 74 | // AdjustAuthorisation structure for adjusting previously authorised amount 75 | type AdjustAuthorisation struct { 76 | ModificationAmount *Amount `json:"modificationAmount"` 77 | Reference string `json:"reference"` 78 | MerchantAccount string `json:"merchantAccount"` 79 | OriginalReference string `json:"originalReference"` 80 | AdditionalData struct { 81 | IndustryUsage string `json:"industryUsage"` 82 | } `json:"additionalData,omitempty"` 83 | } 84 | 85 | // AdjustAuthorisationResponse is a response for AdjustAuthorisation request 86 | type AdjustAuthorisationResponse struct { 87 | PspReference string `json:"pspReference"` 88 | Response string `json:"response"` 89 | } 90 | 91 | /****************** 92 | * Techical Cancel * 93 | ******************/ 94 | 95 | // TechnicalCancel structure for performing technical cancellation 96 | // 97 | // Link - https://docs.adyen.com/developers/payment-modifications#technicalcancel 98 | type TechnicalCancel struct { 99 | MerchantAccount string `json:"merchantAccount"` 100 | OriginalMerchantReference string `json:"originalMerchantReference"` 101 | Reference string `json:"reference,omitempty"` 102 | } 103 | 104 | // TechnicalCancelResponse is a response for TechnicalCancel request 105 | type TechnicalCancelResponse struct { 106 | PspReference string `json:"pspReference"` 107 | Response string `json:"response"` 108 | } 109 | -------------------------------------------------------------------------------- /modification_gateway.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // Adyen Modification actions 4 | const ( 5 | captureType = "capture" 6 | cancelType = "cancel" 7 | cancelOrRefundType = "cancelOrRefund" 8 | refundType = "refund" 9 | adjustAuthorisation = "adjustAuthorisation" 10 | technicalCancel = "technicalCancel" 11 | ) 12 | 13 | // ModificationGateway - Adyen modification transaction logic, capture, cancel, refunds and e.t.c 14 | type ModificationGateway struct { 15 | *Adyen 16 | } 17 | 18 | // Capture - Perform capture payment in Adyen 19 | func (a *ModificationGateway) Capture(req *Capture) (*CaptureResponse, error) { 20 | url := a.adyenURL(PaymentService, captureType, PaymentAPIVersion) 21 | 22 | resp, err := a.execute(url, req) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return resp.capture() 29 | } 30 | 31 | // Cancel - Perform cancellation of the authorised transaction 32 | func (a *ModificationGateway) Cancel(req *Cancel) (*CancelResponse, error) { 33 | url := a.adyenURL(PaymentService, cancelType, PaymentAPIVersion) 34 | 35 | resp, err := a.execute(url, req) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return resp.cancel() 42 | } 43 | 44 | // CancelOrRefund - Perform cancellation for not captured transaction 45 | // otherwise perform refund action 46 | func (a *ModificationGateway) CancelOrRefund(req *Cancel) (*CancelOrRefundResponse, error) { 47 | url := a.adyenURL(PaymentService, cancelOrRefundType, PaymentAPIVersion) 48 | 49 | resp, err := a.execute(url, req) 50 | 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return resp.cancelOrRefund() 56 | } 57 | 58 | // Refund - perform refund for already captured request 59 | func (a *ModificationGateway) Refund(req *Refund) (*RefundResponse, error) { 60 | url := a.adyenURL(PaymentService, refundType, PaymentAPIVersion) 61 | 62 | resp, err := a.execute(url, req) 63 | 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return resp.refund() 69 | } 70 | 71 | // AdjustAuthorisation - perform adjustAuthorisation request to modify already authorised amount 72 | // 73 | // Link - https://docs.adyen.com/developers/payment-modifications#adjustauthorisation 74 | func (a *ModificationGateway) AdjustAuthorisation(req *AdjustAuthorisation) (*AdjustAuthorisationResponse, error) { 75 | url := a.adyenURL(PaymentService, adjustAuthorisation, PaymentAPIVersion) 76 | 77 | resp, err := a.execute(url, req) 78 | 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return resp.adjustAuthorisation() 84 | } 85 | 86 | // TechnicalCancel - perform cancellation without knowing orinal payment reference (PSP), f.e. in case of technical error 87 | // 88 | // Link - https://docs.adyen.com/developers/payment-modifications#technicalcancel 89 | func (a *ModificationGateway) TechnicalCancel(req *TechnicalCancel) (*TechnicalCancelResponse, error) { 90 | url := a.adyenURL(PaymentService, technicalCancel, PaymentAPIVersion) 91 | 92 | resp, err := a.execute(url, req) 93 | 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return resp.technicalCancel() 99 | } 100 | -------------------------------------------------------------------------------- /notification.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import "time" 4 | 5 | // NotificationRequest contains environment specification and list of notifications to process 6 | // 7 | // Link - https://docs.adyen.com/developers/api-reference/notifications-api#notificationrequest 8 | type NotificationRequest struct { 9 | Live StringBool `json:"live"` 10 | NotificationItems []NotificationRequestItem `json:"notificationItems"` 11 | } 12 | 13 | // NotificationRequestItem contains notification details 14 | // 15 | // Depending on notification type, different fields can be populated and send from Adyen 16 | // 17 | // Link - https://docs.adyen.com/developers/api-reference/notifications-api#notificationrequestitem 18 | type NotificationRequestItem struct { 19 | NotificationRequestItem NotificationRequestItemData `json:"NotificationRequestItem"` 20 | } 21 | 22 | // NotificationRequestItemData contains the NotificationRequestItem data. 23 | type NotificationRequestItemData struct { 24 | AdditionalData struct { 25 | ShopperReference string `json:"shopperReference,omitempty"` 26 | ShopperEmail string `json:"shopperEmail,omitempty"` 27 | AuthCode string `json:"authCode,omitempty"` 28 | CardSummary string `json:"cardSummary,omitempty"` 29 | ExpiryDate string `json:"expiryDate,omitempty"` 30 | AuthorisedAmountValue string `json:"authorisedAmountValue,omitempty"` 31 | AuthorisedAmountCurrency string `json:"authorisedAmountCurrency,omitempty"` 32 | HmacSignature string `json:"hmacSignature,omitempty"` 33 | NOFReasonCode string `json:"nofReasonCode,omitempty"` 34 | NOFSchemeCode string `json:"nofSchemeCode,omitempty"` 35 | RFIReasonCode string `json:"rfiReasonCode,omitempty"` 36 | RFISchemeCode string `json:"rfiSchemeCode,omitempty"` 37 | ChargebackReasonCode string `json:"chargebackReasonCode,omitempty"` 38 | ChargebackSchemeCode string `json:"chargebackSchemeCode,omitempty"` 39 | ARN string `json:"arn,omitempty"` 40 | } `json:"additionalData,omitempty"` 41 | Amount Amount `json:"amount"` 42 | PspReference string `json:"pspReference"` 43 | EventCode string `json:"eventCode"` 44 | EventDate time.Time `json:"eventDate"` // Event date in time.RFC3339 format 45 | MerchantAccountCode string `json:"merchantAccountCode"` 46 | Operations []string `json:"operations"` 47 | MerchantReference string `json:"merchantReference"` 48 | OriginalReference string `json:"originalReference,omitempty"` 49 | PaymentMethod string `json:"paymentMethod"` 50 | Reason string `json:"reason,omitempty"` 51 | Success StringBool `json:"success"` 52 | } 53 | -------------------------------------------------------------------------------- /notification_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | // TestNotificationRequest - test adyen notification JSON conversion 14 | func TestNotificationRequest(t *testing.T) { 15 | t.Parallel() 16 | 17 | responseJSON := ` 18 | { 19 | "live":"false", 20 | "notificationItems":[ 21 | { 22 | "NotificationRequestItem":{ 23 | "additionalData":{ 24 | "cardSummary":"7777", 25 | "eci":"N\/A", 26 | "shopperIP":"127.0.0.1", 27 | "totalFraudScore":"10", 28 | "expiryDate":"12\/2012", 29 | "xid":"AAE=", 30 | "billingAddress.street":"Nieuwezijds Voorburgwal", 31 | "cavvAlgorithm":"N\/A", 32 | "cardBin":"976543", 33 | "extraCostsValue":"101", 34 | "billingAddress.city":"Amsterdam", 35 | "threeDAuthenticated":"false", 36 | "alias":"H934380689410347", 37 | "paymentMethodVariant":"visa", 38 | "billingAddress.country":"NL", 39 | "fraudCheck-6-ShopperIpUsage":"10", 40 | "deviceType":"Other", 41 | " NAME1 ":"VALUE1", 42 | "authCode":"1234", 43 | "cardHolderName":"J. De Tester", 44 | "threeDOffered":"false", 45 | "billingAddress.houseNumberOrName":"21 - 5", 46 | "threeDOfferedResponse":"N\/A", 47 | "NAME2":" VALUE2 ", 48 | "billingAddress.postalCode":"1012RC", 49 | "browserCode":"Other", 50 | "cavv":"AAE=", 51 | "issuerCountry":"unknown", 52 | "threeDAuthenticatedResponse":"N\/A", 53 | "aliasType":"Default", 54 | "extraCostsCurrency":"EUR", 55 | "captureDelayHours":"120" 56 | }, 57 | "amount":{ 58 | "currency":"EUR", 59 | "value":10100 60 | }, 61 | "eventCode":"AUTHORISATION", 62 | "eventDate":"2017-12-27T14:53:06+01:00", 63 | "merchantAccountCode":"TestCOM148", 64 | "merchantReference":"8313842560770001", 65 | "operations":["CANCEL","CAPTURE","REFUND"], 66 | "paymentMethod":"visa", 67 | "pspReference":"test_AUTHORISATION_1", 68 | "reason":"1234:7777:12\/2012", 69 | "success":"true" 70 | } 71 | } 72 | ] 73 | } 74 | ` 75 | body := strings.NewReader(responseJSON) 76 | 77 | resp := &http.Response{ 78 | Status: "OK 200", 79 | StatusCode: 200, 80 | ContentLength: int64(body.Len()), 81 | Body: ioutil.NopCloser(body), 82 | } 83 | 84 | buf := new(bytes.Buffer) 85 | if _, err := buf.ReadFrom(resp.Body); err != nil { 86 | t.Error(err) 87 | } 88 | 89 | var notification NotificationRequest 90 | if err := json.Unmarshal(buf.Bytes(), ¬ification); err != nil { 91 | t.Error(err) 92 | } 93 | 94 | if notification.Live { 95 | t.Errorf("Expected notification environment should not be live, %t given", notification.Live) 96 | } 97 | 98 | if len(notification.NotificationItems) != 1 { 99 | t.Errorf("Expected to have only one notification element in a list, %d given", len(notification.NotificationItems)) 100 | } 101 | 102 | item := notification.NotificationItems[0].NotificationRequestItem 103 | 104 | if item.EventCode != "AUTHORISATION" { 105 | t.Errorf("Expected to have AUTHORISATION event code, %s given", item.EventCode) 106 | } 107 | 108 | if item.EventDate.Format(time.RFC3339) != "2017-12-27T14:53:06+01:00" { 109 | t.Errorf("Expected to have 2017-12-27T14:53:06+01:00 event date, %s given", item.EventDate.Format(time.RFC3339)) 110 | } 111 | 112 | if item.MerchantAccountCode != "TestCOM148" { 113 | t.Errorf("Expected to have TestCOM148 merchant account code, %s given", item.MerchantAccountCode) 114 | } 115 | 116 | if item.MerchantReference != "8313842560770001" { 117 | t.Errorf("Expected to have 8313842560770001 merchant reference, %s given", item.MerchantReference) 118 | } 119 | 120 | if strings.Join(item.Operations, ",") != "CANCEL,CAPTURE,REFUND" { 121 | t.Errorf("Expected to have CANCEL,CAPTURE,REFUND operations available, %s given", strings.Join(item.Operations, ",")) 122 | } 123 | 124 | if item.PaymentMethod != "visa" { 125 | t.Errorf("Expected to have visa payment, %s given", item.PaymentMethod) 126 | } 127 | 128 | if item.PspReference != "test_AUTHORISATION_1" { 129 | t.Errorf("Expected to have test_AUTHORISATION_1 as pspRegerence, %s given", item.PspReference) 130 | } 131 | 132 | if item.Reason != "1234:7777:12/2012" { 133 | t.Errorf("Expected to have 1234:7777:12/2012 reason, %s given", item.Reason) 134 | } 135 | 136 | if !item.Success { 137 | t.Errorf("Expected to have successful notification, %t given", item.Success) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /payment.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | /********** 4 | * Payment * 5 | **********/ 6 | 7 | // One-click functionality gives the shopper the option to store their payment details with the merchant, within the Adyen environment. 8 | // 9 | // In this type of transaction, the shopper needs to enter the CVC code for the transaction to get through. 10 | // 11 | // Link: https://docs.adyen.com/developers/api-reference/payments-api#recurring 12 | const ( 13 | RecurringPaymentOneClick = "ONECLICK" 14 | RecurringPaymentRecurring = "RECURRING" 15 | ShopperInteractionContAuth = "ContAuth" 16 | SelectRecurringDetailReferenceLatests = "LATEST" 17 | ) 18 | 19 | // AuthoriseEncrypted structure for Authorisation request (with encrypted card information) 20 | // 21 | // Link - https://docs.adyen.com/developers/api-reference/payments-api#paymentrequest 22 | type AuthoriseEncrypted struct { 23 | AdditionalData *AdditionalData `json:"additionalData,omitempty"` 24 | Amount *Amount `json:"amount"` 25 | BillingAddress *Address `json:"billingAddress,omitempty"` 26 | DeliveryAddress *Address `json:"deliveryAddress,omitempty"` 27 | Reference string `json:"reference"` 28 | MerchantAccount string `json:"merchantAccount"` 29 | ShopperReference string `json:"shopperReference,omitempty"` // Mandatory for recurring payment 30 | Recurring *Recurring `json:"recurring,omitempty"` 31 | ShopperEmail string `json:"shopperEmail,omitempty"` 32 | ShopperInteraction string `json:"shopperInteraction,omitempty"` 33 | ShopperIP string `json:"shopperIP,omitempty"` 34 | ShopperLocale string `json:"shopperLocale,omitempty"` 35 | ShopperName *Name `json:"shopperName,omitempty"` 36 | SelectedRecurringDetailReference string `json:"selectedRecurringDetailReference,omitempty"` 37 | BrowserInfo *BrowserInfo `json:"browserInfo,omitempty"` // Required for a 3DS process 38 | CaptureDelayHours *int `json:"captureDelayHours,omitempty"` 39 | } 40 | 41 | // Authorise structure for Authorisation request (card is not encrypted) 42 | // 43 | // Link - https://docs.adyen.com/developers/api-reference/payments-api#paymentrequest 44 | type Authorise struct { 45 | AdditionalData *AdditionalData `json:"additionalData,omitempty"` 46 | Card *Card `json:"card,omitempty"` 47 | Amount *Amount `json:"amount"` 48 | BillingAddress *Address `json:"billingAddress,omitempty"` 49 | DeliveryAddress *Address `json:"deliveryAddress,omitempty"` 50 | Reference string `json:"reference"` 51 | MerchantAccount string `json:"merchantAccount"` 52 | ShopperReference string `json:"shopperReference,omitempty"` // Mandatory for recurring payment 53 | Recurring *Recurring `json:"recurring,omitempty"` 54 | ShopperEmail string `json:"shopperEmail,omitempty"` 55 | ShopperInteraction string `json:"shopperInteraction,omitempty"` 56 | ShopperIP string `json:"shopperIP,omitempty"` 57 | ShopperLocale string `json:"shopperLocale,omitempty"` 58 | ShopperName *Name `json:"shopperName,omitempty"` 59 | SelectedRecurringDetailReference string `json:"selectedRecurringDetailReference,omitempty"` 60 | BrowserInfo *BrowserInfo `json:"browserInfo,omitempty"` // Required for a 3DS process 61 | CaptureDelayHours *int `json:"captureDelayHours,omitempty"` 62 | } 63 | 64 | // AuthoriseResponse is a response structure for Adyen 65 | // 66 | // Link - https://docs.adyen.com/developers/api-reference/payments-api#paymentresult 67 | type AuthoriseResponse struct { 68 | PspReference string `json:"pspReference"` 69 | ResultCode string `json:"resultCode"` 70 | AuthCode string `json:"authCode"` 71 | RefusalReason string `json:"refusalReason"` 72 | IssuerURL string `json:"issuerUrl"` 73 | MD string `json:"md"` 74 | PaRequest string `json:"paRequest"` 75 | FraudResult *FraudResult `json:"fraudResult,omitempty"` 76 | AdditionalData *AdditionalData `json:"additionalData,omitempty"` 77 | } 78 | 79 | // AdditionalData stores encrypted information about customer's credit card 80 | type AdditionalData struct { 81 | Content string `json:"card.encrypted.json,omitempty"` 82 | AliasType string `json:"aliasType,omitempty"` 83 | Alias string `json:"alias,omitempty"` 84 | ExpiryDate string `json:"expiryDate,omitempty"` 85 | CardBin string `json:"cardBin,omitempty"` 86 | CardSummary string `json:"cardSummary,omitempty"` 87 | CardHolderName string `json:"cardHolderName,omitempty"` 88 | PaymentMethod string `json:"paymentMethod,omitempty"` 89 | CardPaymentMethod string `json:"cardPaymentMethod,omitempty"` 90 | CardIssuingCountry string `json:"cardIssuingCountry,omitempty"` 91 | RecurringDetailReference string `json:"recurring.recurringDetailReference,omitempty"` 92 | ExecuteThreeD *StringBool `json:"executeThreeD,omitempty"` 93 | FundingSource string `json:"fundingSource,omitempty"` 94 | CustomRoutingFlag string `json:"customRoutingFlag,omitempty"` 95 | RequestedTestAcquirerResponseCode int `json:"RequestedTestAcquirerResponseCode,omitempty"` //Used for trigger error from adyen 96 | CVCResult CVCResult `json:"cvcResult,omitempty"` 97 | CVCResultRaw string `json:"cvcResultRaw,omitempty"` 98 | AVSResult AVSResponse `json:"avsResult,omitempty"` 99 | AVSResultRaw string `json:"avsResultRaw,omitempty"` 100 | } 101 | 102 | // BrowserInfo hold information on the user browser 103 | type BrowserInfo struct { 104 | AcceptHeader string `json:"acceptHeader"` 105 | UserAgent string `json:"userAgent"` 106 | } 107 | 108 | // Recurring hold the behavior for a future payment : could be ONECLICK or RECURRING 109 | type Recurring struct { 110 | Contract string `json:"contract"` 111 | } 112 | 113 | // FraudResult hold the fraud score of transaction 114 | type FraudResult struct { 115 | AccountScore int64 `json:"accountScore,omitempty"` 116 | Results []Result `json:"results,omitempty"` 117 | } 118 | 119 | // Result hold the fraud score detail 120 | type Result struct { 121 | FraudCheckResult *FraudCheckResult `json:"FraudCheckResult,omitempty"` 122 | } 123 | 124 | // FraudCheckResult hold information of fraud score detail 125 | type FraudCheckResult struct { 126 | AccountScore int `json:"accountScore,omitempty"` 127 | CheckID int `json:"checkId,omitempty"` 128 | Name string `json:"name,omitempty"` 129 | } 130 | 131 | /************* 132 | * Payment 3D * 133 | *************/ 134 | 135 | // Authorise3D structure for Authorisation request (card is not encrypted) 136 | // 137 | // https://docs.adyen.com/developers/api-reference/payments-api#paymentrequest3d 138 | type Authorise3D struct { 139 | BillingAddress *Address `json:"billingAddress,omitempty"` 140 | DeliveryAddress *Address `json:"deliveryAddress,omitempty"` 141 | MD string `json:"md"` 142 | MerchantAccount string `json:"merchantAccount"` 143 | BrowserInfo *BrowserInfo `json:"browserInfo"` 144 | PaResponse string `json:"paResponse"` 145 | ShopperEmail string `json:"shopperEmail,omitempty"` 146 | ShopperIP string `json:"shopperIP,omitempty"` 147 | ShopperLocale string `json:"shopperLocale,omitempty"` 148 | ShopperName *Name `json:"shopperName,omitempty"` 149 | } 150 | 151 | /******************* 152 | * Directory lookup * 153 | *******************/ 154 | 155 | // DirectoryLookupRequest - get list of available payment methods based on skin, country and order details 156 | // 157 | // Description - https://docs.adyen.com/developers/api-reference/hosted-payment-pages-api#directoryrequest 158 | // CountryCode could be used to test local payment methods, if client's IP is from different country 159 | type DirectoryLookupRequest struct { 160 | CurrencyCode string `url:"currencyCode"` 161 | MerchantAccount string `url:"merchantAccount"` 162 | PaymentAmount int `url:"paymentAmount"` 163 | SkinCode string `url:"skinCode"` 164 | MerchantReference string `url:"merchantReference"` 165 | SessionsValidity string `url:"sessionValidity"` 166 | MerchantSig string `url:"merchantSig"` 167 | CountryCode string `url:"countryCode"` 168 | ShipBeforeDate string `url:"shipBeforeDate"` 169 | } 170 | 171 | // DirectoryLookupResponse - api response for DirectoryLookupRequest 172 | // 173 | // Description - https://docs.adyen.com/developers/api-reference/hosted-payment-pages-api#directoryresponse 174 | type DirectoryLookupResponse struct { 175 | PaymentMethods []PaymentMethod `json:"paymentMethods"` 176 | } 177 | 178 | // PaymentMethod - structure for single payment method in directory look up response 179 | // 180 | // Part of DirectoryLookupResponse 181 | type PaymentMethod struct { 182 | BrandCode string `json:"brandCode"` 183 | Name string `json:"name"` 184 | Logos logos `json:"logos"` 185 | Issuers []issuer `json:"issuers"` 186 | } 187 | 188 | // logos - payment method logos 189 | // 190 | // Part of DirectoryLookupResponse 191 | type logos struct { 192 | Normal string `json:"normal"` 193 | Small string `json:"small"` 194 | Tiny string `json:"tiny"` 195 | } 196 | 197 | // issuer - bank issuer type 198 | // 199 | // Part of DirectoryLookupResponse 200 | type issuer struct { 201 | IssuerID string `json:"issuerId"` 202 | Name string `json:"name"` 203 | } 204 | 205 | /*********** 206 | * Skip HPP * 207 | ***********/ 208 | 209 | // SkipHppRequest contains data that would be used to create Adyen HPP redirect URL 210 | // 211 | // Link: https://docs.adyen.com/developers/ecommerce-integration/local-payment-methods 212 | // 213 | // Request description: https://docs.adyen.com/developers/api-reference/hosted-payment-pages-api#skipdetailsrequest 214 | type SkipHppRequest struct { 215 | MerchantReference string `url:"merchantReference"` 216 | PaymentAmount int `url:"paymentAmount"` 217 | CurrencyCode string `url:"currencyCode"` 218 | ShipBeforeDate string `url:"shipBeforeDate"` 219 | SkinCode string `url:"skinCode"` 220 | MerchantAccount string `url:"merchantAccount"` 221 | ShopperLocale string `url:"shopperLocale"` 222 | SessionsValidity string `url:"sessionValidity"` 223 | MerchantSig string `url:"merchantSig"` 224 | CountryCode string `url:"countryCode"` 225 | BrandCode string `url:"brandCode"` 226 | IssuerID string `url:"issuerId"` 227 | } 228 | -------------------------------------------------------------------------------- /payment_gateway.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import "github.com/google/go-querystring/query" 4 | 5 | // PaymentGateway - Adyen payment transaction logic 6 | type PaymentGateway struct { 7 | *Adyen 8 | } 9 | 10 | // authoriseType - authorise type request, @TODO: move to enums 11 | const authoriseType = "authorise" 12 | 13 | // directoryLookupURL - version 2 url for Directory Lookup request 14 | const directoryLookupURL = "directory/v2" 15 | 16 | // skipHppUrl - SkipDetails request endpoint 17 | const skipHppURL = "skipDetails" 18 | 19 | // authorise3DType - authorise type request, @TODO: move to enums 20 | const authorise3DType = "authorise3d" 21 | 22 | // AuthoriseEncrypted - Perform authorise payment in Adyen 23 | // 24 | // To perform recurring payment, AuthoriseEncrypted need to have contract specified and shopperReference 25 | // 26 | // Example: 27 | // &adyen.AuthoriseEncrypted{ 28 | // Amount: &adyen.Amount{Value: "2000", Currency: "EUR"}, 29 | // MerchantAccount: "merchant-account", 30 | // AdditionalData: &adyen.AdditionalData{Content: r.Form.Get("adyen-encrypted-data")}, // encrypted CC data 31 | // ShopperReference: "unique-customer-reference", 32 | // Recurring: &adyen.Recurring{Contract:adyen.RecurringPaymentRecurring} 33 | // Reference: "some-merchant-reference", 34 | // } 35 | //} 36 | // adyen.Recurring{Contract:adyen.RecurringPaymentRecurring} as one of the contracts 37 | func (a *PaymentGateway) AuthoriseEncrypted(req *AuthoriseEncrypted) (*AuthoriseResponse, error) { 38 | url := a.adyenURL(PaymentService, authoriseType, PaymentAPIVersion) 39 | 40 | resp, err := a.execute(url, req) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return resp.authorize() 47 | } 48 | 49 | // Authorise - Perform authorise payment in Adyen 50 | // 51 | // Used to perform authorisation transaction without credit card data encrypted 52 | // 53 | // NOTE: Due to PCI compliance, it's not recommended to send credit card data to server 54 | // 55 | // Please use AuthoriseEncrypted instead and adyen frontend encryption library 56 | func (a *PaymentGateway) Authorise(req *Authorise) (*AuthoriseResponse, error) { 57 | url := a.adyenURL(PaymentService, authoriseType, PaymentAPIVersion) 58 | 59 | resp, err := a.execute(url, req) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return resp.authorize() 66 | } 67 | 68 | // DirectoryLookup - Execute directory lookup request 69 | // 70 | // Link - https://docs.adyen.com/developers/api-reference/hosted-payment-pages-api 71 | func (a *PaymentGateway) DirectoryLookup(req *DirectoryLookupRequest) (*DirectoryLookupResponse, error) { 72 | 73 | // Calculate HMAC signature to request 74 | err := req.CalculateSignature(a.Adyen) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | url := a.createHPPUrl(directoryLookupURL) 80 | 81 | v, _ := query.Values(req) 82 | url = url + "?" + v.Encode() 83 | 84 | resp, err := a.executeHpp(url, req) 85 | 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return resp.directoryLookup() 91 | } 92 | 93 | // GetHPPRedirectURL - Generates link, so customer could be redirected 94 | // to perform Hosted Payment Page payments 95 | // 96 | // Link - https://docs.adyen.com/developers/api-reference/hosted-payment-pages-api 97 | func (a *PaymentGateway) GetHPPRedirectURL(req *SkipHppRequest) (string, error) { 98 | // Calculate HMAC signature to request 99 | if err := req.CalculateSignature(a.Adyen); err != nil { 100 | return "", err 101 | } 102 | 103 | url := a.createHPPUrl(skipHppURL) 104 | 105 | v, _ := query.Values(req) 106 | url = url + "?" + v.Encode() 107 | 108 | return url, nil 109 | } 110 | 111 | // Authorise3D - Perform authorise payment in Adyen 112 | func (a *PaymentGateway) Authorise3D(req *Authorise3D) (*AuthoriseResponse, error) { 113 | url := a.adyenURL(PaymentService, authorise3DType, PaymentAPIVersion) 114 | 115 | resp, err := a.execute(url, req) 116 | 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return resp.authorize() 122 | } 123 | -------------------------------------------------------------------------------- /payment_gateway_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // TestAuthoriseFailed 12 | func TestAuthoriseFailed(t *testing.T) { 13 | t.Parallel() 14 | 15 | instance := getTestInstance() 16 | 17 | authRequest := &Authorise{ 18 | Card: &Card{ 19 | Number: "4111111111111111", 20 | ExpireMonth: "08", 21 | ExpireYear: "2018", 22 | Cvc: "737", 23 | HolderName: "John Smith", 24 | }, 25 | Amount: &Amount{ 26 | Value: 1000, 27 | Currency: "EUR", 28 | }, 29 | Reference: "", 30 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 31 | } 32 | 33 | _, err := instance.Payment().Authorise(authRequest) 34 | if err == nil { 35 | t.Error("Request should fail, due to missing reference error") 36 | } 37 | 38 | if !strings.Contains(err.Error(), "Reference Missing") { 39 | t.Errorf("Response should contain missing reference error, response - %s", err.Error()) 40 | } 41 | } 42 | 43 | // TestAuthorise 44 | // 45 | // In order to have test running correctly, account should be configured to have full API permissions 46 | // Otherwise, Adyen API will return "not allowed" error. Please check https://github.com/Adyen/adyen-php-api-library/issues/20 47 | func TestAuthorise(t *testing.T) { 48 | t.Parallel() 49 | 50 | instance := getTestInstance() 51 | 52 | authRequest := &Authorise{ 53 | Card: &Card{ 54 | Number: "4111111111111111", 55 | ExpireMonth: "08", 56 | ExpireYear: "2018", 57 | Cvc: "737", 58 | HolderName: "John Smith", 59 | }, 60 | Amount: &Amount{ 61 | Value: 1000, 62 | Currency: "EUR", 63 | }, 64 | Reference: "DE-TEST-1" + randomString(10), 65 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 66 | } 67 | 68 | response, err := instance.Payment().Authorise(authRequest) 69 | 70 | knownError, ok := err.(APIError) 71 | if ok { 72 | t.Errorf("Response should be succesfull. Known API Error: Code - %s, Message - %s, Type - %s", knownError.ErrorCode, knownError.Message, knownError.ErrorType) 73 | } 74 | 75 | if err != nil { 76 | t.Errorf("Response should be succesfull, error - %s", err.Error()) 77 | } 78 | 79 | responseBytes, err := json.Marshal(response) 80 | 81 | if err != nil { 82 | t.Error("Response can't be converted to JSON") 83 | } 84 | 85 | if response.PspReference == "" { 86 | t.Errorf("Response should contain PSP Reference. Response - %s", string(responseBytes)) 87 | } 88 | 89 | if response.ResultCode != "Authorised" { 90 | t.Errorf("Response resultCode should be Authorised, Response - %s", string(responseBytes)) 91 | } 92 | } 93 | 94 | // TestDirectoryLookUpMissingData - DirectoryLookUp Request failing due to missing data 95 | func TestDirectoryLookUpMissingData(t *testing.T) { 96 | t.Parallel() 97 | 98 | instance := getTestInstance() 99 | 100 | timeIn := time.Now().Local().Add(time.Minute * time.Duration(60)) 101 | 102 | directoryRequest := &DirectoryLookupRequest{ 103 | CurrencyCode: "EUR", 104 | PaymentAmount: 1000, 105 | MerchantReference: "DE-TEST-1" + randomString(10), 106 | SessionsValidity: timeIn.Format(time.RFC3339), 107 | CountryCode: "NL", 108 | } 109 | 110 | _, err := instance.Payment().DirectoryLookup(directoryRequest) 111 | if err == nil { 112 | t.Error("Request should fail due to missing request data") 113 | } 114 | 115 | if err.Error() != "merchantID, skinCode and HMAC hash need to be specified" { 116 | t.Errorf("Error should indicate that request is missing configuration data, error - %s", err) 117 | } 118 | } 119 | 120 | // TestDirectoryLookUp - test directory lookup v2 integration 121 | // 122 | // In order to have test running correctly, Adyen Skin need to be configured and passed through environment variable 123 | func TestDirectoryLookUp(t *testing.T) { 124 | t.Parallel() 125 | 126 | instance := getTestInstanceWithHPP() 127 | 128 | timeIn := time.Now().Local().Add(time.Minute * time.Duration(60)) 129 | 130 | directoryRequest := &DirectoryLookupRequest{ 131 | CurrencyCode: "EUR", 132 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 133 | PaymentAmount: 1000, 134 | SkinCode: os.Getenv("ADYEN_SKINCODE"), 135 | MerchantReference: "DE-TEST-1" + randomString(10), 136 | SessionsValidity: timeIn.Format(time.RFC3339), 137 | CountryCode: "NL", 138 | } 139 | 140 | response, err := instance.Payment().DirectoryLookup(directoryRequest) 141 | 142 | if err != nil { 143 | t.Errorf("DirectoryLookup response should be successful, error - %s", err) 144 | } 145 | 146 | if len(response.PaymentMethods) == 0 { 147 | t.Errorf("DirectoryLookup response should contain at least one payment method available, response - %s", response) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /recurring.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | /************ 4 | * Recurring * 5 | ************/ 6 | 7 | // RecurringDetailsRequest structure to list all recurring payment associated to a shopperReference 8 | // 9 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#recurringdetailsrequest 10 | type RecurringDetailsRequest struct { 11 | MerchantAccount string `json:"merchantAccount"` 12 | ShopperReference string `json:"shopperReference,omitempty"` 13 | // Not mandatory 14 | Recurring *Recurring `json:"recurring,omitempty"` 15 | } 16 | 17 | // RecurringDetailsResult structure to hold the RecurringDetails 18 | // 19 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#recurringdetailsresult 20 | type RecurringDetailsResult struct { 21 | CreationDate string `json:"creationDate"` 22 | Details []struct { 23 | RecurringDetail RecurringDetail `json:"RecurringDetail"` 24 | } `json:"details"` 25 | InvalidOneclickContracts string `json:"invalidOneclickContracts"` 26 | ShopperReference string `json:"shopperReference"` 27 | } 28 | 29 | // RecurringDetail structure to hold information associated to a recurring payment 30 | // 31 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#recurringdetail 32 | type RecurringDetail struct { 33 | Acquirer string `json:"acquirer"` 34 | AcquirerAccount string `json:"acquirerAccount"` 35 | AdditionalData struct { 36 | CardBin string `json:"cardBin"` 37 | } `json:"additionalData"` 38 | Alias string `json:"alias"` 39 | AliasType string `json:"aliasType"` 40 | Card Card `json:"card,omitempty"` 41 | ContractTypes []string `json:"contractTypes"` 42 | CreationDate string `json:"creationDate"` 43 | FirstPspReference string `json:"firstPspReference"` 44 | PaymentMethodVariant string `json:"paymentMethodVariant"` 45 | RecurringDetailReference string `json:"recurringDetailReference"` 46 | Variant string `json:"variant"` 47 | } 48 | 49 | // RecurringDisableRequest structure to hold information regarding disable recurring request 50 | // 51 | // If `RecurringDetailReference` is specified, specific payment ID will be disabled 52 | // otherwise all customer saved payment methods will be disabled 53 | // 54 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#disablerequest 55 | type RecurringDisableRequest struct { 56 | MerchantAccount string `json:"merchantAccount"` 57 | ShopperReference string `json:"shopperReference"` 58 | // Type of a contract ONECLICK, RECURRING, PAYOUT or combination of them 59 | Contract string `json:"contract,omitempty"` 60 | // ID of a customer saved payment method, all will be disabled if none is specified 61 | RecurringDetailReference string `json:"recurringDetailReference,omitempty"` 62 | } 63 | 64 | // RecurringDisableResponse structure to hold response for disable recurring request 65 | // 66 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#disableresult 67 | type RecurringDisableResponse struct { 68 | Response string `json:"response"` 69 | } 70 | -------------------------------------------------------------------------------- /recurring_gateway.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | // RecurringGateway - Adyen recurring transaction logic 4 | type RecurringGateway struct { 5 | *Adyen 6 | } 7 | 8 | const ( 9 | // listRecurringDetailsType - listRecurringDetails type request, @TODO: move to enums 10 | listRecurringDetailsType = "listRecurringDetails" 11 | // disableRecurringType - disable recurring type request, @TODO: move to enums 12 | disableRecurringType = "disable" 13 | ) 14 | 15 | // ListRecurringDetails - Get list of recurring payments in Adyen 16 | func (a *RecurringGateway) ListRecurringDetails(req *RecurringDetailsRequest) (*RecurringDetailsResult, error) { 17 | url := a.adyenURL(RecurringService, listRecurringDetailsType, RecurringAPIVersion) 18 | 19 | resp, err := a.execute(url, req) 20 | 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return resp.listRecurringDetails() 26 | } 27 | 28 | // DisableRecurring - disable customer's saved payment method based on a contract type or/and payment method ID 29 | func (a *RecurringGateway) DisableRecurring(req *RecurringDisableRequest) (*RecurringDisableResponse, error) { 30 | url := a.adyenURL(RecurringService, disableRecurringType, RecurringAPIVersion) 31 | 32 | resp, err := a.execute(url, req) 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return resp.disableRecurring() 39 | } 40 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // APIError - handle error (non 200 status) response from Adyen 10 | type APIError struct { 11 | ErrorType string `json:"errorType"` 12 | ErrorCode string `json:"errorCode"` 13 | Message string `json:"message"` 14 | Status int32 `json:"status"` 15 | } 16 | 17 | // Response - Adyen API response structure 18 | type Response struct { 19 | *http.Response 20 | Body []byte 21 | } 22 | 23 | // handleHTTPError - handle non 200 response from Adyen and create Error response instance 24 | func (r *Response) handleHTTPError() error { 25 | var aerr APIError 26 | if err := json.Unmarshal(r.Body, &aerr); err != nil { 27 | return err 28 | } 29 | 30 | if aerr.Status >= http.StatusBadRequest { 31 | return aerr 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // Error - error interface for ApiError 38 | func (e APIError) Error() string { 39 | return fmt.Sprintf("[%s][%d]: (%s) %s", e.ErrorType, e.Status, e.ErrorCode, e.Message) 40 | } 41 | 42 | // authorize - generate Adyen Authorize API Response 43 | func (r *Response) authorize() (*AuthoriseResponse, error) { 44 | var a AuthoriseResponse 45 | if err := json.Unmarshal(r.Body, &a); err != nil { 46 | return nil, err 47 | } 48 | 49 | return &a, nil 50 | } 51 | 52 | // capture - generate Adyen Capture API Response 53 | func (r *Response) capture() (*CaptureResponse, error) { 54 | var a CaptureResponse 55 | if err := json.Unmarshal(r.Body, &a); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &a, nil 60 | } 61 | 62 | // cancel - generate Adyen Cancel API Response 63 | func (r *Response) cancel() (*CancelResponse, error) { 64 | var a CancelResponse 65 | if err := json.Unmarshal(r.Body, &a); err != nil { 66 | return nil, err 67 | } 68 | 69 | return &a, nil 70 | } 71 | 72 | // cancelOrRefund - generate Adyen CancelOrRefund API Response 73 | func (r *Response) cancelOrRefund() (*CancelOrRefundResponse, error) { 74 | var a CancelOrRefundResponse 75 | if err := json.Unmarshal(r.Body, &a); err != nil { 76 | return nil, err 77 | } 78 | 79 | return &a, nil 80 | } 81 | 82 | // refund - generate Adyen Refund API Response 83 | func (r *Response) refund() (*RefundResponse, error) { 84 | var a RefundResponse 85 | if err := json.Unmarshal(r.Body, &a); err != nil { 86 | return nil, err 87 | } 88 | 89 | return &a, nil 90 | } 91 | 92 | // adjustAuthorisation - generate Adyen Refund API Response 93 | func (r *Response) adjustAuthorisation() (*AdjustAuthorisationResponse, error) { 94 | var a AdjustAuthorisationResponse 95 | if err := json.Unmarshal(r.Body, &a); err != nil { 96 | return nil, err 97 | } 98 | 99 | return &a, nil 100 | } 101 | 102 | // technicalCancel - generate Adyen Technical Cancel API Response 103 | func (r *Response) technicalCancel() (*TechnicalCancelResponse, error) { 104 | var a TechnicalCancelResponse 105 | if err := json.Unmarshal(r.Body, &a); err != nil { 106 | return nil, err 107 | } 108 | 109 | return &a, nil 110 | } 111 | 112 | // directoryLookup - generate Adyen Directory Lookup response 113 | func (r *Response) directoryLookup() (*DirectoryLookupResponse, error) { 114 | var a DirectoryLookupResponse 115 | if err := json.Unmarshal(r.Body, &a); err != nil { 116 | return nil, err 117 | } 118 | 119 | return &a, nil 120 | } 121 | 122 | // listRecurringDetails - generate Adyen List Recurring Details response 123 | func (r *Response) listRecurringDetails() (*RecurringDetailsResult, error) { 124 | var a RecurringDetailsResult 125 | if err := json.Unmarshal(r.Body, &a); err != nil { 126 | return nil, err 127 | } 128 | 129 | return &a, nil 130 | } 131 | 132 | // disableRecurring - generate Adyen disable recurring 133 | // 134 | // Link - https://docs.adyen.com/developers/api-reference/recurring-api#disableresult 135 | func (r *Response) disableRecurring() (*RecurringDisableResponse, error) { 136 | var a RecurringDisableResponse 137 | if err := json.Unmarshal(r.Body, &a); err != nil { 138 | return nil, err 139 | } 140 | 141 | return &a, nil 142 | } 143 | 144 | // paymentMethods - generate Adyen CheckoutAPI paymentMethods response. 145 | func (r *Response) paymentMethods() (*PaymentMethodsResponse, error) { 146 | var a PaymentMethodsResponse 147 | if err := json.Unmarshal(r.Body, &a); err != nil { 148 | return nil, err 149 | } 150 | 151 | return &a, nil 152 | } 153 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestResponseErrorResponseStatus - response is valid JSON, but status is > 299, error should be returned 8 | func TestResponseErrorResponseStatus(t *testing.T) { 9 | t.Parallel() 10 | 11 | responseJSON := ` 12 | { 13 | "errorType" : "authorise", 14 | "errorCode" : "501", 15 | "message" : "sample error", 16 | "status" : 501 17 | } 18 | ` 19 | providerResponse, err := createTestResponse(responseJSON, "OK 200", 200) 20 | 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | err = providerResponse.handleHTTPError() 26 | if err == nil { 27 | t.Fatal("Response should raise an error - got nil") 28 | } 29 | 30 | errorContent := "[authorise][501]: (501) sample error" 31 | if err.Error() != errorContent { 32 | t.Fatal("Expected error message ", errorContent, " got ", err.Error()) 33 | } 34 | } 35 | 36 | // TestResponseNotValidJson - response is an empty script, error should be returned 37 | func TestResponseNotValidJson(t *testing.T) { 38 | t.Parallel() 39 | 40 | providerResponse, err := createTestResponse("", "503", 503) 41 | 42 | err = providerResponse.handleHTTPError() 43 | if err == nil { 44 | t.Fatal("Response should raise an error - got nil") 45 | } 46 | } 47 | 48 | func TestAuthorizeResponse(t *testing.T) { 49 | cases := []struct { 50 | name string 51 | input string 52 | reference string 53 | resultCode string 54 | authCode string 55 | expErr bool 56 | }{ 57 | { 58 | name: "authorize response", 59 | input: `{ 60 | "pspReference" : "8413547924770610", 61 | "ResultCode" : "Authorised", 62 | "AuthCode" : "53187" 63 | }`, 64 | reference: "8413547924770610", 65 | resultCode: "Authorised", 66 | authCode: "53187", 67 | }, 68 | { 69 | name: "authorize returns errors", 70 | input: "some error string", 71 | reference: "", 72 | resultCode: "", 73 | authCode: "", 74 | expErr: true, 75 | }, 76 | } 77 | 78 | for _, c := range cases { 79 | t.Run(c.name, func(t *testing.T) { 80 | response, err := createTestResponse(c.input, "OK 200", 200) 81 | 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | res, err := response.authorize() 87 | 88 | if c.expErr { 89 | if err == nil { 90 | t.Fatal("expected error but didn't get one") 91 | } 92 | 93 | return 94 | } 95 | 96 | equals(t, c.reference, res.PspReference) 97 | equals(t, c.resultCode, res.ResultCode) 98 | equals(t, c.authCode, res.AuthCode) 99 | }) 100 | } 101 | } 102 | 103 | func TestCaptureResponse(t *testing.T) { 104 | cases := []struct { 105 | name string 106 | input string 107 | reference string 108 | response string 109 | expErr bool 110 | }{ 111 | { 112 | name: "capture response", 113 | input: `{ 114 | "pspReference" : "8413547924770610", 115 | "response" : "[capture-received]" 116 | }`, 117 | reference: "8413547924770610", 118 | response: "[capture-received]", 119 | }, 120 | { 121 | name: "capture returns errors", 122 | input: "some error string", 123 | reference: "", 124 | response: "", 125 | expErr: true, 126 | }, 127 | } 128 | 129 | for _, c := range cases { 130 | t.Run(c.name, func(t *testing.T) { 131 | response, err := createTestResponse(c.input, "OK 200", 200) 132 | 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | res, err := response.capture() 138 | 139 | if c.expErr { 140 | if err == nil { 141 | t.Fatal("expected error but didn't get one") 142 | } 143 | 144 | return 145 | } 146 | 147 | equals(t, c.reference, res.PspReference) 148 | equals(t, c.response, res.Response) 149 | }) 150 | } 151 | } 152 | 153 | func TestCancelResponse(t *testing.T) { 154 | cases := []struct { 155 | name string 156 | input string 157 | reference string 158 | response string 159 | expErr bool 160 | }{ 161 | { 162 | name: "cancel response", 163 | input: `{ 164 | "pspReference" : "8413547924770610", 165 | "response" : "[cancel-received]" 166 | }`, 167 | reference: "8413547924770610", 168 | response: "[cancel-received]", 169 | }, 170 | { 171 | name: "cancel returns errors", 172 | input: "some error string", 173 | reference: "", 174 | response: "", 175 | expErr: true, 176 | }, 177 | } 178 | 179 | for _, c := range cases { 180 | t.Run(c.name, func(t *testing.T) { 181 | response, err := createTestResponse(c.input, "OK 200", 200) 182 | 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | res, err := response.cancel() 188 | 189 | if c.expErr { 190 | if err == nil { 191 | t.Fatal("expected error but didn't get one") 192 | } 193 | 194 | return 195 | } 196 | 197 | equals(t, c.reference, res.PspReference) 198 | equals(t, c.response, res.Response) 199 | }) 200 | } 201 | } 202 | 203 | func TestCancelOrRefundResponse(t *testing.T) { 204 | cases := []struct { 205 | name string 206 | input string 207 | reference string 208 | response string 209 | expErr bool 210 | }{ 211 | { 212 | name: "cancelOrRefund response", 213 | input: `{ 214 | "pspReference" : "8413547924770610", 215 | "response" : "[cancelOrRefund-received]" 216 | }`, 217 | reference: "8413547924770610", 218 | response: "[cancelOrRefund-received]", 219 | }, 220 | { 221 | name: "cancelOrRefund returns errors", 222 | input: "some error string", 223 | reference: "", 224 | response: "", 225 | expErr: true, 226 | }, 227 | } 228 | 229 | for _, c := range cases { 230 | t.Run(c.name, func(t *testing.T) { 231 | response, err := createTestResponse(c.input, "OK 200", 200) 232 | 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | res, err := response.cancelOrRefund() 238 | 239 | if c.expErr { 240 | if err == nil { 241 | t.Fatal("expected error but didn't get one") 242 | } 243 | 244 | return 245 | } 246 | 247 | equals(t, c.reference, res.PspReference) 248 | equals(t, c.response, res.Response) 249 | }) 250 | } 251 | } 252 | 253 | func TestRefundResponse(t *testing.T) { 254 | cases := []struct { 255 | name string 256 | input string 257 | reference string 258 | response string 259 | expErr bool 260 | }{ 261 | { 262 | name: "refund response", 263 | input: `{ 264 | "pspReference" : "8413547924770610", 265 | "response" : "[refund-received]" 266 | }`, 267 | reference: "8413547924770610", 268 | response: "[refund-received]", 269 | }, 270 | { 271 | name: "refund returns errors", 272 | input: "some error string", 273 | reference: "", 274 | response: "", 275 | expErr: true, 276 | }, 277 | } 278 | 279 | for _, c := range cases { 280 | t.Run(c.name, func(t *testing.T) { 281 | response, err := createTestResponse(c.input, "OK 200", 200) 282 | 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | 287 | res, err := response.refund() 288 | 289 | if c.expErr { 290 | if err == nil { 291 | t.Fatal("expected error but didn't get one") 292 | } 293 | 294 | return 295 | } 296 | 297 | equals(t, c.reference, res.PspReference) 298 | equals(t, c.response, res.Response) 299 | }) 300 | } 301 | } 302 | 303 | func TestListRecurringDetailsResponse(t *testing.T) { 304 | cases := []struct { 305 | name string 306 | input string 307 | shopperReference string 308 | recurringDetailReference string 309 | cartHolderName string 310 | expErr bool 311 | }{ 312 | { 313 | name: "listRecurringDetails response", 314 | input: `{ 315 | "creationDate": "2018-05-23T15:25:40+02:00", 316 | "details": [ 317 | { 318 | "RecurringDetail": { 319 | "additionalData": { 320 | "cardBin": "411111" 321 | }, 322 | "alias": "K333136193308394", 323 | "aliasType": "Default", 324 | "card": { 325 | "expiryMonth": "8", 326 | "expiryYear": "2018", 327 | "holderName": "John Smith", 328 | "number": "1111" 329 | }, 330 | "contractTypes": [ 331 | "PAYOUT", 332 | "RECURRING", 333 | "ONECLICK" 334 | ], 335 | "creationDate": "2018-08-10T10:28:43+02:00", 336 | "firstPspReference": "8815338897222637", 337 | "paymentMethodVariant": "visa", 338 | "recurringDetailReference": "8415336862463792", 339 | "variant": "visa" 340 | } 341 | } 342 | ], 343 | "shopperReference": "yourShopperId_IOfW3k9G2PvXFu2j" 344 | }`, 345 | shopperReference: "yourShopperId_IOfW3k9G2PvXFu2j", 346 | recurringDetailReference: "8415336862463792", 347 | cartHolderName: "John Smith", 348 | }, 349 | { 350 | name: "listRecurringDetails error response", 351 | input: "some error string", 352 | shopperReference: "", 353 | recurringDetailReference: "", 354 | cartHolderName: "", 355 | expErr: true, 356 | }, 357 | } 358 | 359 | for _, c := range cases { 360 | t.Run(c.name, func(t *testing.T) { 361 | response, err := createTestResponse(c.input, "OK 200", 200) 362 | 363 | if err != nil { 364 | t.Fatal(err) 365 | } 366 | 367 | res, err := response.listRecurringDetails() 368 | 369 | if c.expErr { 370 | if err == nil { 371 | t.Fatal("expected error but didn't get one") 372 | } 373 | 374 | return 375 | } 376 | 377 | equals(t, c.shopperReference, res.ShopperReference) 378 | equals(t, 1, len(res.Details)) 379 | equals(t, c.recurringDetailReference, res.Details[0].RecurringDetail.RecurringDetailReference) 380 | equals(t, c.cartHolderName, res.Details[0].RecurringDetail.Card.HolderName) 381 | }) 382 | } 383 | } 384 | 385 | func TestDisableRecurringResponse(t *testing.T) { 386 | cases := []struct { 387 | name string 388 | input string 389 | response string 390 | expErr bool 391 | }{ 392 | { 393 | name: "disableRecurring response", 394 | input: `{ 395 | "response" : "[detail-successfully-disabled]" 396 | }`, 397 | response: "[detail-successfully-disabled]", 398 | }, 399 | { 400 | name: "disableRecurring returns errors", 401 | input: "some error string", 402 | response: "", 403 | expErr: true, 404 | }, 405 | } 406 | 407 | for _, c := range cases { 408 | t.Run(c.name, func(t *testing.T) { 409 | response, err := createTestResponse(c.input, "OK 200", 200) 410 | 411 | if err != nil { 412 | t.Fatal(err) 413 | } 414 | 415 | res, err := response.disableRecurring() 416 | 417 | if c.expErr { 418 | if err == nil { 419 | t.Fatal("expected error but didn't get one") 420 | } 421 | 422 | return 423 | } 424 | 425 | equals(t, c.response, res.Response) 426 | }) 427 | } 428 | } 429 | 430 | func TestAdjustAuthorisationResponse(t *testing.T) { 431 | cases := []struct { 432 | name string 433 | input string 434 | reference string 435 | response string 436 | expErr bool 437 | }{ 438 | { 439 | name: "adjustAuthorisation response", 440 | input: `{ 441 | "pspReference" : "8413547924770610", 442 | "response" : "[adjustAuthorisation-received]" 443 | }`, 444 | reference: "8413547924770610", 445 | response: "[adjustAuthorisation-received]", 446 | }, 447 | { 448 | name: "adjustAuthorisation returns errors", 449 | input: "some error string", 450 | reference: "", 451 | response: "", 452 | expErr: true, 453 | }, 454 | } 455 | 456 | for _, c := range cases { 457 | t.Run(c.name, func(t *testing.T) { 458 | response, err := createTestResponse(c.input, "OK 200", 200) 459 | 460 | if err != nil { 461 | t.Fatal(err) 462 | } 463 | 464 | res, err := response.adjustAuthorisation() 465 | 466 | if c.expErr { 467 | if err == nil { 468 | t.Fatal("expected error but didn't get one") 469 | } 470 | 471 | return 472 | } 473 | 474 | equals(t, c.reference, res.PspReference) 475 | equals(t, c.response, res.Response) 476 | }) 477 | } 478 | } 479 | 480 | func TestTechnicalCancelResponse(t *testing.T) { 481 | cases := []struct { 482 | name string 483 | input string 484 | reference string 485 | response string 486 | expErr bool 487 | }{ 488 | { 489 | name: "technicalCancel response", 490 | input: `{ 491 | "pspReference" : "8413547924770610", 492 | "response" : "[technical-cancel-received]" 493 | }`, 494 | reference: "8413547924770610", 495 | response: "[technical-cancel-received]", 496 | }, 497 | { 498 | name: "technicalCancel returns errors", 499 | input: "some error string", 500 | reference: "", 501 | response: "", 502 | expErr: true, 503 | }, 504 | } 505 | 506 | for _, c := range cases { 507 | t.Run(c.name, func(t *testing.T) { 508 | response, err := createTestResponse(c.input, "OK 200", 200) 509 | 510 | if err != nil { 511 | t.Fatal(err) 512 | } 513 | 514 | res, err := response.technicalCancel() 515 | 516 | if c.expErr { 517 | if err == nil { 518 | t.Fatal("expected error but didn't get one") 519 | } 520 | 521 | return 522 | } 523 | 524 | equals(t, c.reference, res.PspReference) 525 | equals(t, c.response, res.Response) 526 | }) 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /signature.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "errors" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // replaceSpecialChars replace special characters according to Adyen documentation 14 | // 15 | // Link: https://docs.adyen.com/developers/payments/accepting-payments/hmac-signature-calculation 16 | func replaceSpecialChars(value string) string { 17 | temp := strings.Replace(value, "\\", "\\\\", -1) 18 | temp = strings.Replace(temp, ":", "\\:", -1) 19 | 20 | return temp 21 | } 22 | 23 | // CalculateSignature calculate HMAC signature for request 24 | // 25 | // Link: https://docs.adyen.com/developers/payments/accepting-payments/hmac-signature-calculation 26 | func (r *DirectoryLookupRequest) CalculateSignature(adyen *Adyen) error { 27 | if r.MerchantAccount == "" || r.SkinCode == "" || adyen.Credentials.Hmac == "" { 28 | return errors.New("merchantID, skinCode and HMAC hash need to be specified") 29 | } 30 | 31 | keyString := strings.Join([]string{ 32 | "countryCode", 33 | "currencyCode", 34 | "merchantAccount", 35 | "merchantReference", 36 | "paymentAmount", 37 | "sessionValidity", 38 | "shipBeforeDate", 39 | "skinCode", 40 | }, ":") 41 | 42 | valueString := strings.Join([]string{ 43 | replaceSpecialChars(r.CountryCode), 44 | replaceSpecialChars(r.CurrencyCode), 45 | replaceSpecialChars(r.MerchantAccount), 46 | replaceSpecialChars(r.MerchantReference), 47 | replaceSpecialChars(strconv.Itoa(r.PaymentAmount)), 48 | replaceSpecialChars(r.SessionsValidity), 49 | replaceSpecialChars(r.ShipBeforeDate), 50 | replaceSpecialChars(r.SkinCode), 51 | }, ":") 52 | 53 | fullString := keyString + ":" + valueString 54 | 55 | src, err := hex.DecodeString(adyen.Credentials.Hmac) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | mac := hmac.New(sha256.New, src) 61 | if _, err = mac.Write([]byte(fullString)); err != nil { 62 | return err 63 | } 64 | 65 | r.MerchantSig = base64.StdEncoding.EncodeToString(mac.Sum(nil)) 66 | return nil 67 | } 68 | 69 | // CalculateSignature calculate HMAC signature for request 70 | // 71 | // Link: https://docs.adyen.com/developers/payments/accepting-payments/hmac-signature-calculation 72 | func (r *SkipHppRequest) CalculateSignature(adyen *Adyen) error { 73 | if r.MerchantAccount == "" || r.SkinCode == "" || adyen.Credentials.Hmac == "" { 74 | return errors.New("merchantID, skinCode and HMAC hash need to be specified") 75 | } 76 | 77 | keyString := strings.Join([]string{ 78 | "brandCode", 79 | "countryCode", 80 | "currencyCode", 81 | "issuerId", 82 | "merchantAccount", 83 | "merchantReference", 84 | "paymentAmount", 85 | "sessionValidity", 86 | "shipBeforeDate", 87 | "shopperLocale", 88 | "skinCode", 89 | }, ":") 90 | 91 | valueString := strings.Join([]string{ 92 | replaceSpecialChars(r.BrandCode), 93 | replaceSpecialChars(r.CountryCode), 94 | replaceSpecialChars(r.CurrencyCode), 95 | replaceSpecialChars(r.IssuerID), 96 | replaceSpecialChars(r.MerchantAccount), 97 | replaceSpecialChars(r.MerchantReference), 98 | replaceSpecialChars(strconv.Itoa(r.PaymentAmount)), 99 | replaceSpecialChars(r.SessionsValidity), 100 | replaceSpecialChars(r.ShipBeforeDate), 101 | replaceSpecialChars(r.ShopperLocale), 102 | replaceSpecialChars(r.SkinCode), 103 | }, ":") 104 | 105 | fullString := keyString + ":" + valueString 106 | 107 | src, err := hex.DecodeString(adyen.Credentials.Hmac) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | mac := hmac.New(sha256.New, src) 113 | if _, err = mac.Write([]byte(fullString)); err != nil { 114 | return err 115 | } 116 | 117 | r.MerchantSig = base64.StdEncoding.EncodeToString(mac.Sum(nil)) 118 | return nil 119 | } 120 | 121 | // ValidateSignature validate HMAC signature for notification event 122 | // 123 | // Link: https://docs.adyen.com/development-resources/notifications/verify-hmac-signatures#verify-using-your-own-solution 124 | func (n *NotificationRequestItemData) ValidateSignature(adyen *Adyen) (bool, error) { 125 | var precondition error 126 | providedSig := n.AdditionalData.HmacSignature 127 | if len(providedSig) == 0 { 128 | precondition = errors.New("no HMAC signature in message") 129 | } else if len(adyen.Credentials.Hmac) == 0 { 130 | precondition = errors.New("no HMAC key configured; cannot validate signature") 131 | } 132 | if precondition != nil { 133 | return false, precondition 134 | } 135 | 136 | valueInJavaFloatToStringStyle := strconv.FormatFloat(float64(n.Amount.Value), 'f', -1, 32) 137 | valueString := strings.Join([]string{ 138 | replaceSpecialChars(n.PspReference), 139 | replaceSpecialChars(n.OriginalReference), 140 | replaceSpecialChars(n.MerchantAccountCode), 141 | replaceSpecialChars(n.MerchantReference), 142 | valueInJavaFloatToStringStyle, 143 | replaceSpecialChars(n.Amount.Currency), 144 | replaceSpecialChars(n.EventCode), 145 | strconv.FormatBool(bool(n.Success)), 146 | }, ":") 147 | 148 | src, err := hex.DecodeString(adyen.Credentials.Hmac) 149 | if err != nil { 150 | return false, err 151 | } 152 | 153 | mac := hmac.New(sha256.New, src) 154 | if _, err = mac.Write([]byte(valueString)); err != nil { 155 | return false, err 156 | } 157 | expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil)) 158 | return expectedSig == providedSig, nil 159 | } 160 | -------------------------------------------------------------------------------- /signature_test.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "testing" 8 | 9 | "github.com/google/go-querystring/query" 10 | ) 11 | 12 | func TestSignatureCalculateSignature(t *testing.T) { 13 | t.Parallel() 14 | 15 | instance := getTestInstanceWithHPP() 16 | 17 | req := DirectoryLookupRequest{ 18 | CurrencyCode: "EUR", 19 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 20 | ShipBeforeDate: "2015-11-31T13:42:40+1:00", 21 | PaymentAmount: 1000, 22 | SkinCode: os.Getenv("ADYEN_SKINCODE"), 23 | MerchantReference: "DE-100100GMWJGS", 24 | SessionsValidity: "2015-11-29T13:42:40+1:00", 25 | } 26 | 27 | err := req.CalculateSignature(instance) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | v, _ := query.Values(req) 33 | 34 | // there is no automated way to verify full URL, cause adyen webpage require authenication first 35 | // to debug request signature, print URL params, login to https://ca-test.adyen.com and follow full link below 36 | url := "https://ca-test.adyen.com/ca/ca/skin/checkhmac.shtml" + "?" + v.Encode() 37 | 38 | if _, err = http.NewRequest(http.MethodGet, url, nil); err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestSignatureCalculateSignatureForSkipHppRequest(t *testing.T) { 44 | t.Parallel() 45 | 46 | instance := getTestInstanceWithHPP() 47 | 48 | req := SkipHppRequest{ 49 | MerchantReference: "DE-100100GMWJGS", 50 | PaymentAmount: 1000, 51 | CurrencyCode: instance.Currency, 52 | ShipBeforeDate: "2015-11-31T13:42:40+1:00", 53 | SkinCode: os.Getenv("ADYEN_SKINCODE"), 54 | MerchantAccount: os.Getenv("ADYEN_ACCOUNT"), 55 | ShopperLocale: "en_GB", 56 | SessionsValidity: "2015-11-29T13:42:40+1:00", 57 | CountryCode: "NL", 58 | BrandCode: "ideal", 59 | } 60 | 61 | err := req.CalculateSignature(instance) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | v, _ := query.Values(req) 67 | 68 | // there is no automated way to verify full URL, cause adyen webpage require authenication first 69 | // to debug request signature, print URL params, login to https://ca-test.adyen.com and follow full link below 70 | url := "https://ca-test.adyen.com/ca/ca/skin/checkhmac.shtml" + "?" + v.Encode() 71 | 72 | if _, err = http.NewRequest(http.MethodGet, url, nil); err != nil { 73 | t.Fatal(err) 74 | } 75 | } 76 | 77 | func TestSignatureNotification(t *testing.T) { 78 | t.Parallel() 79 | 80 | //ref: https://github.com/Adyen/adyen-ruby-api-library/blob/53d9a03ab09d58927ec34e65d3d2acc1c5dc1ea7/spec/utils/hmac_validator_spec.rb 81 | itemDataJSON := ` 82 | { 83 | "additionalData": { 84 | "authCode": "1234", 85 | "cardSummary": "7777" 86 | }, 87 | "amount": { 88 | "currency": "EUR", 89 | "value": 1130 90 | }, 91 | "eventCode": "AUTHORISATION", 92 | "eventDate": "2020-01-01T10:00:00+05:00", 93 | "merchantAccountCode": "TestMerchant", 94 | "merchantReference": "TestPayment-1407325143704", 95 | "operations": ["CANCEL", "CAPTURE", "REFUND"], 96 | "paymentMethod": "visa", 97 | "pspReference": "7914073381342284", 98 | "reason": "1234:7777:12\/2012", 99 | "success": "true" 100 | } 101 | ` 102 | 103 | validSignature := "coqCmt/IZ4E3CzPvMY8zTjQVL5hYJUiBRg8UU+iCWo0=" 104 | cases := []struct { 105 | name string 106 | signatureInMessage string 107 | hmacKey string 108 | exp bool 109 | expErr bool 110 | }{ 111 | { 112 | name: "no sig", 113 | signatureInMessage: "", 114 | hmacKey: "", 115 | exp: false, 116 | expErr: true, 117 | }, 118 | { 119 | name: "sig, no key", 120 | signatureInMessage: validSignature, 121 | hmacKey: "", 122 | exp: false, 123 | expErr: true, 124 | }, 125 | { 126 | name: "sig, wrong key", 127 | signatureInMessage: validSignature, 128 | hmacKey: "DFB1EB5485895CFA84146406857104ABB4CBCABDC8AAF103A624C8F6A3EAAB00", 129 | exp: false, 130 | expErr: false, 131 | }, 132 | { 133 | name: "sig, correct key", 134 | signatureInMessage: validSignature, 135 | hmacKey: "44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056", 136 | exp: true, 137 | expErr: false, 138 | }, 139 | { 140 | name: "sig, malformed key", 141 | signatureInMessage: validSignature, 142 | hmacKey: "invalid_hmac", 143 | exp: false, 144 | expErr: true, 145 | }, 146 | } 147 | 148 | for _, c := range cases { 149 | t.Run(c.name, func(t *testing.T) { 150 | var itemData NotificationRequestItemData 151 | err := json.Unmarshal([]byte(itemDataJSON), &itemData) 152 | if err != nil { 153 | t.Fatalf("unmarshal error: %v", err) 154 | } 155 | if c.hmacKey != "" { 156 | itemData.AdditionalData.HmacSignature = c.signatureInMessage 157 | } 158 | config := NewWithHMAC(Testing, "username", "fake_password", c.hmacKey) 159 | res, err := itemData.ValidateSignature(config) 160 | if (err != nil) != c.expErr { 161 | t.Fatalf("expected error?: %t, actual error: %v", c.expErr, err) 162 | } 163 | if res != c.exp { 164 | t.Fatalf("expected result: %t, actual result: %t", c.exp, res) 165 | } 166 | }) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package adyen 2 | 3 | const ( 4 | // Male to indicate "male" gender 5 | Male = "MALE" 6 | // Female to indicate "female" gender 7 | Female = "FEMALE" 8 | // Unknown to indicate "unknown" gender 9 | Unknown = "UNKNOWN" 10 | ) 11 | 12 | /********** 13 | * Address * 14 | **********/ 15 | 16 | // Address - base address type for customer billing and delivery addresses 17 | // 18 | // Link - https://docs.adyen.com/developers/api-reference/common-api#address 19 | type Address struct { 20 | City string `json:"city"` 21 | Country string `json:"country"` 22 | HouseNumberOrName string `json:"houseNumberOrName"` 23 | PostalCode string `json:"postalCode,omitempty"` 24 | StateOrProvince string `json:"stateOrProvince,omitempty"` 25 | Street string `json:"street"` 26 | } 27 | 28 | /******* 29 | * Card * 30 | *******/ 31 | 32 | // Card structure representation 33 | type Card struct { 34 | Number string `json:"number"` 35 | ExpireMonth string `json:"expiryMonth"` 36 | ExpireYear string `json:"expiryYear"` 37 | Cvc string `json:"cvc"` 38 | HolderName string `json:"holderName"` 39 | } 40 | 41 | /******* 42 | * Name * 43 | *******/ 44 | 45 | // Name - generic name structure 46 | // 47 | // Link - https://docs.adyen.com/developers/api-reference/common-api#name 48 | type Name struct { 49 | FirstName string `json:"firstName"` 50 | Gender string `json:"gender"` // Should be ENUM (Male, Female, Unknown) from a constants 51 | Infix string `json:"infix,omitempty"` 52 | LastName string `json:"lastName"` 53 | } 54 | --------------------------------------------------------------------------------