├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── go.mod └── v1 ├── account.go ├── account_test.go ├── card.go ├── card_test.go ├── charge.go ├── charge_test.go ├── client.go ├── client_test.go ├── customer.go ├── customer_test.go ├── doc.go ├── event.go ├── event_test.go ├── examples ├── account │ └── main.go ├── charge │ └── main.go ├── plan │ └── main.go ├── subscription │ └── main.go └── transfer │ └── main.go ├── payjperror.go ├── plan.go ├── plan_test.go ├── subscription.go ├── subscription_test.go ├── token.go ├── token_test.go ├── transfer.go ├── transfer_test.go ├── util.go └── util_test.go /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build Test 2 | 3 | on: push 4 | 5 | jobs: 6 | build-test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go: [ '1.16', '1.15', '1.14', '1.13', '1.12', '1.11', '1.10', '1.9' ] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go }} 18 | # - name: Install Lint 19 | # run: go get -u golang.org/x/lint/golint 20 | # - name: Execute Lint 21 | # run: golint ./v1 22 | - name: Execute Test 23 | run: go test -v ./v1 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Yoshiki Shibukawa 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-present BASE, Inc. (http://binc.jp/) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAY.JP for Go 2 | 3 | [![Build Status](https://github.com/payjp/payjp-go/actions/workflows/build-test.yml/badge.svg?branch=master)](https://github.com/payjp/payjp-go/actions) 4 | 5 | ## Notice 6 | 7 | 現在の最新のコードは `beta` ブランチに移行しています。 8 | 新規にご利用の際は下位互換の為残してある `master` ではなくそちらをご利用ください。 9 | このブランチのバージョンはv0.0.1となっており、[最新のリリースバージョン](https://github.com/payjp/payjp-go/releases)および `beta` ブランチと大きく差分があります。 10 | 11 | 12 | ## Installation 13 | 14 | go get github.com/payjp/payjp-go/v1 15 | 16 | ## Documentation 17 | 18 | Please see our official [documentation](http://pay.jp/docs/api). 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/payjp/payjp-go 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /v1/account.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // AccountService はあなたのアカウント情報を返します。 11 | type AccountService struct { 12 | service *Service 13 | } 14 | 15 | func newAccountService(service *Service) *AccountService { 16 | return &AccountService{service} 17 | } 18 | 19 | // Retrieve account object. あなたのアカウント情報を取得します。 20 | func (t *AccountService) Retrieve() (*AccountResponse, error) { 21 | request, err := http.NewRequest("GET", t.service.apiBase+"/accounts", nil) 22 | if err != nil { 23 | return nil, err 24 | } 25 | request.Header.Add("Authorization", t.service.apiKey) 26 | 27 | resp, err := t.service.Client.Do(request) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer resp.Body.Close() 32 | body, err := ioutil.ReadAll(resp.Body) 33 | result := &AccountResponse{} 34 | err = json.Unmarshal(body, result) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return result, nil 39 | } 40 | 41 | // AccountResponse はAccount.Retrieve()メソッドが返す構造体です 42 | type AccountResponse struct { 43 | ID string // acct_で始まる一意なオブジェクトを示す文字列 44 | Email string // メールアドレス 45 | CreatedAt time.Time // このアカウント作成時のタイムスタンプ 46 | Merchant struct { 47 | ID string // acct_mch_で始まる一意なオブジェクトを示す文字列 48 | BankEnabled bool // 入金先銀行口座情報が設定済みかどうか 49 | BrandsAccepted []string // 本番環境で利用可能なカードブランドのリスト 50 | CurrenciesSupported []string // 対応通貨のリスト 51 | DefaultCurrency string // 3文字のISOコード(現状 “jpy” のみサポート) 52 | BusinessType string // 業務形態 53 | ContactPhone string // 電話番号 54 | Country string // 所在国 55 | ChargeType []string // 支払い方法種別のリスト 56 | ProductDetail string // 販売商品の詳細 57 | ProductName string // 販売商品名 58 | ProductType []string // 販売商品の種類リスト 59 | DetailsSubmitted bool // 本番環境申請情報が提出済みかどうか 60 | LiveModeEnabled bool // 本番環境が有効かどうか 61 | LiveModeActivatedAt time.Time // 本番環境が許可された日時のタイムスタンプ 62 | SitePublished bool // 申請対象のサイトがオープン済みかどうか 63 | 64 | URL string // 申請対象サイトのURL 65 | CreatedAt time.Time // 作成時のタイムスタンプ 66 | } // マーチャントアカウントの詳細情報 67 | TeamID string // アカウントに紐付くチームID 68 | } 69 | 70 | type accountResponseParser struct { 71 | CreatedEpoch int `json:"created"` 72 | Email string `json:"email"` 73 | ID string `json:"id"` 74 | Merchant struct { 75 | BankEnabled bool `json:"bank_enabled"` 76 | BrandsAccepted []string `json:"brands_accepted"` 77 | BusinessType string `json:"business_type"` 78 | ChargeType []string `json:"charge_type"` 79 | ContactPhone string `json:"contact_phone"` 80 | Country string `json:"country"` 81 | CreatedEpoch int `json:"created"` 82 | CurrenciesSupported []string `json:"currencies_supported"` 83 | DefaultCurrency string `json:"default_currency"` 84 | DetailsSubmitted bool `json:"details_submitted"` 85 | ID string `json:"id"` 86 | LiveModeActivatedEpoch int `json:"livemode_activated_at"` 87 | LiveModeEnabled bool `json:"livemode_enabled"` 88 | Object string `json:"object"` 89 | ProductDetail string `json:"product_detail"` 90 | ProductName string `json:"product_name"` 91 | ProductType []string `json:"product_type"` 92 | SitePublished bool `json:"site_published"` 93 | URL string `json:"url"` 94 | } `json:"merchant"` 95 | Object string `json:"object"` 96 | TeamID string `json:"team_id"` 97 | } 98 | 99 | // UnmarshalJSON はJSONパース用の内部APIです。 100 | func (a *AccountResponse) UnmarshalJSON(b []byte) error { 101 | raw := accountResponseParser{} 102 | err := json.Unmarshal(b, &raw) 103 | if err == nil && raw.Object == "account" { 104 | a.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 105 | a.Email = raw.Email 106 | a.ID = raw.ID 107 | m := &a.Merchant 108 | rm := &raw.Merchant 109 | m.BankEnabled = rm.BankEnabled 110 | m.BrandsAccepted = rm.BrandsAccepted 111 | m.BusinessType = rm.BusinessType 112 | m.ChargeType = rm.ChargeType 113 | m.ContactPhone = rm.ContactPhone 114 | m.Country = rm.Country 115 | m.CreatedAt = time.Unix(int64(rm.CreatedEpoch), 0) 116 | m.CurrenciesSupported = rm.CurrenciesSupported 117 | m.DefaultCurrency = rm.DefaultCurrency 118 | m.DetailsSubmitted = rm.DetailsSubmitted 119 | m.ID = rm.ID 120 | m.LiveModeActivatedAt = time.Unix(int64(rm.LiveModeActivatedEpoch), 0) 121 | m.LiveModeEnabled = rm.LiveModeEnabled 122 | m.ProductDetail = rm.ProductDetail 123 | m.ProductName = rm.ProductName 124 | m.ProductType = rm.ProductType 125 | m.SitePublished = rm.SitePublished 126 | m.URL = rm.URL 127 | a.TeamID = raw.TeamID 128 | return nil 129 | } 130 | rawError := errorResponse{} 131 | err = json.Unmarshal(b, &rawError) 132 | if err == nil && rawError.Error.Status != 0 { 133 | return &rawError.Error 134 | } 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /v1/account_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | var accountResponseJSON = []byte(` 9 | { 10 | "created": 1439706600, 11 | "email": "liveaccount@mail.com", 12 | "id": "acct_8a27db83a7bf11a0c12b0c2833f", 13 | "merchant": { 14 | "bank_enabled": false, 15 | "brands_accepted": [ 16 | "Visa", 17 | "MasterCard", 18 | "JCB", 19 | "American Express", 20 | "Diners Club", 21 | "Discover" 22 | ], 23 | "business_type": null, 24 | "charge_type": null, 25 | "contact_phone": null, 26 | "country": "JP", 27 | "created": 1439706600, 28 | "currencies_supported": [ 29 | "jpy" 30 | ], 31 | "default_currency": "jpy", 32 | "details_submitted": false, 33 | "id": "acct_mch_21a96cb898ceb6db0932983", 34 | "livemode_activated_at": 0, 35 | "livemode_enabled": false, 36 | "object": "merchant", 37 | "product_detail": null, 38 | "product_name": null, 39 | "product_type": null, 40 | "site_published": null, 41 | "url": null 42 | }, 43 | "object": "account", 44 | "team_id": "example-team-id" 45 | } 46 | `) 47 | 48 | func TestParseAccountResponseJSON(t *testing.T) { 49 | account := &AccountResponse{} 50 | err := json.Unmarshal(accountResponseJSON, account) 51 | 52 | if err != nil { 53 | t.Errorf("err should be nil, but %v", err) 54 | } 55 | if account.ID != "acct_8a27db83a7bf11a0c12b0c2833f" { 56 | t.Errorf("customer.ID should be 'acct_8a27db83a7bf11a0c12b0c2833f', but '%s'", account.ID) 57 | } 58 | if account.Merchant.DefaultCurrency != "jpy" { 59 | t.Errorf("defaultCurrency should be 'jpy', but %s", account.Merchant.DefaultCurrency) 60 | } 61 | if account.TeamID != "example-team-id" { 62 | t.Errorf("account.TeamID should be 'example-team-id', but %s", account.TeamID) 63 | } 64 | } 65 | 66 | func TestAccountRetrieve(t *testing.T) { 67 | mock, transport := NewMockClient(200, accountResponseJSON) 68 | service := New("api-key", mock) 69 | account, err := service.Account.Retrieve() 70 | if transport.URL != "https://api.pay.jp/v1/accounts" { 71 | t.Errorf("URL is wrong: %s", transport.URL) 72 | } 73 | if transport.Method != "GET" { 74 | t.Errorf("Method should be GET, but %s", transport.Method) 75 | } 76 | if err != nil { 77 | t.Errorf("err should be nil, but %v", err) 78 | return 79 | } else if account == nil { 80 | t.Error("plan should not be nil") 81 | } else if account.Email != "liveaccount@mail.com" { 82 | t.Errorf("parse error: account.Email should be 'liveaccount@mail.com', but %s.", account.Email) 83 | } else if account.TeamID != "example-team-id" { 84 | t.Errorf("parse error: account.TeamID should be 'example-team-id', but %s.", account.TeamID) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /v1/card.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | // Card はCustomerやTokenのAPIでカード情報を設定する時に使う構造体です 10 | type Card struct { 11 | Name interface{} // カード保有者名(e.g. YUI ARAGAKI) 12 | Number interface{} // カード番号 13 | ExpMonth interface{} // 有効期限月 14 | ExpYear interface{} // 有効期限年 15 | CVC interface{} // CVCコード 16 | Country interface{} // 2桁のISOコード(e.g. JP) 17 | AddressZip interface{} // 郵便番号 18 | AddressState interface{} // 都道府県 19 | AddressCity interface{} // 市区町村 20 | AddressLine1 interface{} // 番地など 21 | AddressLine2 interface{} // 建物名など 22 | Metadata map[string]string // メタデータ 23 | } 24 | 25 | func (c Card) valid() bool { 26 | _, ok := c.Number.(string) 27 | return ok 28 | } 29 | 30 | func parseCard(service *Service, body []byte, result *CardResponse, customerID string) (*CardResponse, error) { 31 | err := json.Unmarshal(body, result) 32 | if err != nil { 33 | return nil, err 34 | } 35 | result.service = service 36 | result.customerID = customerID 37 | return result, nil 38 | } 39 | 40 | // CardResponse はCustomerやTokenのAPIが返す構造体です 41 | type CardResponse struct { 42 | CreatedAt time.Time // カード作成時のタイムスタンプ 43 | ID string // car_で始まる一意なオブジェクトを示す文字列 44 | Name string // カード保有者名(e.g. YUI ARAGAKI) 45 | Last4 string // カード番号の下四桁 46 | ExpMonth int // 有効期限月 47 | ExpYear int // 有効期限年 48 | Brand string // カードブランド名(e.g. Visa) 49 | CvcCheck string // CVCコードチェックの結果 50 | Fingerprint string // このクレジットカード番号に紐づけられた一意(他と重複しない)キー 51 | Country string // 2桁のISOコード(e.g. JP) 52 | AddressZip string // 郵便番号 53 | AddressZipCheck string // 郵便番号存在チェックの結果 54 | AddressState string // 都道府県 55 | AddressCity string // 市区町村 56 | AddressLine1 string // 番地など 57 | AddressLine2 string // 建物名など 58 | Metadata map[string]string // メタデータ 59 | 60 | customerID string 61 | service *Service 62 | } 63 | 64 | type cardResponseParser struct { 65 | AddressCity string `json:"address_city"` 66 | AddressLine1 string `json:"address_line1"` 67 | AddressLine2 string `json:"address_line2"` 68 | AddressState string `json:"address_state"` 69 | AddressZip string `json:"address_zip"` 70 | AddressZipCheck string `json:"address_zip_check"` 71 | Brand string `json:"brand"` 72 | Country string `json:"country"` 73 | CreatedEpoch int `json:"created"` 74 | CvcCheck string `json:"cvc_check"` 75 | ExpMonth int `json:"exp_month"` 76 | ExpYear int `json:"exp_year"` 77 | Fingerprint string `json:"fingerprint"` 78 | ID string `json:"id"` 79 | Last4 string `json:"last4"` 80 | Name string `json:"name"` 81 | Object string `json:"object"` 82 | Metadata map[string]string `json:"metadata"` 83 | } 84 | 85 | // Update メソッドはカードの内容を更新します 86 | // Customer情報から得られるカードでしか更新はできません 87 | func (c *CardResponse) Update(card Card) error { 88 | if c.customerID == "" { 89 | return errors.New("Token's card doens't support Update()") 90 | } 91 | _, err := c.service.Customer.postCard(c.customerID, "/"+c.ID, card, c) 92 | return err 93 | } 94 | 95 | // Delete メソッドは顧客に登録されているカードを削除します 96 | // Customer情報から得られるカードでしか削除はできません 97 | func (c *CardResponse) Delete() error { 98 | if c.customerID == "" { 99 | return errors.New("Token's card doens't support Delete()") 100 | } 101 | return c.service.delete("/customers/" + c.customerID + "/cards/" + c.ID) 102 | } 103 | 104 | // UnmarshalJSON はJSONパース用の内部APIです。 105 | func (c *CardResponse) UnmarshalJSON(b []byte) error { 106 | raw := cardResponseParser{} 107 | err := json.Unmarshal(b, &raw) 108 | if err == nil && raw.Object == "card" { 109 | c.AddressCity = raw.AddressCity 110 | c.AddressLine1 = raw.AddressLine1 111 | c.AddressLine2 = raw.AddressLine2 112 | c.AddressState = raw.AddressState 113 | c.AddressZip = raw.AddressZip 114 | c.AddressZipCheck = raw.AddressZipCheck 115 | c.Brand = raw.Brand 116 | c.Country = raw.Country 117 | c.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 118 | c.CvcCheck = raw.CvcCheck 119 | c.ExpMonth = raw.ExpMonth 120 | c.ExpYear = raw.ExpYear 121 | c.Fingerprint = raw.Fingerprint 122 | c.ID = raw.ID 123 | c.Last4 = raw.Last4 124 | c.Name = raw.Name 125 | c.Metadata = raw.Metadata 126 | return nil 127 | } 128 | rawError := errorResponse{} 129 | err = json.Unmarshal(b, &rawError) 130 | if err == nil && rawError.Error.Status != 0 { 131 | return &rawError.Error 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /v1/card_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var cardResponseJSON = []byte(` 10 | { 11 | "address_city": null, 12 | "address_line1": null, 13 | "address_line2": null, 14 | "address_state": null, 15 | "address_zip": null, 16 | "address_zip_check": "unchecked", 17 | "brand": "Visa", 18 | "country": null, 19 | "created": 1433127983, 20 | "cvc_check": "unchecked", 21 | "exp_month": 2, 22 | "exp_year": 2020, 23 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 24 | "id": "car_f7d9fa98594dc7c2e42bfcd641ff", 25 | "last4": "4242", 26 | "livemode": false, 27 | "name": null, 28 | "object": "card" 29 | } 30 | `) 31 | 32 | var cardListResponseJSON = []byte(` 33 | { 34 | "count": 1, 35 | "data": [ 36 | { 37 | "address_city": null, 38 | "address_line1": null, 39 | "address_line2": null, 40 | "address_state": null, 41 | "address_zip": null, 42 | "address_zip_check": "unchecked", 43 | "brand": "Visa", 44 | "country": null, 45 | "created": 1433127983, 46 | "cvc_check": "unchecked", 47 | "exp_month": 2, 48 | "exp_year": 2020, 49 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 50 | "id": "car_f7d9fa98594dc7c2e42bfcd641ff", 51 | "last4": "4242", 52 | "livemode": false, 53 | "name": null, 54 | "object": "card" 55 | } 56 | ], 57 | "object": "list", 58 | "has_more": true, 59 | "url": "/v1/customers/cus_4df4b5ed720933f4fb9e28857517/cards" 60 | } 61 | `) 62 | 63 | var cardDeleteResponseJSON = []byte(` 64 | { 65 | "deleted": true, 66 | "id": "car_f7d9fa98594dc7c2e42bfcd641ff", 67 | "livemode": false 68 | } 69 | `) 70 | 71 | var cardErrorResponseJSON = []byte(` 72 | { 73 | "error": { 74 | "message": "There is no card with ID: dummy", 75 | "param": "id", 76 | "status": 404, 77 | "type": "client_error" 78 | } 79 | } 80 | `) 81 | 82 | func TestParseCardResponseJSON(t *testing.T) { 83 | card := &CardResponse{} 84 | json.Unmarshal(cardResponseJSON, card) 85 | 86 | if card.ID != "car_f7d9fa98594dc7c2e42bfcd641ff" { 87 | t.Errorf("card.Id should be 'car_f7d9fa98594dc7c2e42bfcd641ff', but '%s'", card.ID) 88 | } 89 | createdAt := card.CreatedAt.UTC().Format("2006-01-02 15:04:05") 90 | if createdAt != "2015-06-01 03:06:23" { 91 | t.Errorf("card.CreatedAt() should be '2015-06-01 03:06:23' but '%s'", createdAt) 92 | } 93 | } 94 | 95 | func TestCustomerAddCard(t *testing.T) { 96 | mock, transport := NewMockClient(200, cardResponseJSON) 97 | transport.AddResponse(200, cardResponseJSON) 98 | service := New("api-key", mock) 99 | card, err := service.Customer.AddCard("cus_121673955bd7aa144de5a8f6c262", Card{ 100 | Number: "4242424242424242", 101 | ExpMonth: 2, 102 | ExpYear: 2020, 103 | }) 104 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards" { 105 | t.Errorf("URL is wrong: %s", transport.URL) 106 | } 107 | if transport.Method != "POST" { 108 | t.Errorf("Method should be POST, but %s", transport.Method) 109 | } 110 | if err != nil { 111 | t.Errorf("err should be nil, but %v", err) 112 | return 113 | } 114 | if card == nil { 115 | t.Error("card should not be nil") 116 | } else if card.Last4 != "4242" { 117 | t.Errorf("card.Last4 should be '4242', but %s.", card.Last4) 118 | } 119 | } 120 | 121 | func TestCustomerAddCard2(t *testing.T) { 122 | mock, transport := NewMockClient(200, customerResponseJSON) 123 | transport.AddResponse(200, cardResponseJSON) 124 | service := New("api-key", mock) 125 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 126 | if customer == nil { 127 | t.Error("plan should not be nil") 128 | return 129 | } 130 | card, err := customer.AddCard(Card{ 131 | Number: "4242424242424242", 132 | ExpMonth: "2", 133 | ExpYear: "2020", 134 | CVC: "000", 135 | }) 136 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards" { 137 | t.Errorf("URL is wrong: %s", transport.URL) 138 | } 139 | if transport.Method != "POST" { 140 | t.Errorf("Method should be POST, but %s", transport.Method) 141 | } 142 | if err != nil { 143 | t.Errorf("err should be nil, but %v", err) 144 | return 145 | } 146 | if card == nil { 147 | t.Error("card should not be nil") 148 | } else if card.Last4 != "4242" { 149 | t.Errorf("card.Last4 should be '4242', but %s.", card.Last4) 150 | } 151 | } 152 | 153 | func TestCustomerGetCard(t *testing.T) { 154 | mock, transport := NewMockClient(200, cardResponseJSON) 155 | service := New("api-key", mock) 156 | card, err := service.Customer.GetCard("cus_121673955bd7aa144de5a8f6c262", "car_f7d9fa98594dc7c2e42bfcd641ff") 157 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 158 | t.Errorf("URL is wrong: %s", transport.URL) 159 | } 160 | if transport.Method != "GET" { 161 | t.Errorf("Method should be GET, but %s", transport.Method) 162 | } 163 | if err != nil { 164 | t.Errorf("err should be nil, but %v", err) 165 | return 166 | } else if card == nil { 167 | t.Error("card should not be nil") 168 | } else if card.Last4 != "4242" { 169 | t.Errorf("parse error: card.Last4 should be 4242, but %s.", card.Last4) 170 | } 171 | } 172 | 173 | func TestCustomerGetCard2(t *testing.T) { 174 | mock, transport := NewMockClient(200, customerResponseJSON) 175 | transport.AddResponse(200, cardResponseJSON) 176 | service := New("api-key", mock) 177 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 178 | if customer == nil { 179 | t.Error("plan should not be nil") 180 | return 181 | } 182 | card, err := customer.GetCard("car_f7d9fa98594dc7c2e42bfcd641ff") 183 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 184 | t.Errorf("URL is wrong: %s", transport.URL) 185 | } 186 | if transport.Method != "GET" { 187 | t.Errorf("Method should be GET, but %s", transport.Method) 188 | } 189 | if err != nil { 190 | t.Errorf("err should be nil, but %v", err) 191 | return 192 | } else if card == nil { 193 | t.Error("card should not be nil") 194 | } else if card.Last4 != "4242" { 195 | t.Errorf("parse error: card.Last4 should be 4242, but %s.", card.Last4) 196 | } 197 | } 198 | 199 | func TestCustomerUpdateCard(t *testing.T) { 200 | mock, transport := NewMockClient(200, cardResponseJSON) 201 | service := New("api-key", mock) 202 | card, err := service.Customer.UpdateCard("cus_121673955bd7aa144de5a8f6c262", "car_f7d9fa98594dc7c2e42bfcd641ff", Card{ 203 | Number: "4242424242424242", 204 | ExpMonth: 2, 205 | ExpYear: 2020, 206 | }) 207 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 208 | t.Errorf("URL is wrong: %s", transport.URL) 209 | } 210 | if transport.Method != "POST" { 211 | t.Errorf("Method should be POST, but %s", transport.Method) 212 | } 213 | if err != nil { 214 | t.Errorf("err should be nil, but %v", err) 215 | return 216 | } 217 | if card == nil { 218 | t.Error("card should not be nil") 219 | } else if card.Last4 != "4242" { 220 | t.Errorf("parse error: card.Last4 should be 4242, but %s.", card.Last4) 221 | } 222 | } 223 | 224 | func TestCustomerUpdateCard2(t *testing.T) { 225 | mock, transport := NewMockClient(200, customerResponseJSON) 226 | transport.AddResponse(200, cardResponseJSON) 227 | service := New("api-key", mock) 228 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 229 | if customer == nil { 230 | t.Error("plan should not be nil") 231 | return 232 | } 233 | err = customer.Cards[0].Update(Card{ 234 | Number: "4242424242424242", 235 | ExpMonth: "2", 236 | ExpYear: "2020", 237 | CVC: "000", 238 | }) 239 | if err != nil { 240 | t.Errorf("err should be nil, but %v", err) 241 | } 242 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 243 | t.Errorf("URL is wrong: %s", transport.URL) 244 | } 245 | if transport.Method != "POST" { 246 | t.Errorf("Method should be POST, but %s", transport.Method) 247 | } 248 | } 249 | 250 | func TestCustomerUpdateCardError(t *testing.T) { 251 | mock, _ := NewMockClient(200, cardErrorResponseJSON) 252 | service := New("api-key", mock) 253 | card, err := service.Customer.UpdateCard("cus_121673955bd7aa144de5a8f6c262", "car_f7d9fa98594dc7c2e42bfcd641ff", Card{ 254 | Number: "4242424242424242", 255 | ExpMonth: 2, 256 | ExpYear: 2020, 257 | }) 258 | if err == nil { 259 | t.Error("err should not be nil") 260 | return 261 | } 262 | if card != nil { 263 | t.Errorf("card should be nil, but %v", card) 264 | } 265 | } 266 | 267 | func TestCustomerDeleteCard(t *testing.T) { 268 | mock, transport := NewMockClient(200, cardResponseJSON) 269 | transport.AddResponse(200, cardDeleteResponseJSON) 270 | service := New("api-key", mock) 271 | err := service.Customer.DeleteCard("cus_121673955bd7aa144de5a8f6c262", "car_f7d9fa98594dc7c2e42bfcd641ff") 272 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 273 | t.Errorf("URL is wrong: %s", transport.URL) 274 | } 275 | if transport.Method != "DELETE" { 276 | t.Errorf("Method should be DELETE, but %s", transport.Method) 277 | } 278 | if err != nil { 279 | t.Errorf("err should be nil, but %v", err) 280 | } 281 | } 282 | 283 | func TestCustomerDeleteCard2(t *testing.T) { 284 | mock, transport := NewMockClient(200, customerResponseJSON) 285 | transport.AddResponse(200, cardDeleteResponseJSON) 286 | service := New("api-key", mock) 287 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 288 | if customer == nil { 289 | t.Error("card should not be nil") 290 | return 291 | } 292 | err = customer.Cards[0].Delete() 293 | if err != nil { 294 | t.Errorf("err should be nil, but %v", err) 295 | } 296 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards/car_f7d9fa98594dc7c2e42bfcd641ff" { 297 | t.Errorf("URL is wrong: %s", transport.URL) 298 | } 299 | if transport.Method != "DELETE" { 300 | t.Errorf("Method should be DELETE, but %s", transport.Method) 301 | } 302 | } 303 | 304 | func TestCustomerListCard(t *testing.T) { 305 | mock, transport := NewMockClient(200, cardListResponseJSON) 306 | service := New("api-key", mock) 307 | cards, hasMore, err := service.Customer.ListCard("cus_121673955bd7aa144de5a8f6c262"). 308 | Limit(10). 309 | Offset(15). 310 | Since(time.Unix(1455328095, 0)). 311 | Until(time.Unix(1455500895, 0)).Do() 312 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards?limit=10&offset=15&since=1455328095&until=1455500895" { 313 | t.Errorf("URL is wrong: %s", transport.URL) 314 | } 315 | if transport.Method != "GET" { 316 | t.Errorf("Method should be GET, but %s", transport.Method) 317 | } 318 | if err != nil { 319 | t.Errorf("err should be nil, but %v", err) 320 | return 321 | } 322 | if !hasMore { 323 | t.Error("parse error: hasMore") 324 | } 325 | if len(cards) != 1 { 326 | t.Error("parse error: plans") 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /v1/charge.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // ChargeService 都度の支払いや定期購入の引き落としのときに生成される、支払い情報を取り扱います。 13 | type ChargeService struct { 14 | service *Service 15 | } 16 | 17 | func newChargeService(service *Service) *ChargeService { 18 | return &ChargeService{ 19 | service: service, 20 | } 21 | } 22 | 23 | // Charge 構造体はCharge.Createのパラメータを設定するのに使用します 24 | type Charge struct { 25 | Currency string // 必須: 3文字のISOコード(現状 “jpy” のみサポート) 26 | CustomerID string // 顧客ID (CardかCustomerのどちらかは必須パラメータ) 27 | Card Card // カードオブジェクト(cardかcustomerのどちらかは必須) 28 | CardToken string // トークンID (CardかCustomerのどちらかは必須パラメータ) 29 | CustomerCardID string // 顧客のカードID 30 | Capture bool // 支払い処理を確定するかどうか (falseの場合、カードの認証と支払い額の確保のみ行う) 31 | Description string // 概要 32 | ExpireDays interface{} // デフォルトで7日となっており、1日~60日の間で設定が可能 33 | Metadata map[string]string // メタデータ 34 | } 35 | 36 | // Create はトークンID、カードを保有している顧客ID、カードオブジェクトのいずれかのパラメーターを指定して支払いを作成します。 37 | // 顧客IDを使って支払いを作成する場合は CustomerCardID に顧客の保有するカードのIDを指定でき、省略された場合はデフォルトカードとして登録されているものが利用されます。 38 | // テスト用のキーでは、本番用の決済ネットワークへは接続されず、実際の請求が行われることもありません。 本番用のキーでは、決済ネットワークで処理が行われ、実際の請求が行われます。 39 | // 40 | // 支払いを確定せずに、カードの認証と支払い額のみ確保する場合は、 Capture に false を指定してください。 このとき ExpireDays を指定することで、認証の期間を定めることができます。 ExpireDays はデフォルトで7日となっており、1日~60日の間で設定が可能です。 41 | func (c ChargeService) Create(amount int, charge Charge) (*ChargeResponse, error) { 42 | var errorMessages []string 43 | if amount < 50 || amount > 9999999 { 44 | errorMessages = append(errorMessages, fmt.Sprintf("Amount should be between 50 and 9,999,999, but %d.", amount)) 45 | } 46 | counter := 0 47 | if charge.CustomerID != "" { 48 | counter++ 49 | } 50 | if charge.CardToken != "" { 51 | counter++ 52 | } 53 | if charge.Card.valid() { 54 | counter++ 55 | } 56 | switch counter { 57 | case 0: 58 | errorMessages = append(errorMessages, "One of the following parameters is required: CustomerID, CardToken, Card") 59 | case 1: 60 | case 2, 3: 61 | errorMessages = append(errorMessages, "The following parameters are exclusive: CustomerID, CardToken, Card") 62 | } 63 | if charge.Currency == "" { 64 | charge.Currency = "jpy" 65 | } else if charge.Currency != "jpy" { 66 | // todo: if pay.jp supports other currency, fix this condition 67 | errorMessages = append(errorMessages, fmt.Sprintf("Only supports 'jpy' as currency, but '%s'.", charge.Currency)) 68 | } 69 | expireDays, ok := charge.ExpireDays.(int) 70 | if ok && (expireDays < -1 || expireDays > 60) { 71 | errorMessages = append(errorMessages, fmt.Sprintf("ExpireDays should be between 1 and 60, but %d.", expireDays)) 72 | } 73 | if len(errorMessages) > 0 { 74 | return nil, fmt.Errorf("Charge.Create() parameter error: %s", strings.Join(errorMessages, ", ")) 75 | } 76 | qb := newRequestBuilder() 77 | qb.Add("amount", amount) 78 | qb.Add("currency", charge.Currency) 79 | if charge.CustomerID != "" { 80 | qb.Add("customer", charge.CustomerID) 81 | if charge.CustomerCardID != "" { 82 | qb.Add("card", charge.CustomerCardID) 83 | } 84 | } else if charge.CardToken != "" { 85 | qb.Add("card", charge.CardToken) 86 | } 87 | qb.AddCard(charge.Card) 88 | qb.Add("description", charge.Description) 89 | qb.Add("capture", charge.Capture) 90 | qb.Add("expiry_days", charge.ExpireDays) 91 | qb.AddMetadata(charge.Metadata) 92 | 93 | request, err := http.NewRequest("POST", c.service.apiBase+"/charges", qb.Reader()) 94 | if err != nil { 95 | return nil, err 96 | } 97 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 98 | request.Header.Add("Authorization", c.service.apiKey) 99 | 100 | body, err := respToBody(c.service.Client.Do(request)) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return parseCharge(c.service, body, &ChargeResponse{}) 105 | 106 | } 107 | 108 | // Retrieve charge object. 支払い情報を取得します。 109 | func (c ChargeService) Retrieve(chargeID string) (*ChargeResponse, error) { 110 | body, err := c.service.retrieve("/charges/" + chargeID) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return parseCharge(c.service, body, &ChargeResponse{}) 115 | } 116 | 117 | func (c ChargeService) update(chargeID, description string, metadata map[string]string) ([]byte, error) { 118 | qb := newRequestBuilder() 119 | qb.Add("description", description) 120 | qb.AddMetadata(metadata) 121 | request, err := http.NewRequest("POST", c.service.apiBase+"/charges/"+chargeID, qb.Reader()) 122 | if err != nil { 123 | return nil, err 124 | } 125 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 126 | request.Header.Add("Authorization", c.service.apiKey) 127 | 128 | return parseResponseError(c.service.Client.Do(request)) 129 | } 130 | 131 | // Update は支払い情報のDescriptionを更新します。 132 | func (c ChargeService) Update(chargeID, description string, metadata ...map[string]string) (*ChargeResponse, error) { 133 | var md map[string]string 134 | switch len(metadata) { 135 | case 0: 136 | case 1: 137 | md = metadata[0] 138 | default: 139 | return nil, fmt.Errorf("Update can accept zero or one metadata map, but %d are passed", len(metadata)) 140 | } 141 | body, err := c.update(chargeID, description, md) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return parseCharge(c.service, body, &ChargeResponse{}) 146 | } 147 | 148 | func (c ChargeService) refund(id string, reason string, amount []int) ([]byte, error) { 149 | qb := newRequestBuilder() 150 | if len(amount) > 0 { 151 | qb.Add("amount", amount[0]) 152 | } 153 | qb.Add("refund_reason", reason) 154 | request, err := http.NewRequest("POST", c.service.apiBase+"/charges/"+id+"/refund", qb.Reader()) 155 | if err != nil { 156 | return nil, err 157 | } 158 | request.Header.Add("Authorization", c.service.apiKey) 159 | 160 | return parseResponseError(c.service.Client.Do(request)) 161 | } 162 | 163 | // Refund は支払い済みとなった処理を返金します。 164 | // Amount省略時は全額返金、指定時に金額の部分返金を行うことができます。 165 | func (c ChargeService) Refund(chargeID, reason string, amount ...int) (*ChargeResponse, error) { 166 | body, err := c.refund(chargeID, reason, amount) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return parseCharge(c.service, body, &ChargeResponse{}) 171 | } 172 | 173 | 174 | 175 | func (c ChargeService) capture(chargeID string, amount []int) ([]byte, error) { 176 | qb := newRequestBuilder() 177 | if len(amount) > 0 { 178 | qb.Add("amount", amount[0]) 179 | } 180 | request, err := http.NewRequest("POST", c.service.apiBase+"/charges/"+chargeID+"/capture", qb.Reader()) 181 | if err != nil { 182 | return nil, err 183 | } 184 | request.Header.Add("Authorization", c.service.apiKey) 185 | 186 | return parseResponseError(c.service.Client.Do(request)) 187 | } 188 | 189 | // Capture は認証状態となった処理待ちの支払い処理を確定させます。具体的には Captured="false" となった支払いが該当します。 190 | // 191 | // amount をセットすることで、支払い生成時の金額と異なる金額の支払い処理を行うことができます。 ただし amount は、支払い生成時の金額よりも少額である必要があるためご注意ください。 192 | // 193 | // amount をセットした場合、AmountRefunded に認証時の amount との差額が入ります。 194 | // 195 | // 例えば、認証時に amount=500 で作成し、 amount=400 で支払い確定を行った場合、 AmountRefunded=100 となり、確定金額が400円に変更された状態で支払いが確定されます。 196 | func (c ChargeService) Capture(chargeID string, amount ...int) (*ChargeResponse, error) { 197 | body, err := c.capture(chargeID, amount) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return parseCharge(c.service, body, &ChargeResponse{}) 202 | } 203 | 204 | // List は生成した支払い情報のリストを取得します。リストは、直近で生成された順番に取得されます。 205 | func (c ChargeService) List() *ChargeListCaller { 206 | return &ChargeListCaller{ 207 | service: c.service, 208 | } 209 | } 210 | 211 | // ChargeListCaller はリスト取得に使用する構造体です。 212 | // 213 | // Fluentインタフェースを提供しており、最後にDoを呼ぶことでリストが取得できます: 214 | // 215 | // pay := payjp.New("api-key", nil) 216 | // charges, err := pay.Charge.List().Limit(50).Offset(150).Do() 217 | type ChargeListCaller struct { 218 | service *Service 219 | limit int 220 | offset int 221 | since int 222 | until int 223 | customerID string 224 | subscriptionID string 225 | } 226 | 227 | // Limit はリストの要素数の最大値を設定します(1-100) 228 | func (c *ChargeListCaller) Limit(limit int) *ChargeListCaller { 229 | c.limit = limit 230 | return c 231 | } 232 | 233 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 234 | func (c *ChargeListCaller) Offset(offset int) *ChargeListCaller { 235 | c.offset = offset 236 | return c 237 | } 238 | 239 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 240 | func (c *ChargeListCaller) Since(since time.Time) *ChargeListCaller { 241 | c.since = int(since.Unix()) 242 | return c 243 | } 244 | 245 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 246 | func (c *ChargeListCaller) Until(until time.Time) *ChargeListCaller { 247 | c.until = int(until.Unix()) 248 | return c 249 | } 250 | 251 | // CustomerID を指定すると、指定した顧客の支払いのみを取得します 252 | func (c *ChargeListCaller) CustomerID(id string) *ChargeListCaller { 253 | c.customerID = id 254 | return c 255 | } 256 | 257 | // SubscriptionID を指定すると、指定した定期購読の支払いのみを取得します 258 | func (c *ChargeListCaller) SubscriptionID(id string) *ChargeListCaller { 259 | c.subscriptionID = id 260 | return c 261 | } 262 | 263 | // Do は指定されたクエリーを元に支払いのリストを配列で取得します。 264 | func (c *ChargeListCaller) Do() ([]*ChargeResponse, bool, error) { 265 | body, err := c.service.queryList("/charges", c.limit, c.offset, c.since, c.until, func(values *url.Values) bool { 266 | result := false 267 | if c.customerID != "" { 268 | values.Add("customer", c.customerID) 269 | result = true 270 | } 271 | if c.subscriptionID != "" { 272 | values.Add("subscription", c.subscriptionID) 273 | result = true 274 | } 275 | return result 276 | }) 277 | if err != nil { 278 | return nil, false, err 279 | } 280 | raw := &listResponseParser{} 281 | err = json.Unmarshal(body, raw) 282 | if err != nil { 283 | return nil, false, err 284 | } 285 | result := make([]*ChargeResponse, len(raw.Data)) 286 | for i, rawCharge := range raw.Data { 287 | charge := &ChargeResponse{} 288 | json.Unmarshal(rawCharge, charge) 289 | charge.service = c.service 290 | result[i] = charge 291 | } 292 | return result, raw.HasMore, nil 293 | } 294 | 295 | func parseCharge(service *Service, data []byte, result *ChargeResponse) (*ChargeResponse, error) { 296 | err := json.Unmarshal(data, result) 297 | if err != nil { 298 | return nil, err 299 | } 300 | result.service = service 301 | return result, nil 302 | } 303 | 304 | // ChargeResponse はCharge.Getなどで返される、支払いに関する情報を持った構造体です 305 | type ChargeResponse struct { 306 | ID string // ch_で始まる一意なオブジェクトを示す文字列 307 | LiveMode bool // 本番環境かどうか 308 | CreatedAt time.Time // この支払い作成時のタイムスタンプ 309 | Amount int // 支払額 310 | Currency string // 3文字のISOコード(現状 “jpy” のみサポート) 311 | Paid bool // 認証処理が成功しているかどうか。 312 | ExpiredAt time.Time // 認証状態が自動的に失効される日時のタイムスタンプ 313 | Captured bool // 支払い処理を確定しているかどうか 314 | CapturedAt time.Time // 支払い処理確定時のタイムスタンプ 315 | Card CardResponse // 支払いされたクレジットカードの情報 316 | CustomerID string // 顧客ID 317 | Description string // 概要 318 | FailureCode string // 失敗した支払いのエラーコード 319 | FailureMessage string // 失敗した支払いの説明 320 | Refunded bool // 返金済みかどうか 321 | AmountRefunded int // この支払いに対しての返金額 322 | RefundReason string // 返金理由 323 | SubscriptionID string // sub_から始まる定期課金のID 324 | Metadata map[string]string // メタデータ 325 | FeeRate string // 決済手数料率 326 | 327 | service *Service 328 | } 329 | 330 | // Update は支払い情報のDescriptionとメタデータ(オプション)を更新します 331 | func (c *ChargeResponse) Update(description string, metadata ...map[string]string) error { 332 | var md map[string]string 333 | switch len(metadata) { 334 | case 0: 335 | case 1: 336 | md = metadata[0] 337 | default: 338 | return fmt.Errorf("Update can accept zero or one metadata map, but %d are passed", len(metadata)) 339 | } 340 | body, err := c.service.Charge.update(c.ID, description, md) 341 | if err != nil { 342 | return err 343 | } 344 | _, err = parseCharge(c.service, body, c) 345 | return err 346 | } 347 | 348 | // Refund 支払い済みとなった処理を返金します。 349 | // 全額返金、及び amount を指定することで金額の部分返金を行うことができます。ただし部分返金を最初に行った場合、2度目の返金は全額返金しか行うことができないため、ご注意ください。 350 | func (c *ChargeResponse) Refund(reason string, amount ...int) error { 351 | var body []byte 352 | var err error 353 | body, err = c.service.Charge.refund(c.ID, reason, amount) 354 | if err != nil { 355 | return err 356 | } 357 | _, err = parseCharge(c.service, body, c) 358 | return err 359 | } 360 | 361 | // Capture は認証状態となった処理待ちの支払い処理を確定させます。具体的には Captured="false" となった支払いが該当します。 362 | // 363 | // amount をセットすることで、支払い生成時の金額と異なる金額の支払い処理を行うことができます。 ただし amount は、支払い生成時の金額よりも少額である必要があるためご注意ください。 364 | // 365 | // amount をセットした場合、AmountRefunded に認証時の amount との差額が入ります。 366 | // 367 | // 例えば、認証時に amount=500 で作成し、 amount=400 で支払い確定を行った場合、 AmountRefunded=100 となり、確定金額が400円に変更された状態で支払いが確定されます。 368 | func (c *ChargeResponse) Capture(amount ...int) error { 369 | body, err := c.service.Charge.capture(c.ID, amount) 370 | if err != nil { 371 | return err 372 | } 373 | _, err = parseCharge(c.service, body, c) 374 | return err 375 | } 376 | 377 | type chargeResponseParser struct { 378 | Amount int `json:"amount"` 379 | AmountRefunded int `json:"amount_refunded"` 380 | Captured bool `json:"captured"` 381 | CapturedEpoch int `json:"captured_at"` 382 | Card json.RawMessage `json:"card"` 383 | CreatedEpoch int `json:"created"` 384 | Currency string `json:"currency"` 385 | Customer string `json:"customer"` 386 | Description string `json:"description"` 387 | ExpiredEpoch int `json:"expired_at"` 388 | FailureCode string `json:"failure_code"` 389 | FailureMessage string `json:"failure_message"` 390 | ID string `json:"id"` 391 | LiveMode bool `json:"livemode"` 392 | Object string `json:"object"` 393 | Paid bool `json:"paid"` 394 | RefundReason string `json:"refund_reason"` 395 | Refunded bool `json:"refunded"` 396 | Subscription string `json:"subscription"` 397 | Metadata map[string]string `json:"metadata"` 398 | FeeRate string `json:"fee_rate"` 399 | } 400 | 401 | // UnmarshalJSON はJSONパース用の内部APIです。 402 | func (c *ChargeResponse) UnmarshalJSON(b []byte) error { 403 | raw := chargeResponseParser{} 404 | err := json.Unmarshal(b, &raw) 405 | if err == nil && raw.Object == "charge" { 406 | c.Amount = raw.Amount 407 | c.AmountRefunded = raw.AmountRefunded 408 | c.Captured = raw.Captured 409 | c.CapturedAt = time.Unix(int64(raw.CapturedEpoch), 0) 410 | json.Unmarshal(raw.Card, &c.Card) 411 | c.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 412 | c.Currency = raw.Currency 413 | c.CustomerID = raw.Customer 414 | c.Description = raw.Description 415 | c.ExpiredAt = time.Unix(int64(raw.ExpiredEpoch), 0) 416 | c.FailureCode = raw.FailureCode 417 | c.FailureMessage = raw.FailureMessage 418 | c.ID = raw.ID 419 | c.LiveMode = raw.LiveMode 420 | c.Paid = raw.Paid 421 | c.RefundReason = raw.RefundReason 422 | c.Refunded = raw.Refunded 423 | c.SubscriptionID = raw.Subscription 424 | c.Metadata = raw.Metadata 425 | c.FeeRate = raw.FeeRate 426 | return nil 427 | } 428 | rawError := errorResponse{} 429 | err = json.Unmarshal(b, &rawError) 430 | if err == nil && rawError.Error.Status != 0 { 431 | return &rawError.Error 432 | } 433 | return nil 434 | } 435 | -------------------------------------------------------------------------------- /v1/charge_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var chargeResponseJSON = []byte(` 10 | { 11 | "amount": 3500, 12 | "amount_refunded": 0, 13 | "captured": true, 14 | "captured_at": 1433127983, 15 | "card": { 16 | "address_city": null, 17 | "address_line1": null, 18 | "address_line2": null, 19 | "address_state": null, 20 | "address_zip": null, 21 | "address_zip_check": "unchecked", 22 | "brand": "Visa", 23 | "country": null, 24 | "created": 1433127983, 25 | "cvc_check": "unchecked", 26 | "exp_month": 2, 27 | "exp_year": 2020, 28 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 29 | "id": "car_d0e44730f83b0a19ba6caee04160", 30 | "last4": "4242", 31 | "name": null, 32 | "object": "card" 33 | }, 34 | "created": 1433127983, 35 | "currency": "jpy", 36 | "customer": null, 37 | "description": null, 38 | "expired_at": null, 39 | "failure_code": null, 40 | "failure_message": null, 41 | "fee_rate": "3.00", 42 | "id": "ch_fa990a4c10672a93053a774730b0a", 43 | "livemode": false, 44 | "object": "charge", 45 | "paid": true, 46 | "refund_reason": null, 47 | "refunded": false, 48 | "subscription": null 49 | } 50 | `) 51 | 52 | var chargeListResponseJSON = []byte(` 53 | { 54 | "count": 1, 55 | "data": [ 56 | { 57 | "amount": 1000, 58 | "amount_refunded": 0, 59 | "captured": true, 60 | "captured_at": 1432965397, 61 | "card": { 62 | "address_city": "\u8d64\u5742", 63 | "address_line1": "7-4", 64 | "address_line2": "203", 65 | "address_state": "\u6e2f\u533a", 66 | "address_zip": "1070050", 67 | "address_zip_check": "passed", 68 | "brand": "Visa", 69 | "country": "JP", 70 | "created": 1432965397, 71 | "cvc_check": "passed", 72 | "exp_month": 12, 73 | "exp_year": 2016, 74 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 75 | "id": "car_7a79b41fed704317ec0deb4ebf93", 76 | "last4": "4242", 77 | "name": "Test Hodler", 78 | "object": "card" 79 | }, 80 | "created": 1432965397, 81 | "currency": "jpy", 82 | "customer": "cus_67fab69c14d8888bba941ae2009b", 83 | "description": "test charge", 84 | "expired_at": null, 85 | "failure_code": null, 86 | "failure_message": null, 87 | "fee_rate": "3.00", 88 | "id": "ch_6421ddf0e12a5e5641d7426f2a2c9", 89 | "livemode": false, 90 | "object": "charge", 91 | "paid": true, 92 | "refund_reason": null, 93 | "refunded": false, 94 | "subscription": null 95 | } 96 | ], 97 | "has_more": true, 98 | "object": "list", 99 | "url": "/v1/charges" 100 | } 101 | `) 102 | 103 | var chargeErrorResponseJSON = []byte(` 104 | { 105 | "error": { 106 | "code": "invalid_number", 107 | "message": "Your card number is invalid.", 108 | "param": "card[number]", 109 | "status": 400, 110 | "type": "card_error" 111 | } 112 | } 113 | `) 114 | 115 | func TestParseChargeResponseJSON(t *testing.T) { 116 | charge := &ChargeResponse{} 117 | err := json.Unmarshal(chargeResponseJSON, charge) 118 | 119 | if err != nil { 120 | t.Errorf("err should be nil, but %v", err) 121 | } 122 | } 123 | 124 | func TestParseChargeErrorResponseJSON(t *testing.T) { 125 | charge := &ChargeResponse{} 126 | err := json.Unmarshal(chargeErrorResponseJSON, charge) 127 | 128 | if err == nil { 129 | t.Error("err should not be nil") 130 | } 131 | } 132 | 133 | func TestChargeCreate(t *testing.T) { 134 | mock, transport := NewMockClient(200, chargeResponseJSON) 135 | service := New("api-key", mock) 136 | charge, err := service.Charge.Create(1000, Charge{ 137 | Card: Card{ 138 | Number: "4242424242424242", 139 | ExpMonth: 2, 140 | ExpYear: 2020, 141 | }, 142 | }) 143 | if transport.URL != "https://api.pay.jp/v1/charges" { 144 | t.Errorf("URL is wrong: %s", transport.URL) 145 | } 146 | if transport.Method != "POST" { 147 | t.Errorf("Method should be POST, but %s", transport.Method) 148 | } 149 | if err != nil { 150 | t.Errorf("err should be nil, but %v", err) 151 | return 152 | } 153 | if charge == nil { 154 | t.Error("charge should not be nil") 155 | } else if charge.Amount != 3500 { 156 | t.Errorf("charge.Amount should be 3500, but %d.", charge.Amount) 157 | } 158 | } 159 | 160 | func TestChargeCreateByNonDefaultard(t *testing.T) { 161 | mock, transport := NewMockClient(200, chargeResponseJSON) 162 | service := New("api-key", mock) 163 | charge, err := service.Charge.Create(1000, Charge{ 164 | CustomerID: "cus_xxxxxxxxx", 165 | CustomerCardID: "car_xxxxxxxxx", 166 | }) 167 | 168 | if transport.URL != "https://api.pay.jp/v1/charges" { 169 | t.Errorf("URL is wrong: %s", transport.URL) 170 | } 171 | 172 | if err != nil { 173 | t.Errorf("err should be nil, but %v", err) 174 | return 175 | } 176 | 177 | if charge == nil { 178 | t.Error("charge should not be nil") 179 | } 180 | } 181 | 182 | func TestChargeRetrieve(t *testing.T) { 183 | mock, transport := NewMockClient(200, chargeResponseJSON) 184 | service := New("api-key", mock) 185 | plan, err := service.Charge.Retrieve("ch_fa990a4c10672a93053a774730b0a") 186 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a" { 187 | t.Errorf("URL is wrong: %s", transport.URL) 188 | } 189 | if transport.Method != "GET" { 190 | t.Errorf("Method should be GET, but %s", transport.Method) 191 | } 192 | if err != nil { 193 | t.Errorf("err should be nil, but %v", err) 194 | return 195 | } else if plan == nil { 196 | t.Error("plan should not be nil") 197 | } else if plan.Amount != 3500 { 198 | t.Errorf("parse error: plan.Amount should be 500, but %d.", plan.Amount) 199 | } 200 | } 201 | 202 | func TestChargeGetError(t *testing.T) { 203 | mock, _ := NewMockClient(200, chargeErrorResponseJSON) 204 | service := New("api-key", mock) 205 | plan, err := service.Charge.Retrieve("ch_fa990a4c10672a93053a774730b0a") 206 | if err == nil { 207 | t.Error("err should not be nil") 208 | } 209 | if plan != nil { 210 | t.Errorf("plan should be nil, but %v", plan) 211 | } 212 | } 213 | 214 | func TestChargeUpdate(t *testing.T) { 215 | mock, transport := NewMockClient(200, chargeResponseJSON) 216 | service := New("api-key", mock) 217 | plan, err := service.Charge.Update("ch_fa990a4c10672a93053a774730b0a", "new description") 218 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a" { 219 | t.Errorf("URL is wrong: %s", transport.URL) 220 | } 221 | if transport.Method != "POST" { 222 | t.Errorf("Method should be POST, but %s", transport.Method) 223 | } 224 | if err != nil { 225 | t.Errorf("err should be nil, but %v", err) 226 | return 227 | } 228 | if plan == nil { 229 | t.Error("plan should not be nil") 230 | } else if plan.Amount != 3500 { 231 | t.Errorf("parse error: plan.Amount should be 500, but %d.", plan.Amount) 232 | } 233 | } 234 | 235 | func TestChargeUpdate2(t *testing.T) { 236 | mock, transport := NewMockClient(200, chargeResponseJSON) 237 | service := New("api-key", mock) 238 | plan, err := service.Charge.Retrieve("ch_fa990a4c10672a93053a774730b0a") 239 | if plan == nil { 240 | t.Error("plan should not be nil") 241 | return 242 | } 243 | err = plan.Update("new description") 244 | if err != nil { 245 | t.Errorf("err should be nil, but %v", err) 246 | } 247 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a" { 248 | t.Errorf("URL is wrong: %s", transport.URL) 249 | } 250 | if transport.Method != "POST" { 251 | t.Errorf("Method should be POST, but %s", transport.Method) 252 | } 253 | } 254 | 255 | func TestChargeRefund(t *testing.T) { 256 | mock, transport := NewMockClient(200, chargeResponseJSON) 257 | service := New("api-key", mock) 258 | _, err := service.Charge.Refund("ch_fa990a4c10672a93053a774730b0a", "reason") 259 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a/refund" { 260 | t.Errorf("URL is wrong: %s", transport.URL) 261 | } 262 | if transport.Method != "POST" { 263 | t.Errorf("Method should be POST, but %s", transport.Method) 264 | } 265 | if err != nil { 266 | t.Errorf("err should be nil, but %v", err) 267 | } 268 | } 269 | 270 | func TestChargeRefund2(t *testing.T) { 271 | mock, transport := NewMockClient(200, chargeResponseJSON) 272 | service := New("api-key", mock) 273 | plan, err := service.Charge.Retrieve("ch_fa990a4c10672a93053a774730b0a") 274 | if plan == nil { 275 | t.Error("plan should not be nil") 276 | return 277 | } 278 | err = plan.Refund("reason") 279 | if err != nil { 280 | t.Errorf("err should be nil, but %v", err) 281 | } 282 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a/refund" { 283 | t.Errorf("URL is wrong: %s", transport.URL) 284 | } 285 | if transport.Method != "POST" { 286 | t.Errorf("Method should be POST, but %s", transport.Method) 287 | } 288 | } 289 | 290 | func TestChargeCapture(t *testing.T) { 291 | mock, transport := NewMockClient(200, chargeResponseJSON) 292 | service := New("api-key", mock) 293 | _, err := service.Charge.Capture("ch_fa990a4c10672a93053a774730b0a") 294 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a/capture" { 295 | t.Errorf("URL is wrong: %s", transport.URL) 296 | } 297 | if transport.Method != "POST" { 298 | t.Errorf("Method should be POST, but %s", transport.Method) 299 | } 300 | if err != nil { 301 | t.Errorf("err should be nil, but %v", err) 302 | } 303 | } 304 | 305 | func TestServiceChargeCaptureChangeAmount(t *testing.T) { 306 | mock, transport := NewMockClient(200, chargeResponseJSON) 307 | service := New("api-key", mock) 308 | _, err := service.Charge.Capture("ch_fa990a4c10672a93053a774730b0a", 300) 309 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a/capture" { 310 | t.Errorf("URL is wrong: %s", transport.URL) 311 | } 312 | if transport.Method != "POST" { 313 | t.Errorf("Method should be POST, but %s", transport.Method) 314 | } 315 | if err != nil { 316 | t.Errorf("err should be nil, but %v", err) 317 | } 318 | } 319 | 320 | func TestChargeCapture2(t *testing.T) { 321 | mock, transport := NewMockClient(200, chargeResponseJSON) 322 | service := New("api-key", mock) 323 | plan, err := service.Charge.Retrieve("ch_fa990a4c10672a93053a774730b0a") 324 | if plan == nil { 325 | t.Error("plan should not be nil") 326 | return 327 | } 328 | err = plan.Capture() 329 | if err != nil { 330 | t.Errorf("err should be nil, but %v", err) 331 | } 332 | if transport.URL != "https://api.pay.jp/v1/charges/ch_fa990a4c10672a93053a774730b0a/capture" { 333 | t.Errorf("URL is wrong: %s", transport.URL) 334 | } 335 | if transport.Method != "POST" { 336 | t.Errorf("Method should be POST, but %s", transport.Method) 337 | } 338 | } 339 | 340 | func TestChargeCaptureChangedAmount(t *testing.T) { 341 | mock, transport := NewMockClient(200, chargeResponseJSON) 342 | service := New("api-key", mock) 343 | chargeID := "ch_fa990a4c10672a93053a774730b0a" 344 | charge, err := service.Charge.Retrieve(chargeID) 345 | newAmount := 100 346 | err = charge.Capture(newAmount) 347 | if err != nil { 348 | t.Errorf("err should be nil, but %v", err) 349 | } 350 | if transport.URL != "https://api.pay.jp/v1/charges/"+chargeID+"/capture" { 351 | t.Errorf("URL is wrong: %s", transport.URL) 352 | } 353 | if transport.Method != "POST" { 354 | t.Errorf("Method should be POST, but %s", transport.Method) 355 | } 356 | } 357 | 358 | func TestChargeList(t *testing.T) { 359 | mock, transport := NewMockClient(200, chargeListResponseJSON) 360 | service := New("api-key", mock) 361 | plans, hasMore, err := service.Charge.List(). 362 | Limit(10). 363 | Offset(15). 364 | Since(time.Unix(1455328095, 0)). 365 | Until(time.Unix(1455500895, 0)).Do() 366 | if transport.URL != "https://api.pay.jp/v1/charges?limit=10&offset=15&since=1455328095&until=1455500895" { 367 | t.Errorf("URL is wrong: %s", transport.URL) 368 | } 369 | if transport.Method != "GET" { 370 | t.Errorf("Method should be GET, but %s", transport.Method) 371 | } 372 | if err != nil { 373 | t.Errorf("err should be nil, but %v", err) 374 | return 375 | } 376 | if !hasMore { 377 | t.Error("parse error: hasMore") 378 | } 379 | if len(plans) != 1 { 380 | t.Error("parse error: plans") 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /v1/client.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | ) 10 | 11 | // Config 構造体はNewに渡すパラメータを設定するのに使用します。 12 | type Config struct { 13 | APIBase string // APIのエンドポイントのURL(省略時は'https://api.pay.jp/v1') 14 | } 15 | 16 | // Service 構造体はPAY.JPのすべてのAPIの起点となる構造体です。 17 | // New()を使ってインスタンスを生成します。 18 | type Service struct { 19 | Client *http.Client 20 | apiKey string 21 | apiBase string 22 | 23 | Charge *ChargeService // 支払いに関するAPI 24 | Customer *CustomerService // 顧客情報に関するAPI 25 | Plan *PlanService // プランに関するAPI 26 | Subscription *SubscriptionService // 定期課金に関するAPI 27 | Token *TokenService // トークンに関するAPI 28 | Transfer *TransferService // 入金に関するAPI 29 | Event *EventService // イベント情報に関するAPI 30 | Account *AccountService // アカウント情報に関するAPI 31 | } 32 | 33 | // New はPAY.JPのAPIを初期化する関数です。 34 | // 35 | // apiKeyはPAY.JPのウェブサイトで作成したキーを指定します。 36 | // 37 | // clientは特別な設定をしたhttp.Clientを使用する場合に渡します。nilを指定するとデフォルトのもhttp.Clientを指定します。 38 | // 39 | // configは追加の設定が必要な場合に渡します。現状で設定できるのはAPIのエントリーポイントのURLのみです。省略できます。 40 | func New(apiKey string, client *http.Client, config ...Config) *Service { 41 | if client == nil { 42 | client = &http.Client{} 43 | } 44 | service := &Service{ 45 | apiKey: "Basic " + base64.StdEncoding.EncodeToString([]byte(apiKey+":")), 46 | Client: client, 47 | } 48 | if len(config) > 0 { 49 | service.apiBase = config[0].APIBase 50 | } else { 51 | service.apiBase = "https://api.pay.jp/v1" 52 | } 53 | 54 | service.Charge = newChargeService(service) 55 | service.Customer = newCustomerService(service) 56 | service.Plan = newPlanService(service) 57 | service.Subscription = newSubscriptionService(service) 58 | service.Account = newAccountService(service) 59 | service.Token = newTokenService(service) 60 | service.Transfer = newTransferService(service) 61 | service.Event = newEventService(service) 62 | 63 | return service 64 | } 65 | 66 | // APIBase はPAY.JPのエントリーポイントの基底部分のURLを返します。 67 | func (s Service) APIBase() string { 68 | return s.apiBase 69 | } 70 | 71 | func (s Service) retrieve(resourceURL string) ([]byte, error) { 72 | request, err := http.NewRequest("GET", s.apiBase+resourceURL, nil) 73 | if err != nil { 74 | return nil, err 75 | } 76 | request.Header.Add("Authorization", s.apiKey) 77 | 78 | return respToBody(s.Client.Do(request)) 79 | } 80 | 81 | func (s Service) delete(resourceURL string) error { 82 | request, err := http.NewRequest("DELETE", s.apiBase+resourceURL, nil) 83 | if err != nil { 84 | return err 85 | } 86 | request.Header.Add("Authorization", s.apiKey) 87 | 88 | _, err = parseResponseError(s.Client.Do(request)) 89 | return err 90 | } 91 | 92 | func (s Service) queryList(resourcePath string, limit, offset, since, until int, callbacks ...func(*url.Values) bool) ([]byte, error) { 93 | return s.queryListAll(resourcePath, limit, offset, since, until, 0, 0, callbacks...) 94 | } 95 | 96 | func (s Service) queryTransferList(resourcePath string, limit, offset, since, until, sinceSheduledDate, untilSheduledDate int, callbacks ...func(*url.Values) bool) ([]byte, error) { 97 | return s.queryListAll(resourcePath, limit, offset, since, until, sinceSheduledDate, untilSheduledDate, callbacks...) 98 | } 99 | 100 | func (s Service) queryListAll(resourcePath string, limit, offset, since, until, sinceSheduledDate, untilSheduledDate int, callbacks ...func(*url.Values) bool) ([]byte, error) { 101 | if limit < 0 || limit > 100 { 102 | return nil, fmt.Errorf("method Limit() should be between 1 and 100, but %d", limit) 103 | } 104 | 105 | values := url.Values{} 106 | hasParam := false 107 | if limit != 0 { 108 | values.Add("limit", strconv.Itoa(limit)) 109 | hasParam = true 110 | } 111 | if offset != 0 { 112 | values.Add("offset", strconv.Itoa(offset)) 113 | hasParam = true 114 | } 115 | if since != 0 { 116 | values.Add("since", strconv.Itoa(since)) 117 | hasParam = true 118 | } 119 | if until != 0 { 120 | values.Add("until", strconv.Itoa(until)) 121 | hasParam = true 122 | } 123 | if sinceSheduledDate != 0 { 124 | values.Add("since_sheduled_date", strconv.Itoa(sinceSheduledDate)) 125 | hasParam = true 126 | } 127 | if untilSheduledDate != 0 { 128 | values.Add("until_sheduled_date", strconv.Itoa(untilSheduledDate)) 129 | hasParam = true 130 | } 131 | // add extra parameters 132 | for _, callback := range callbacks { 133 | if callback(&values) { 134 | hasParam = true 135 | } 136 | } 137 | var requestURL string 138 | if hasParam { 139 | requestURL = s.apiBase + resourcePath + "?" + values.Encode() 140 | } else { 141 | requestURL = s.apiBase + resourcePath 142 | } 143 | request, err := http.NewRequest("GET", requestURL, nil) 144 | if err != nil { 145 | return nil, err 146 | } 147 | request.Header.Add("Authorization", s.apiKey) 148 | 149 | return respToBody(s.Client.Do(request)) 150 | } 151 | -------------------------------------------------------------------------------- /v1/client_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestNewClient(t *testing.T) { 9 | // default constructor 10 | service := New("sk_test_37dba67cf2cb5932eb4859af", nil) 11 | 12 | if service == nil { 13 | t.Error("service should be valid") 14 | } 15 | if service.APIBase() != "https://api.pay.jp/v1" { 16 | t.Errorf(`ApiBase should be "https://api.pay.jp/v1", but "%s"`, service.APIBase()) 17 | } 18 | } 19 | 20 | func TestNewClientWithClient(t *testing.T) { 21 | // init with http.Client (to support proxy, etc) 22 | client := &http.Client{} 23 | service := New("sk_test_37dba67cf2cb5932eb4859af", client) 24 | 25 | if service == nil { 26 | t.Error("service should be valid") 27 | } else if service.Client != client { 28 | t.Error("service.Client should have passed client") 29 | } 30 | } 31 | 32 | func TestNewClientWithConfig(t *testing.T) { 33 | // init with http.Client (to support proxy, etc) 34 | client := &http.Client{} 35 | service := New("sk_test_37dba67cf2cb5932eb4859af", client, Config{ 36 | APIBase: "https://api.pay.jp/v2", 37 | }) 38 | 39 | if service == nil { 40 | t.Error("service should be valid") 41 | } else if service.Client != client { 42 | t.Error("service.Client should have passed client") 43 | } 44 | if service.APIBase() != "https://api.pay.jp/v2" { 45 | t.Errorf(`ApiBase should be "https://api.pay.jp/v2", but "%s"`, service.APIBase()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /v1/customer.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // CustomerService は顧客を管理する機能を提供します。 10 | // 11 | // 顧客における都度の支払いや定期購入、複数カードの管理など、さまざまなことができます。 12 | // 作成した顧客は、あとからカードを追加・更新・削除したり、顧客自体を削除することができます。 13 | type CustomerService struct { 14 | service *Service 15 | } 16 | 17 | func newCustomerService(service *Service) *CustomerService { 18 | return &CustomerService{ 19 | service: service, 20 | } 21 | } 22 | 23 | // Customer は顧客の登録や更新時に使用する構造体です 24 | type Customer struct { 25 | Email interface{} // メールアドレス 26 | Description interface{} // 概要 27 | ID interface{} // 一意の顧客ID 28 | CardToken interface{} // トークンID 29 | DefaultCard interface{} // デフォルトカード 30 | Card Card // カード 31 | Metadata map[string]string // メタデータ 32 | } 33 | 34 | func parseCustomer(service *Service, body []byte, result *CustomerResponse) (*CustomerResponse, error) { 35 | err := json.Unmarshal(body, result) 36 | if err != nil { 37 | return nil, err 38 | } 39 | result.service = service 40 | for _, card := range result.Cards { 41 | card.service = service 42 | card.customerID = result.ID 43 | } 44 | return result, nil 45 | } 46 | 47 | // Create はメールアドレスやIDなどを指定して顧客を作成します。 48 | // 49 | // 作成と同時にカード情報を登録する場合、トークンIDかカードオブジェクトのどちらかを指定します。 50 | // 51 | // 作成した顧客やカード情報はあとから更新・削除することができます。 52 | // 53 | // DefaultCardは更新時のみ設定が可能です 54 | func (c CustomerService) Create(customer Customer) (*CustomerResponse, error) { 55 | qb := newRequestBuilder() 56 | if customer.Email != "" { 57 | qb.Add("email", customer.Email) 58 | } 59 | if customer.Description != "" { 60 | qb.Add("description", customer.Description) 61 | } 62 | if customer.ID != "" { 63 | qb.Add("id", customer.ID) 64 | } 65 | if customer.CardToken != "" { 66 | qb.Add("card", customer.CardToken) 67 | } else { 68 | qb.AddCard(customer.Card) 69 | } 70 | qb.AddMetadata(customer.Metadata) 71 | 72 | request, err := http.NewRequest("POST", c.service.apiBase+"/customers", qb.Reader()) 73 | if err != nil { 74 | return nil, err 75 | } 76 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 77 | request.Header.Add("Authorization", c.service.apiKey) 78 | 79 | body, err := respToBody(c.service.Client.Do(request)) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return parseCustomer(c.service, body, &CustomerResponse{}) 84 | } 85 | 86 | // Retrieve customer object. 顧客情報を取得します。 87 | func (c CustomerService) Retrieve(id string) (*CustomerResponse, error) { 88 | body, err := c.service.retrieve("/customers/" + id) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return parseCustomer(c.service, body, &CustomerResponse{}) 93 | } 94 | 95 | // Update は生成した顧客情報を更新したり、新たなカードを顧客に追加します。 96 | // 97 | // また default_card に保持しているカードIDを指定することで、メイン利用のカードを変更することもできます。 98 | func (c CustomerService) Update(id string, customer Customer) (*CustomerResponse, error) { 99 | body, err := c.update(id, customer) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return parseCustomer(c.service, body, &CustomerResponse{}) 104 | } 105 | 106 | func (c CustomerService) update(id string, customer Customer) ([]byte, error) { 107 | qb := newRequestBuilder() 108 | if customer.Email != "" { 109 | qb.Add("email", customer.Email) 110 | } 111 | if customer.Description != "" { 112 | qb.Add("description", customer.Description) 113 | } 114 | if customer.DefaultCard != "" { 115 | qb.Add("default_card", customer.DefaultCard) 116 | } 117 | if customer.CardToken != "" { 118 | qb.Add("card", customer.CardToken) 119 | } else { 120 | qb.AddCard(customer.Card) 121 | } 122 | qb.AddMetadata(customer.Metadata) 123 | request, err := http.NewRequest("POST", c.service.apiBase+"/customers/"+id, qb.Reader()) 124 | if err != nil { 125 | return nil, err 126 | } 127 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 128 | request.Header.Add("Authorization", c.service.apiKey) 129 | 130 | return parseResponseError(c.service.Client.Do(request)) 131 | } 132 | 133 | // Delete は生成した顧客情報を削除します。削除した顧客情報は、もう一度生成することができないためご注意ください。 134 | func (c CustomerService) Delete(id string) error { 135 | return c.service.delete("/customers/" + id) 136 | } 137 | 138 | // List は生成した顧客情報のリストを取得します。リストは、直近で生成された順番に取得されます。 139 | func (c CustomerService) List() *CustomerListCaller { 140 | return &CustomerListCaller{ 141 | service: c.service, 142 | } 143 | } 144 | 145 | // AddCardToken はトークンIDを指定して、新たにカードを追加します。ただし同じカード番号および同じ有効期限年/月のカードは、重複追加することができません。 146 | func (c CustomerService) AddCardToken(customerID, token string) (*CardResponse, error) { 147 | qb := newRequestBuilder() 148 | qb.Add("card", token) 149 | 150 | request, err := http.NewRequest("POST", c.service.apiBase+"/customers/"+customerID+"/cards", qb.Reader()) 151 | if err != nil { 152 | return nil, err 153 | } 154 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 155 | request.Header.Add("Authorization", c.service.apiKey) 156 | 157 | body, err := respToBody(c.service.Client.Do(request)) 158 | if err != nil { 159 | return nil, err 160 | } 161 | return parseCard(c.service, body, &CardResponse{}, customerID) 162 | } 163 | 164 | func (c CustomerService) postCard(customerID, resourcePath string, card Card, result *CardResponse) (*CardResponse, error) { 165 | qb := newRequestBuilder() 166 | qb.AddCard(card) 167 | 168 | request, err := http.NewRequest("POST", c.service.apiBase+"/customers/"+customerID+"/cards"+resourcePath, qb.Reader()) 169 | if err != nil { 170 | return nil, err 171 | } 172 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 173 | request.Header.Add("Authorization", c.service.apiKey) 174 | 175 | body, err := respToBody(c.service.Client.Do(request)) 176 | if err != nil { 177 | return nil, err 178 | } 179 | return parseCard(c.service, body, result, customerID) 180 | } 181 | 182 | // AddCard はカード情報のパラメーターを指定して、新たにカードを追加します。ただし同じカード番号および同じ有効期限年/月のカードは、重複追加することができません。 183 | func (c CustomerService) AddCard(customerID string, card Card) (*CardResponse, error) { 184 | return c.postCard(customerID, "", card, &CardResponse{}) 185 | } 186 | 187 | // GetCard は顧客の特定のカード情報を取得します。 188 | func (c CustomerService) GetCard(customerID, cardID string) (*CardResponse, error) { 189 | body, err := c.service.retrieve("/customers/" + customerID + "/cards/" + cardID) 190 | if err != nil { 191 | return nil, err 192 | } 193 | return parseCard(c.service, body, &CardResponse{}, customerID) 194 | } 195 | 196 | // UpdateCard は顧客の特定のカード情報を更新します。 197 | func (c CustomerService) UpdateCard(customerID, cardID string, card Card) (*CardResponse, error) { 198 | result := &CardResponse{ 199 | customerID: customerID, 200 | service: c.service, 201 | } 202 | return c.postCard(customerID, "/"+cardID, card, result) 203 | } 204 | 205 | // DeleteCard は顧客の特定のカードを削除します。 206 | func (c CustomerService) DeleteCard(customerID, cardID string) error { 207 | return c.service.delete("/customers/" + customerID + "/cards/" + cardID) 208 | } 209 | 210 | // ListCard は顧客の保持しているカードリストを取得します。リストは、直近で生成された順番に取得されます。 211 | func (c CustomerService) ListCard(customerID string) *CustomerCardListCaller { 212 | return &CustomerCardListCaller{ 213 | service: c.service, 214 | customerID: customerID, 215 | } 216 | } 217 | 218 | // GetSubscription は顧客の特定の定期課金情報を取得します。 219 | func (c CustomerService) GetSubscription(customerID, subscriptionID string) (*SubscriptionResponse, error) { 220 | return c.service.Subscription.Retrieve(customerID, subscriptionID) 221 | } 222 | 223 | // ListSubscription は顧客の定期課金リストを取得します。リストは、直近で生成された順番に取得されます。 224 | func (c CustomerService) ListSubscription(customerID string) *SubscriptionListCaller { 225 | return &SubscriptionListCaller{ 226 | service: c.service, 227 | customerID: customerID, 228 | } 229 | } 230 | 231 | // CustomerListCaller はリスト取得に使用する構造体です。 232 | // 233 | // Fluentインタフェースを提供しており、最後にDoを呼ぶことでリストが取得できます: 234 | // 235 | // pay := payjp.New("api-key", nil) 236 | // customers, err := pay.Customer.List().Limit(50).Offset(150).Do() 237 | type CustomerListCaller struct { 238 | service *Service 239 | limit int 240 | offset int 241 | since int 242 | until int 243 | } 244 | 245 | // Limit はリストの要素数の最大値を設定します(1-100) 246 | func (c *CustomerListCaller) Limit(limit int) *CustomerListCaller { 247 | c.limit = limit 248 | return c 249 | } 250 | 251 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 252 | func (c *CustomerListCaller) Offset(offset int) *CustomerListCaller { 253 | c.offset = offset 254 | return c 255 | } 256 | 257 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 258 | func (c *CustomerListCaller) Since(since time.Time) *CustomerListCaller { 259 | c.since = int(since.Unix()) 260 | return c 261 | } 262 | 263 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 264 | func (c *CustomerListCaller) Until(until time.Time) *CustomerListCaller { 265 | c.until = int(until.Unix()) 266 | return c 267 | } 268 | 269 | // Do は指定されたクエリーを元に顧客のリストを配列で取得します。 270 | func (c *CustomerListCaller) Do() ([]*CustomerResponse, bool, error) { 271 | body, err := c.service.queryList("/customers", c.limit, c.offset, c.since, c.until) 272 | if err != nil { 273 | return nil, false, err 274 | } 275 | raw := &listResponseParser{} 276 | err = json.Unmarshal(body, raw) 277 | if err != nil { 278 | return nil, false, err 279 | } 280 | result := make([]*CustomerResponse, len(raw.Data)) 281 | 282 | for i, raw := range raw.Data { 283 | customer := &CustomerResponse{} 284 | json.Unmarshal(raw, customer) 285 | customer.service = c.service 286 | result[i] = customer 287 | } 288 | return result, raw.HasMore, nil 289 | } 290 | 291 | // CustomerCardListCaller はカードのリスト取得に使用する構造体です。 292 | // 293 | // Fluentインタフェースを提供しており、最後にDoを呼ぶことでリストが取得できます: 294 | // 295 | // pay := payjp.New("api-key", nil) 296 | // cards, err := pay.Customer.ListCard("userID").Limit(50).Offset(150).Do() 297 | type CustomerCardListCaller struct { 298 | service *Service 299 | customerID string 300 | limit int 301 | offset int 302 | since int 303 | until int 304 | } 305 | 306 | // Limit はリストの要素数の最大値を設定します(1-100) 307 | func (c *CustomerCardListCaller) Limit(limit int) *CustomerCardListCaller { 308 | c.limit = limit 309 | return c 310 | } 311 | 312 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 313 | func (c *CustomerCardListCaller) Offset(offset int) *CustomerCardListCaller { 314 | c.offset = offset 315 | return c 316 | } 317 | 318 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 319 | func (c *CustomerCardListCaller) Since(since time.Time) *CustomerCardListCaller { 320 | c.since = int(since.Unix()) 321 | return c 322 | } 323 | 324 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 325 | func (c *CustomerCardListCaller) Until(until time.Time) *CustomerCardListCaller { 326 | c.until = int(until.Unix()) 327 | return c 328 | } 329 | 330 | // Do は指定されたクエリーを元に支払いのリストを配列で取得します。 331 | func (c *CustomerCardListCaller) Do() ([]*CardResponse, bool, error) { 332 | body, err := c.service.queryList("/customers/"+c.customerID+"/cards", c.limit, c.offset, c.since, c.until) 333 | if err != nil { 334 | return nil, false, err 335 | } 336 | raw := &listResponseParser{} 337 | err = json.Unmarshal(body, raw) 338 | if err != nil { 339 | return nil, false, err 340 | } 341 | result := make([]*CardResponse, len(raw.Data)) 342 | for i, rawCustomer := range raw.Data { 343 | card := &CardResponse{} 344 | json.Unmarshal(rawCustomer, card) 345 | result[i] = card 346 | } 347 | return result, raw.HasMore, nil 348 | } 349 | 350 | // CustomerResponse はCustomerService.GetやCustomerService.Listで返される顧客を表す構造体です 351 | type CustomerResponse struct { 352 | ID string // 一意なオブジェクトを示す文字列 353 | LiveMode bool // 本番環境かどうか 354 | CreatedAt time.Time // この顧客作成時のタイムスタンプ 355 | DefaultCard string // 支払いに使用されるカードのcar_から始まるID 356 | Cards []*CardResponse // この顧客に紐づけられているカードのリスト 357 | Email string // メールアドレス 358 | Description string // 概要 359 | Subscriptions []*SubscriptionResponse // この顧客が購読している定期課金のリスト 360 | Metadata map[string]string // メタデータ 361 | 362 | service *Service 363 | } 364 | 365 | type customerResponseParser struct { 366 | Cards listResponseParser `json:"cards"` 367 | CreatedEpoch int `json:"created"` 368 | DefaultCard string `json:"default_card"` 369 | Description string `json:"description"` 370 | Email string `json:"email"` 371 | ID string `json:"id"` 372 | LiveMode bool `json:"livemode"` 373 | Object string `json:"object"` 374 | Subscriptions listResponseParser `json:"subscriptions"` 375 | Metadata map[string]string `json:"metadata"` 376 | } 377 | 378 | // Update は生成した顧客情報を更新したり、新たなカードを顧客に追加することができます。 379 | // 380 | // また default_card に保持しているカードIDを指定することで、メイン利用のカードを変更することもできます。 381 | func (c *CustomerResponse) Update(customer Customer) error { 382 | body, err := c.service.Customer.update(c.ID, customer) 383 | if err != nil { 384 | return err 385 | } 386 | return json.Unmarshal(body, c) 387 | } 388 | 389 | // Delete は生成した顧客情報を削除します。削除した顧客情報は、もう一度生成することができないためご注意ください。 390 | func (c *CustomerResponse) Delete() error { 391 | return c.service.Customer.Delete(c.ID) 392 | } 393 | 394 | // AddCard はカード情報のパラメーターを指定して、新たにカードを追加します。ただし同じカード番号および同じ有効期限年/月のカードは、重複追加することができません。 395 | func (c *CustomerResponse) AddCard(card Card) (*CardResponse, error) { 396 | return c.service.Customer.AddCard(c.ID, card) 397 | } 398 | 399 | // AddCardToken はトークンIDを指定して、新たにカードを追加します。ただし同じカード番号および同じ有効期限年/月のカードは、重複追加することができません。 400 | func (c *CustomerResponse) AddCardToken(token string) (*CardResponse, error) { 401 | return c.service.Customer.AddCardToken(c.ID, token) 402 | } 403 | 404 | // GetCard は顧客の特定のカード情報を取得します。 405 | func (c *CustomerResponse) GetCard(cardID string) (*CardResponse, error) { 406 | return c.service.Customer.GetCard(c.ID, cardID) 407 | } 408 | 409 | // UpdateCard は顧客の特定のカード情報を更新します。 410 | func (c CustomerResponse) UpdateCard(cardID string, card Card) (*CardResponse, error) { 411 | return c.service.Customer.UpdateCard(c.ID, cardID, card) 412 | } 413 | 414 | // DeleteCard は顧客の特定のカードを削除します。 415 | func (c CustomerResponse) DeleteCard(cardID string) error { 416 | return c.service.Customer.DeleteCard(c.ID, cardID) 417 | } 418 | 419 | // ListCard は顧客の保持しているカードリストを取得します。リストは、直近で生成された順番に取得されます。 420 | func (c *CustomerResponse) ListCard() *CustomerCardListCaller { 421 | return c.service.Customer.ListCard(c.ID) 422 | } 423 | 424 | // GetSubscription は顧客の特定の定期課金情報を取得します。 425 | func (c *CustomerResponse) GetSubscription(subscriptionID string) (*SubscriptionResponse, error) { 426 | return c.service.Customer.GetSubscription(c.ID, subscriptionID) 427 | } 428 | 429 | // ListSubscription は顧客の定期課金リストを取得します。リストは、直近で生成された順番に取得されます。 430 | func (c *CustomerResponse) ListSubscription() *SubscriptionListCaller { 431 | return c.service.Customer.ListSubscription(c.ID) 432 | } 433 | 434 | // UnmarshalJSON はJSONパース用の内部APIです。 435 | func (c *CustomerResponse) UnmarshalJSON(b []byte) error { 436 | raw := customerResponseParser{} 437 | err := json.Unmarshal(b, &raw) 438 | if err == nil && raw.Object == "customer" { 439 | c.Cards = make([]*CardResponse, len(raw.Cards.Data)) 440 | for i, rawCard := range raw.Cards.Data { 441 | card := &CardResponse{} 442 | json.Unmarshal(rawCard, card) 443 | c.Cards[i] = card 444 | } 445 | c.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 446 | c.DefaultCard = raw.DefaultCard 447 | c.Description = raw.Description 448 | c.Email = raw.Email 449 | c.ID = raw.ID 450 | c.LiveMode = raw.LiveMode 451 | c.Subscriptions = make([]*SubscriptionResponse, len(raw.Subscriptions.Data)) 452 | for i, rawSubscription := range raw.Subscriptions.Data { 453 | subscription := &SubscriptionResponse{} 454 | json.Unmarshal(rawSubscription, subscription) 455 | c.Subscriptions[i] = subscription 456 | } 457 | c.Metadata = raw.Metadata 458 | return nil 459 | } 460 | rawError := errorResponse{} 461 | err = json.Unmarshal(b, &rawError) 462 | if err == nil && rawError.Error.Status != 0 { 463 | return &rawError.Error 464 | } 465 | 466 | return nil 467 | } 468 | -------------------------------------------------------------------------------- /v1/customer_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var customerResponseJSON = []byte(` 10 | { 11 | "cards": { 12 | "count": 1, 13 | "data": [ 14 | { 15 | "address_city": null, 16 | "address_line1": null, 17 | "address_line2": null, 18 | "address_state": null, 19 | "address_zip": null, 20 | "address_zip_check": "unchecked", 21 | "brand": "Visa", 22 | "country": null, 23 | "created": 1433127983, 24 | "cvc_check": "unchecked", 25 | "exp_month": 2, 26 | "exp_year": 2020, 27 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 28 | "id": "car_f7d9fa98594dc7c2e42bfcd641ff", 29 | "last4": "4242", 30 | "livemode": false, 31 | "name": null, 32 | "object": "card" 33 | } 34 | ], 35 | "has_more": false, 36 | "object": "list", 37 | "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/cards" 38 | }, 39 | "created": 1433127983, 40 | "default_card": null, 41 | "description": "test", 42 | "email": null, 43 | "id": "cus_121673955bd7aa144de5a8f6c262", 44 | "livemode": false, 45 | "object": "customer", 46 | "subscriptions": { 47 | "count": 1, 48 | "data": [ 49 | { 50 | "canceled_at": null, 51 | "created": 1433127983, 52 | "current_period_end": 1435732422, 53 | "current_period_start": 1433140422, 54 | "customer": "cus_4df4b5ed720933f4fb9e28857517", 55 | "id": "sub_567a1e44562932ec1a7682d746e0", 56 | "livemode": false, 57 | "object": "subscription", 58 | "paused_at": null, 59 | "plan": { 60 | "amount": 1000, 61 | "billing_day": null, 62 | "created": 1432965397, 63 | "currency": "jpy", 64 | "id": "pln_9589006d14aad86aafeceac06b60", 65 | "interval": "month", 66 | "name": "test plan", 67 | "object": "plan", 68 | "trial_days": 0 69 | }, 70 | "resumed_at": null, 71 | "start": 1433140422, 72 | "status": "active", 73 | "trial_end": null, 74 | "trial_start": null, 75 | "prorate": false 76 | } 77 | ], 78 | "has_more": false, 79 | "object": "list", 80 | "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions" 81 | } 82 | } 83 | `) 84 | 85 | var customerErrorResponseJSON = []byte(` 86 | { 87 | "error": { 88 | "code": "invalid_param_key", 89 | "message": "Invalid param key to customer.", 90 | "param": "dummy", 91 | "status": 400, 92 | "type": "client_error" 93 | } 94 | } 95 | `) 96 | 97 | var customerListResponseJSON = []byte(` 98 | { 99 | "count": 1, 100 | "data": [ 101 | { 102 | "cards": { 103 | "count": 0, 104 | "data": [], 105 | "has_more": false, 106 | "object": "list", 107 | "url": "/v1/customers/cus_842e21be700d1c8156d9dac025f6/cards" 108 | }, 109 | "created": 1433059905, 110 | "default_card": null, 111 | "description": "test", 112 | "email": null, 113 | "id": "cus_842e21be700d1c8156d9dac025f6", 114 | "livemode": false, 115 | "object": "customer", 116 | "subscriptions": { 117 | "count": 0, 118 | "data": [], 119 | "has_more": false, 120 | "object": "list", 121 | "url": "/v1/customers/cus_842e21be700d1c8156d9dac025f6/subscriptions" 122 | } 123 | } 124 | ], 125 | "has_more": true, 126 | "object": "list", 127 | "url": "/v1/customers" 128 | } 129 | `) 130 | 131 | func TestParseCustomerResponseJson(t *testing.T) { 132 | customer := &CustomerResponse{} 133 | err := json.Unmarshal(customerResponseJSON, customer) 134 | 135 | if err != nil { 136 | t.Errorf("err should be nil, but %v", err) 137 | } 138 | if customer.ID != "cus_121673955bd7aa144de5a8f6c262" { 139 | t.Errorf("customer.ID should be 'cus_121673955bd7aa144de5a8f6c262', but '%s'", customer.ID) 140 | } 141 | } 142 | 143 | func TestCustomerCreate(t *testing.T) { 144 | mock, transport := NewMockClient(200, customerResponseJSON) 145 | service := New("api-key", mock) 146 | customer, err := service.Customer.Create(Customer{ 147 | Description: "test", 148 | }) 149 | if transport.URL != "https://api.pay.jp/v1/customers" { 150 | t.Errorf("URL is wrong: %s", transport.URL) 151 | } 152 | if transport.Method != "POST" { 153 | t.Errorf("Method should be POST, but %s", transport.Method) 154 | } 155 | if err != nil { 156 | t.Errorf("err should be nil, but %v", err) 157 | return 158 | } 159 | if customer == nil { 160 | t.Error("customer should not be nil") 161 | } else if customer.Description != "test" { 162 | t.Errorf("customer.Description should be 'test', but %s.", customer.Description) 163 | } 164 | } 165 | 166 | func TestCustomerCreateError(t *testing.T) { 167 | mock, _ := NewMockClient(400, customerErrorResponseJSON) 168 | service := New("api-key", mock) 169 | customer, err := service.Customer.Create(Customer{ 170 | Description: "test", 171 | }) 172 | if err == nil { 173 | t.Error("err should not be nil") 174 | } 175 | if customer != nil { 176 | t.Errorf("customer should be nil, but %v", customer) 177 | } 178 | } 179 | 180 | func TestCustomerRetrieve(t *testing.T) { 181 | mock, transport := NewMockClient(200, customerResponseJSON) 182 | service := New("api-key", mock) 183 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 184 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262" { 185 | t.Errorf("URL is wrong: %s", transport.URL) 186 | } 187 | if transport.Method != "GET" { 188 | t.Errorf("Method should be GET, but %s", transport.Method) 189 | } 190 | if err != nil { 191 | t.Errorf("err should be nil, but %v", err) 192 | return 193 | } else if customer == nil { 194 | t.Error("customer should not be nil") 195 | } else if customer.Description != "test" { 196 | t.Errorf("parse error: customer.Description should be 500, but %s.", customer.Description) 197 | } else if len(customer.Cards) != 1 { 198 | t.Errorf("parse error: customer.Cards should have 1 card, but %d cards.", len(customer.Cards)) 199 | } 200 | } 201 | 202 | func TestCustomerUpdate(t *testing.T) { 203 | mock, transport := NewMockClient(200, customerResponseJSON) 204 | service := New("api-key", mock) 205 | customer, err := service.Customer.Update("cus_121673955bd7aa144de5a8f6c262", Customer{ 206 | Email: "test@mail.com", 207 | }) 208 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262" { 209 | t.Errorf("URL is wrong: %s", transport.URL) 210 | } 211 | if transport.Method != "POST" { 212 | t.Errorf("Method should be POST, but %s", transport.Method) 213 | } 214 | if err != nil { 215 | t.Errorf("err should be nil, but %v", err) 216 | return 217 | } 218 | if customer == nil { 219 | t.Error("plan should not be nil") 220 | } else if customer.Description != "test" { 221 | t.Errorf("parse error: customer.Description should be 500, but %s.", customer.Description) 222 | } 223 | } 224 | 225 | func TestCustomerUpdate2(t *testing.T) { 226 | mock, transport := NewMockClient(200, customerResponseJSON) 227 | service := New("api-key", mock) 228 | plan, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 229 | if plan == nil { 230 | t.Error("plan should not be nil") 231 | return 232 | } 233 | err = plan.Update(Customer{ 234 | Email: "test@mail.com", 235 | }) 236 | if err != nil { 237 | t.Errorf("err should be nil, but %v", err) 238 | } 239 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262" { 240 | t.Errorf("URL is wrong: %s", transport.URL) 241 | } 242 | if transport.Method != "POST" { 243 | t.Errorf("Method should be POST, but %s", transport.Method) 244 | } 245 | } 246 | 247 | func TestCustomerDelete(t *testing.T) { 248 | mock, transport := NewMockClient(200, customerResponseJSON) 249 | service := New("api-key", mock) 250 | err := service.Customer.Delete("cus_121673955bd7aa144de5a8f6c262") 251 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262" { 252 | t.Errorf("URL is wrong: %s", transport.URL) 253 | } 254 | if transport.Method != "DELETE" { 255 | t.Errorf("Method should be DELETE, but %s", transport.Method) 256 | } 257 | if err != nil { 258 | t.Errorf("err should be nil, but %v", err) 259 | } 260 | } 261 | 262 | func TestCustomerDelete2(t *testing.T) { 263 | mock, transport := NewMockClient(200, customerResponseJSON) 264 | service := New("api-key", mock) 265 | customer, err := service.Customer.Retrieve("cus_121673955bd7aa144de5a8f6c262") 266 | if customer == nil { 267 | t.Error("plan should not be nil") 268 | return 269 | } 270 | err = customer.Delete() 271 | if err != nil { 272 | t.Errorf("err should be nil, but %v", err) 273 | } 274 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262" { 275 | t.Errorf("URL is wrong: %s", transport.URL) 276 | } 277 | if transport.Method != "DELETE" { 278 | t.Errorf("Method should be DELETE, but %s", transport.Method) 279 | } 280 | } 281 | 282 | func TestCustomerList(t *testing.T) { 283 | mock, transport := NewMockClient(200, customerListResponseJSON) 284 | service := New("api-key", mock) 285 | plans, hasMore, err := service.Customer.List(). 286 | Limit(10). 287 | Offset(15). 288 | Since(time.Unix(1455328095, 0)). 289 | Until(time.Unix(1455500895, 0)).Do() 290 | if transport.URL != "https://api.pay.jp/v1/customers?limit=10&offset=15&since=1455328095&until=1455500895" { 291 | t.Errorf("URL is wrong: %s", transport.URL) 292 | } 293 | if transport.Method != "GET" { 294 | t.Errorf("Method should be GET, but %s", transport.Method) 295 | } 296 | if err != nil { 297 | t.Errorf("err should be nil, but %v", err) 298 | return 299 | } 300 | if !hasMore { 301 | t.Error("parse error: hasMore") 302 | } 303 | if len(plans) != 1 { 304 | t.Error("parse error: plans") 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /v1/doc.go: -------------------------------------------------------------------------------- 1 | // PAY.JP For Golang 2 | 3 | // Package payjp contains REST based payment API for PAY.JP. It supports paying in each case and 4 | // subscription paying, customer management and more. 5 | // 6 | // PAY.JPは、RESTをベースに構成された決済APIです。都度の支払い、定期的な支払い、 7 | // 顧客情報の管理など、ビジネス運用における様々なことができます。 8 | // 9 | // - PAY.JP Document: https://pay.jp/docs 10 | // 11 | // - PAY.JP API Docs: https://pay.jp/docs/api/ 12 | // 13 | // Installation 14 | // 15 | // You can download Pay.jp Golang SDK by the following command: 16 | // 17 | // $ go get github.com/payjp/payjp-go/v1 18 | // 19 | // How To Use 20 | // 21 | // Entry point of this package is payjp.New(): 22 | // 23 | // pay := payjp.New("api key", nil) 24 | // 25 | // pay has several children like Token, Customer, Plan, Subscription etc: 26 | // 27 | // customer := pay.Customer.Retrieve("customer ID") 28 | // 29 | package payjp 30 | -------------------------------------------------------------------------------- /v1/event.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | // EventType は、イベントのレスポンスの型を表す列挙型です。 11 | // EventResponse.ResultTypeで種類を表すのに使用されます。 12 | type EventType int 13 | 14 | const ( 15 | // ChargeEvent の場合はイベントに含まれるのがCharge型です 16 | ChargeEvent EventType = iota 17 | // TokenEvent の場合はイベントに含まれるのがToken型です 18 | TokenEvent 19 | // CustomerEvent の場合はイベントに含まれるのがCustomer型です 20 | CustomerEvent 21 | // CardEvent の場合はイベントに含まれるのがCard型です 22 | CardEvent 23 | // PlanEvent の場合はイベントに含まれるのがPlan型です 24 | PlanEvent 25 | // DeleteEvent の場合はイベントに含まれるのがDelete型です 26 | DeleteEvent 27 | // SubscriptionEvent の場合はイベントに含まれるのがSubscription型です 28 | SubscriptionEvent 29 | // TransferEvent の場合はイベントに含まれるのがTransfer型です 30 | TransferEvent 31 | ) 32 | 33 | var eventTypes = map[string]EventType{ 34 | "charge.succeeded": ChargeEvent, 35 | "charge.failed": ChargeEvent, 36 | "charge.updated": ChargeEvent, 37 | "charge.refunded": ChargeEvent, 38 | "charge.captured": ChargeEvent, 39 | "token.create": TokenEvent, 40 | "customer.created": CustomerEvent, 41 | "customer.updated": CustomerEvent, 42 | "customer.deleted": DeleteEvent, 43 | "customer.card.created": CardEvent, 44 | "customer.card.updated": CardEvent, 45 | "customer.card.deleted": DeleteEvent, 46 | "plan.created": PlanEvent, 47 | "plan.updated": PlanEvent, 48 | "plan.deleted": DeleteEvent, 49 | "subscription.created": SubscriptionEvent, 50 | "subscription.updated": SubscriptionEvent, 51 | "subscription.deleted": DeleteEvent, 52 | "subscription.paused": SubscriptionEvent, 53 | "subscription.resumed": SubscriptionEvent, 54 | "subscription.canceled": SubscriptionEvent, 55 | "subscription.renewed": SubscriptionEvent, 56 | "transfer.succeeded": TransferEvent, 57 | } 58 | 59 | // EventService は作成、更新、削除などのイベントを表示するサービスです。 60 | // 61 | // イベント情報は、Webhookで任意のURLへ通知設定をすることができます。 62 | type EventService struct { 63 | service *Service 64 | } 65 | 66 | func newEventService(service *Service) *EventService { 67 | return &EventService{ 68 | service: service, 69 | } 70 | } 71 | 72 | // Retrieve event object. 特定のイベント情報を取得します。 73 | func (e EventService) Retrieve(id string) (*EventResponse, error) { 74 | data, err := e.service.retrieve("/events/" + id) 75 | if err != nil { 76 | return nil, err 77 | } 78 | result := &EventResponse{} 79 | err = json.Unmarshal(data, result) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return result, nil 84 | } 85 | 86 | // List はイベントリストを取得します。リストは、直近で生成された順番に取得されます。 87 | func (e EventService) List() *EventListCaller { 88 | return &EventListCaller{ 89 | service: e.service, 90 | } 91 | } 92 | 93 | // EventListCaller はイベントのリスト取得に使用する構造体です。 94 | type EventListCaller struct { 95 | service *Service 96 | limit int 97 | offset int 98 | resourceID string 99 | typeString string 100 | object string 101 | since int 102 | until int 103 | } 104 | 105 | // Limit はリストの要素数の最大値を設定します(1-100) 106 | func (e *EventListCaller) Limit(limit int) *EventListCaller { 107 | e.limit = limit 108 | return e 109 | } 110 | 111 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 112 | func (e *EventListCaller) Offset(offset int) *EventListCaller { 113 | e.offset = offset 114 | return e 115 | } 116 | 117 | // ResourceID は取得するeventに紐づくAPIリソースのIDを設定します (e.g. customer.id) 118 | func (e *EventListCaller) ResourceID(id string) *EventListCaller { 119 | e.resourceID = id 120 | return e 121 | } 122 | 123 | // Object は取得するeventに紐づくAPIリソースのオブジェクト名を設定します (e.g. customer, charge) 124 | func (e *EventListCaller) Object(object string) *EventListCaller { 125 | e.object = object 126 | return e 127 | } 128 | 129 | // Type は取得するeventのtypeを設定します 130 | func (e *EventListCaller) Type(typeString string) *EventListCaller { 131 | e.typeString = typeString 132 | return e 133 | } 134 | 135 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 136 | func (e *EventListCaller) Since(since time.Time) *EventListCaller { 137 | e.since = int(since.Unix()) 138 | return e 139 | } 140 | 141 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 142 | func (e *EventListCaller) Until(until time.Time) *EventListCaller { 143 | e.until = int(until.Unix()) 144 | return e 145 | } 146 | 147 | // Do は指定されたクエリーを元にイベントのリストを配列で取得します。 148 | func (e *EventListCaller) Do() ([]*EventResponse, bool, error) { 149 | body, err := e.service.queryList("/events", e.limit, e.offset, e.since, e.until, func(values *url.Values) bool { 150 | hasParam := false 151 | if e.resourceID != "" { 152 | values.Set("resource_id", e.resourceID) 153 | hasParam = true 154 | } 155 | if e.object != "" { 156 | values.Set("object", e.object) 157 | hasParam = true 158 | } 159 | if e.typeString != "" { 160 | values.Set("type", e.typeString) 161 | hasParam = true 162 | } 163 | return hasParam 164 | }) 165 | if err != nil { 166 | return nil, false, err 167 | } 168 | raw := &listResponseParser{} 169 | err = json.Unmarshal(body, raw) 170 | if err != nil { 171 | return nil, false, err 172 | } 173 | result := make([]*EventResponse, len(raw.Data)) 174 | for i, rawPlan := range raw.Data { 175 | event := &EventResponse{} 176 | json.Unmarshal(rawPlan, event) 177 | result[i] = event 178 | } 179 | return result, raw.HasMore, nil 180 | } 181 | 182 | // EventResponse は、EventService.Retrieve()/EventService.List()が返す構造体です。 183 | type EventResponse struct { 184 | CreatedAt time.Time 185 | ID string 186 | LiveMode bool 187 | Type string 188 | PendingWebHooks int 189 | ResultType EventType 190 | 191 | data json.RawMessage 192 | } 193 | 194 | // ChargeData は、イベントの種類がChargeEventの時にChargeResponse構造体を返します。 195 | func (e EventResponse) ChargeData() (*ChargeResponse, error) { 196 | if e.ResultType != ChargeEvent { 197 | return nil, errors.New("this event is not charge type") 198 | } 199 | result := &ChargeResponse{} 200 | json.Unmarshal(e.data, result) 201 | return result, nil 202 | } 203 | 204 | // TokenData は、イベントの種類がTokenEventの時にTokenResponse構造体を返します。 205 | func (e EventResponse) TokenData() (*TokenResponse, error) { 206 | if e.ResultType != TokenEvent { 207 | return nil, errors.New("this event is not token type") 208 | } 209 | result := &TokenResponse{} 210 | json.Unmarshal(e.data, result) 211 | return result, nil 212 | } 213 | 214 | // CustomerData は、イベントの種類がCustomerEventの時にCustomerResponse構造体を返します。 215 | func (e EventResponse) CustomerData() (*CustomerResponse, error) { 216 | if e.ResultType != CustomerEvent { 217 | return nil, errors.New("this event is not customer type") 218 | } 219 | result := &CustomerResponse{} 220 | json.Unmarshal(e.data, result) 221 | return result, nil 222 | } 223 | 224 | // CardData は、イベントの種類がCardEventの時にCardResponse構造体を返します。 225 | func (e EventResponse) CardData() (*CardResponse, error) { 226 | if e.ResultType != CardEvent { 227 | return nil, errors.New("this event is not card type") 228 | } 229 | result := &CardResponse{} 230 | json.Unmarshal(e.data, result) 231 | return result, nil 232 | } 233 | 234 | // PlanData は、イベントの種類がPlanEventの時にPlanResponse構造体を返します。 235 | func (e EventResponse) PlanData() (*PlanResponse, error) { 236 | if e.ResultType != PlanEvent { 237 | return nil, errors.New("this event is not plan type") 238 | } 239 | result := &PlanResponse{} 240 | json.Unmarshal(e.data, result) 241 | return result, nil 242 | } 243 | 244 | // SubscriptionData は、イベントの種類がSubscriptionEventの時にSubscriptionResponse構造体を返します。 245 | func (e EventResponse) SubscriptionData() (*SubscriptionResponse, error) { 246 | if e.ResultType != SubscriptionEvent { 247 | return nil, errors.New("this event is not subscription type") 248 | } 249 | result := &SubscriptionResponse{} 250 | json.Unmarshal(e.data, result) 251 | return result, nil 252 | } 253 | 254 | // TransferData は、イベントの種類がTransferEventの時にTransferResponse構造体を返します。 255 | func (e EventResponse) TransferData() (*TransferResponse, error) { 256 | if e.ResultType != TransferEvent { 257 | return nil, errors.New("this event is not tranfer type") 258 | } 259 | result := &TransferResponse{} 260 | json.Unmarshal(e.data, result) 261 | return result, nil 262 | } 263 | 264 | // DeleteData は、イベントの種類がDeleteEventの時にDeleteResponse構造体を返します。 265 | func (e EventResponse) DeleteData() (*DeleteResponse, error) { 266 | if e.ResultType != DeleteEvent { 267 | return nil, errors.New("this event is not delete type") 268 | } 269 | result := &DeleteResponse{} 270 | json.Unmarshal(e.data, result) 271 | return result, nil 272 | } 273 | 274 | type eventResponseParser struct { 275 | CreatedEpoch int `json:"created"` 276 | Data json.RawMessage `json:"data"` 277 | ID string `json:"id"` 278 | LiveMode bool `json:"livemode"` 279 | Object string `json:"object"` 280 | PendingWebHooks int `json:"pending_webhooks"` 281 | Type string `json:"type"` 282 | 283 | CreatedAt time.Time 284 | } 285 | 286 | // UnmarshalJSON はJSONパース用の内部APIです。 287 | func (e *EventResponse) UnmarshalJSON(b []byte) error { 288 | raw := eventResponseParser{} 289 | err := json.Unmarshal(b, &raw) 290 | if err == nil && raw.Object == "event" { 291 | e.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 292 | e.data = raw.Data 293 | e.ID = raw.ID 294 | e.LiveMode = raw.LiveMode 295 | e.PendingWebHooks = raw.PendingWebHooks 296 | e.Type = raw.Type 297 | e.ResultType = eventTypes[raw.Type] 298 | 299 | return nil 300 | } 301 | rawError := errorResponse{} 302 | err = json.Unmarshal(b, &rawError) 303 | if err == nil && rawError.Error.Status != 0 { 304 | return &rawError.Error 305 | } 306 | 307 | return nil 308 | } 309 | 310 | // DeleteResponse はイベントの種類がDeleteEventの時にDeleteData()が返す構造体です。 311 | type DeleteResponse struct { 312 | Deleted bool `json:"deleted"` 313 | ID string `json:"id"` 314 | LiveMode bool `json:"livemode"` 315 | } 316 | -------------------------------------------------------------------------------- /v1/event_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var eventResponseJSON = []byte(` 10 | { 11 | "created": 1442288882, 12 | "data": { 13 | "cards": { 14 | "count": 0, 15 | "data": [], 16 | "has_more": false, 17 | "object": "list", 18 | "url": "/v1/customers/cus_a16c7b4df01168eb82557fe93de4/cards" 19 | }, 20 | "created": 1441936720, 21 | "default_card": null, 22 | "description": "updated\n", 23 | "email": null, 24 | "id": "cus_a16c7b4df01168eb82557fe93de4", 25 | "livemode": false, 26 | "object": "customer", 27 | "subscriptions": { 28 | "count": 0, 29 | "data": [], 30 | "has_more": false, 31 | "object": "list", 32 | "url": "/v1/customers/cus_a16c7b4df01168eb82557fe93de4/subscriptions" 33 | } 34 | }, 35 | "id": "evnt_54db4d63c7886256acdbc784ccf", 36 | "livemode": false, 37 | "object": "event", 38 | "pending_webhooks": 1, 39 | "type": "customer.updated" 40 | } 41 | `) 42 | 43 | var eventListResponseJSON = []byte(` 44 | { 45 | "count": 1, 46 | "data": [ 47 | { 48 | "created": 1442298026, 49 | "data": { 50 | "amount": 5000, 51 | "amount_refunded": 5000, 52 | "captured": true, 53 | "captured_at": 1442212986, 54 | "card": { 55 | "address_city": null, 56 | "address_line1": null, 57 | "address_line2": null, 58 | "address_state": null, 59 | "address_zip": null, 60 | "address_zip_check": "unchecked", 61 | "brand": "Visa", 62 | "country": null, 63 | "created": 1442212986, 64 | "customer": null, 65 | "cvc_check": "passed", 66 | "exp_month": 1, 67 | "exp_year": 2016, 68 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 69 | "id": "car_f0984a6f68a730b7e1814ceabfe1", 70 | "last4": "4242", 71 | "name": null, 72 | "object": "card" 73 | }, 74 | "created": 1442212986, 75 | "currency": "jpy", 76 | "customer": null, 77 | "description": "hogehoe", 78 | "expired_at": null, 79 | "failure_code": null, 80 | "failure_message": null, 81 | "id": "ch_bcb7776459913c743c20e9f9351d4", 82 | "livemode": false, 83 | "object": "charge", 84 | "paid": true, 85 | "refund_reason": null, 86 | "refunded": true, 87 | "subscription": null 88 | }, 89 | "id": "evnt_8064917698aa417a3c86d292266", 90 | "livemode": false, 91 | "object": "event", 92 | "pending_webhooks": 1, 93 | "type": "charge.updated" 94 | } 95 | ], 96 | "has_more": true, 97 | "object": "list", 98 | "url": "/v1/events" 99 | } 100 | `) 101 | 102 | func TestParseEventResponseJSON(t *testing.T) { 103 | event := &EventResponse{} 104 | err := json.Unmarshal(eventResponseJSON, event) 105 | 106 | if err != nil { 107 | t.Errorf("err should be nil, but %v", err) 108 | } 109 | if event.ID != "evnt_54db4d63c7886256acdbc784ccf" { 110 | t.Errorf("event.ID should be 'evnt_54db4d63c7886256acdbc784ccf', but '%s'", event.ID) 111 | } 112 | if event.Type != "customer.updated" { 113 | t.Errorf("parse error: %s", event.Type) 114 | } 115 | if event.ResultType != CustomerEvent { 116 | t.Errorf("parse error: %v", event.ResultType) 117 | } 118 | 119 | card, err := event.CardData() 120 | if card != nil { 121 | t.Errorf("card should be nil, but %v", card) 122 | } 123 | if err == nil { 124 | t.Error("error should not be nil") 125 | } 126 | 127 | customer, err := event.CustomerData() 128 | if customer == nil { 129 | t.Error("customer should not be nil") 130 | } 131 | if err != nil { 132 | t.Errorf("error should be nil, but %v", err) 133 | } 134 | } 135 | 136 | func TestEventRetrieve(t *testing.T) { 137 | mock, transport := NewMockClient(200, eventResponseJSON) 138 | service := New("api-key", mock) 139 | event, err := service.Event.Retrieve("evnt_54db4d63c7886256acdbc784ccf") 140 | if transport.URL != "https://api.pay.jp/v1/events/evnt_54db4d63c7886256acdbc784ccf" { 141 | t.Errorf("URL is wrong: %s", transport.URL) 142 | } 143 | if transport.Method != "GET" { 144 | t.Errorf("Method should be GET, but %s", transport.Method) 145 | } 146 | if err != nil { 147 | t.Errorf("err should be nil, but %v", err) 148 | return 149 | } else if event == nil { 150 | t.Error("event should not be nil") 151 | } else if event.PendingWebHooks != 1 { 152 | t.Errorf("parse error: event.PendingWebHooks should be 1, but %d.", event.PendingWebHooks) 153 | } 154 | } 155 | 156 | func TestEventList(t *testing.T) { 157 | mock, transport := NewMockClient(200, eventListResponseJSON) 158 | service := New("api-key", mock) 159 | events, hasMore, err := service.Event.List(). 160 | Limit(10). 161 | Offset(15). 162 | Type("charge.updated"). 163 | Since(time.Unix(1455328095, 0)). 164 | Until(time.Unix(1455500895, 0)).Do() 165 | if transport.URL != "https://api.pay.jp/v1/events?limit=10&offset=15&since=1455328095&type=charge.updated&until=1455500895" { 166 | t.Errorf("URL is wrong: %s", transport.URL) 167 | } 168 | if transport.Method != "GET" { 169 | t.Errorf("Method should be GET, but %s", transport.Method) 170 | } 171 | if err != nil { 172 | t.Errorf("err should be nil, but %v", err) 173 | return 174 | } 175 | if !hasMore { 176 | t.Error("parse error: hasMore") 177 | } 178 | if len(events) != 1 { 179 | t.Error("parse error: plans") 180 | } else if events[0].PendingWebHooks != 1 { 181 | t.Errorf("parse error: event.PendingWebHooks should be 1, but %d.", events[0].PendingWebHooks) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /v1/examples/account/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/payjp/payjp-go/v1" 9 | ) 10 | 11 | func main() { 12 | payjpService := payjp.New("sk_test_c62fade9d045b54cd76d7036", nil) 13 | 14 | // Get Account 15 | fmt.Println("Get Account") 16 | account, _ := payjpService.Account.Retrieve() 17 | fmt.Println(" Id:", account.ID) 18 | fmt.Println(" CreatedAt: ", account.CreatedAt.Format(time.RFC1123Z)) 19 | fmt.Println(" Email: ", account.Email) 20 | fmt.Println(" Merchant:") 21 | fmt.Println(" Id", account.Merchant.ID) 22 | fmt.Println(" BankEnabled:", account.Merchant.BankEnabled) 23 | fmt.Println(" BrandsAccepted:", strings.Join(account.Merchant.BrandsAccepted, ", ")) 24 | fmt.Println(" ContactPhone:", account.Merchant.ContactPhone) 25 | fmt.Println(" Country:", account.Merchant.Country) 26 | fmt.Println(" CurrenciesSupported:", account.Merchant.CurrenciesSupported) 27 | fmt.Println(" DefaultCurrency:", account.Merchant.DefaultCurrency) 28 | fmt.Println(" DetailsSubmitted:", account.Merchant.DetailsSubmitted) 29 | fmt.Println(" LiveModeActivatedAt:", account.Merchant.LiveModeActivatedAt.Format(time.RFC1123Z)) 30 | fmt.Println(" LiveModeEnabled:", account.Merchant.LiveModeEnabled) 31 | fmt.Println(" ProductDetail:", account.Merchant.ProductDetail) 32 | fmt.Println(" ProductName:", account.Merchant.ProductName) 33 | fmt.Println(" SitePublished:", account.Merchant.SitePublished) 34 | fmt.Println(" URL:", account.Merchant.URL) 35 | fmt.Println(" TeamId: ", account.TeamID) 36 | } 37 | -------------------------------------------------------------------------------- /v1/examples/charge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/payjp/payjp-go/v1" 6 | ) 7 | 8 | func main() { 9 | pay := payjp.New("sk_test_c62fade9d045b54cd76d7036", nil) 10 | 11 | // 支払いをします 12 | 13 | // カードトークンを作成(サンプルのトークンは以下などで生成できます) 14 | // https://pay.jp/docs/checkout 15 | var tokenToCharge string = "tok_xxxxx" 16 | charge, _ := pay.Charge.Create(3500, payjp.Charge{ 17 | // 現在はjpyのみサポート 18 | Currency: "jpy", 19 | CardToken: tokenToCharge, 20 | Capture: true, 21 | // 概要のテキストを設定できます 22 | Description: "Book: 'The Art of Community'", 23 | // 追加のメタデータを20件まで設定できます 24 | Metadata: map[string]string{ 25 | "ISBN": "1449312063", 26 | }, 27 | }) 28 | fmt.Println("Amount:", charge.Amount) 29 | fmt.Println("Paid:", charge.Paid) 30 | // Output: 31 | // Paid: true 32 | 33 | // 与信確保をします 34 | // 上記支払いをする際に使ったトークンとは別のものを指定してください。 35 | 36 | // カードトークンを作成 37 | var tokenToAuth string = "tok_yyyyy" 38 | 39 | authorizedCharge, _ := pay.Charge.Create(2800, payjp.Charge{ 40 | // 現在はjpyのみサポート 41 | Currency: "jpy", 42 | CardToken: tokenToAuth, 43 | Capture: false, 44 | // 概要のテキストを設定できます 45 | Description: "Book: 'The Art of Community'", 46 | // 追加のメタデータを20件まで設定できます 47 | Metadata: map[string]string{ 48 | "ISBN": "1449312063", 49 | }, 50 | }) 51 | fmt.Println("Amount:", authorizedCharge.Amount) 52 | fmt.Println("AmountRefunded:", authorizedCharge.AmountRefunded) 53 | fmt.Println("Paid:", authorizedCharge.Paid) 54 | fmt.Println("Captured:", authorizedCharge.Captured) 55 | // Output: 56 | // Amount: 2800 57 | // AmountRefunded: 0 58 | // Paid: true 59 | // Captured: false 60 | 61 | authorizedCharge.Capture(2000) 62 | capturedCharge, _ := pay.Charge.Retrieve(authorizedCharge.ID) 63 | fmt.Println("Amount:", capturedCharge.Amount) 64 | fmt.Println("AmountRefunded:", capturedCharge.AmountRefunded) 65 | fmt.Println("Capture:", capturedCharge.Captured) 66 | // Output: 67 | // Amount: 2800 68 | // AmountRefunded: 800 69 | // Captured: truea 70 | } 71 | -------------------------------------------------------------------------------- /v1/examples/plan/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/payjp/payjp-go/v1" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | payjpService := payjp.New("sk_test_c62fade9d045b54cd76d7036", nil) 11 | 12 | fmt.Println("Getting existing plan") 13 | plans, hasMore, _ := payjpService.Plan.List().Limit(10).Offset(10).Do() 14 | fmt.Println("hasMore:", hasMore) 15 | for i, plan := range plans { 16 | fmt.Printf("%d:\n", i) 17 | fmt.Println(" Id:", plan.ID) 18 | fmt.Println(" BillingDay:", plan.BillingDay) 19 | fmt.Println(" CreatedAt:", plan.CreatedAt.Format(time.RFC1123Z)) 20 | fmt.Println(" Amount:", plan.Amount) 21 | fmt.Println(" Currency:", plan.Currency) 22 | fmt.Println(" Interval:", plan.Interval) 23 | fmt.Println(" LiveMode:", plan.LiveMode) 24 | fmt.Println(" Name:", plan.Name) 25 | fmt.Println(" TrialDays:", plan.TrialDays) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /v1/examples/subscription/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/payjp/payjp-go/v1" 6 | ) 7 | 8 | func main() { 9 | service := payjp.New("sk_test_c62fade9d045b54cd76d7036", nil) 10 | subscriptions, _, err := service.Subscription.List().Do() 11 | if err != nil { 12 | fmt.Println("subscription list error") 13 | } 14 | subscription := subscriptions[0] 15 | fmt.Println("NextCyclePlan:", subscription.NextCyclePlan) 16 | id := subscription.ID 17 | 18 | plan, err := service.Plan.Create(payjp.Plan{ 19 | Interval: "month", 20 | Currency: "jpy", 21 | Amount: 1000, 22 | }) 23 | if err != nil { 24 | fmt.Println("err:", err) 25 | } 26 | fmt.Println("Plan:", plan) 27 | 28 | setSubscr, err := service.Subscription.Update(id, payjp.Subscription{ 29 | NextCyclePlanID: plan.ID, 30 | }) 31 | fmt.Println("NextCyclePlan:", setSubscr.NextCyclePlan) 32 | 33 | delSubscr, err := service.Subscription.Update(id, payjp.Subscription{ 34 | NextCyclePlanID: "", 35 | }) 36 | fmt.Println("NextCyclePlan:", delSubscr.NextCyclePlan) 37 | if service.Plan.Delete(plan.ID) != nil { 38 | fmt.Println("err:", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /v1/examples/transfer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/payjp/payjp-go/v1" 6 | ) 7 | 8 | func main() { 9 | payjpService := payjp.New("sk_test_c62fade9d045b54cd76d7036", nil) 10 | 11 | fmt.Println("See transfer summary") 12 | transfers, _, _ := payjpService.Transfer.List().Limit(1).Do() 13 | for i, transfer := range transfers { 14 | fmt.Printf("%d:\n", i) 15 | fmt.Println(" id:", transfer.ID) 16 | fmt.Println(" summary[charge_count]:", transfer.Summary.ChargeCount) 17 | fmt.Println(" summary[charge_fee]:", transfer.Summary.ChargeFee) 18 | fmt.Println(" summary[charge_gross]:", transfer.Summary.ChargeGross) 19 | fmt.Println(" summary[net]:", transfer.Summary.Net) 20 | fmt.Println(" summary[refund_amount]:", transfer.Summary.RefundAmount) 21 | fmt.Println(" summary[refund_count]:", transfer.Summary.RefundCount) 22 | fmt.Println(" summary[dispute_amount]:", transfer.Summary.DisputeAmount) 23 | fmt.Println(" summary[dispute_count]:", transfer.Summary.DisputeCount) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /v1/payjperror.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Error はPAY.JP固有のエラーを表す構造体です 10 | type Error struct { 11 | Code string `json:"code"` 12 | Message string `json:"message"` 13 | Param string `json:"param"` 14 | Status int `json:"status"` 15 | Type string `json:"type"` 16 | } 17 | 18 | func (ce Error) Error() string { 19 | if ce.Param != "" { 20 | return fmt.Sprintf("%d: Type: %s Code: %s Message: %s, Param: %s", ce.Status, ce.Type, ce.Code, ce.Message, ce.Param) 21 | } 22 | return fmt.Sprintf("%d: Type: %s Code: %s Message: %s", ce.Status, ce.Type, ce.Code, ce.Message) 23 | } 24 | 25 | type errorResponse struct { 26 | Error Error `json:"error"` 27 | } 28 | 29 | func parseResponseError(resp *http.Response, err error) ([]byte, error) { 30 | body, err := respToBody(resp, err) 31 | if err != nil { 32 | return nil, err 33 | } 34 | payjpError := &Error{} 35 | err = json.Unmarshal(body, payjpError) 36 | if err != nil { 37 | // ignore JSON parsing error. 38 | // Subscription JSON has same name property 'status' but it is string. 39 | // it would be error, but it can be omitted. 40 | return body, nil 41 | } 42 | if payjpError.Status != 0 { 43 | return nil, payjpError 44 | } 45 | return body, nil 46 | } 47 | -------------------------------------------------------------------------------- /v1/plan.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // PlanService は定期購入のときに使用する静的なプラン情報を扱います。 13 | // 14 | // 金額、支払い実行日(1-31)、トライアル日数などを指定して、 あなたのビジネスに必要なさまざまなプランを生成することができます。 15 | // 16 | // 生成したプランは、顧客と紐付けて定期購入処理を行うことができます。 17 | type PlanService struct { 18 | service *Service 19 | } 20 | 21 | func newPlanService(service *Service) *PlanService { 22 | return &PlanService{ 23 | service: service, 24 | } 25 | } 26 | 27 | // Plan はプランの作成時に使用する構造体です。 28 | type Plan struct { 29 | Amount int // 必須: 金額。50~9,999,999の整数 30 | Currency string // 3文字のISOコード(現状 “jpy” のみサポート) 31 | Interval string // month のみ指定可能 32 | ID string // プランID 33 | Name string // プランの名前 34 | TrialDays int // トライアル日数 35 | BillingDay int // 支払いの実行日(1〜31) 36 | Metadata map[string]string // メタデータ 37 | } 38 | 39 | // Create は金額や通貨などを指定して定期購入に利用するプランを生成します。 40 | // 41 | // トライアル日数を指定することで、トライアル付きのプランを生成することができます。 42 | // 43 | // また、支払いの実行日を指定すると、支払い日の固定されたプランを生成することができます。 44 | func (p PlanService) Create(plan Plan) (*PlanResponse, error) { 45 | var errors []string 46 | if plan.Amount < 50 || plan.Amount > 9999999 { 47 | errors = append(errors, fmt.Sprintf("Amount should be between 50 and 9,999,999, but %d.", plan.Amount)) 48 | } 49 | if plan.Currency == "" { 50 | plan.Currency = "jpy" 51 | } else if plan.Currency != "jpy" { 52 | // todo: if pay.jp supports other currency, fix this condition 53 | errors = append(errors, fmt.Sprintf("Only supports 'jpy' as currency, but '%s'.", plan.Currency)) 54 | } 55 | if plan.Interval == "" { 56 | plan.Interval = "month" 57 | } else if plan.Interval != "month" { 58 | // todo: if pay.jp supports other interval options, fix this condition 59 | errors = append(errors, fmt.Sprintf("Only supports 'month' as interval, but '%s'.", plan.Interval)) 60 | } 61 | if plan.BillingDay < 0 || plan.BillingDay > 31 { 62 | errors = append(errors, fmt.Sprintf("BillingDay should be between 1 and 31, but %d.", plan.BillingDay)) 63 | } 64 | if len(errors) != 0 { 65 | return nil, fmt.Errorf("payjp.Plan.Create() parameter error: %s", strings.Join(errors, ", ")) 66 | } 67 | qb := newRequestBuilder() 68 | qb.Add("amount", strconv.Itoa(plan.Amount)) 69 | qb.Add("currency", plan.Currency) 70 | qb.Add("interval", plan.Interval) 71 | if plan.ID != "" { 72 | qb.Add("id", plan.ID) 73 | } 74 | if plan.Name != "" { 75 | qb.Add("name", plan.Name) 76 | } 77 | if plan.TrialDays != 0 { 78 | qb.Add("trial_days", strconv.Itoa(plan.TrialDays)) 79 | } 80 | if plan.BillingDay != 0 { 81 | qb.Add("billing_day", strconv.Itoa(plan.BillingDay)) 82 | } 83 | qb.AddMetadata(plan.Metadata) 84 | request, err := http.NewRequest("POST", p.service.apiBase+"/plans", qb.Reader()) 85 | if err != nil { 86 | return nil, err 87 | } 88 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 89 | request.Header.Add("Authorization", p.service.apiKey) 90 | 91 | body, err := respToBody(p.service.Client.Do(request)) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return parsePlan(p.service, body, &PlanResponse{}) 96 | } 97 | 98 | // Retrieve plan object. 特定のプラン情報を取得します。 99 | func (p PlanService) Retrieve(id string) (*PlanResponse, error) { 100 | body, err := p.service.retrieve("/plans/" + id) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return parsePlan(p.service, body, &PlanResponse{}) 105 | } 106 | 107 | func parsePlan(service *Service, body []byte, result *PlanResponse) (*PlanResponse, error) { 108 | err := json.Unmarshal(body, result) 109 | if err != nil { 110 | return nil, err 111 | } 112 | result.service = service 113 | return result, nil 114 | } 115 | 116 | func (p PlanService) update(id, name string) ([]byte, error) { 117 | qb := newRequestBuilder() 118 | qb.Add("name", name) 119 | request, err := http.NewRequest("POST", p.service.apiBase+"/plans/"+id, qb.Reader()) 120 | if err != nil { 121 | return nil, err 122 | } 123 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 124 | request.Header.Add("Authorization", p.service.apiKey) 125 | 126 | return parseResponseError(p.service.Client.Do(request)) 127 | } 128 | 129 | // Update はプラン情報を更新します。 130 | func (p PlanService) Update(id, name string) (*PlanResponse, error) { 131 | body, err := p.update(id, name) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return parsePlan(p.service, body, &PlanResponse{}) 136 | } 137 | 138 | // Delete はプランを削除します。 139 | func (p PlanService) Delete(id string) error { 140 | return p.service.delete("/plans/" + id) 141 | } 142 | 143 | // List は生成したプランのリストを取得します。リストは、直近で生成された順番に取得されます。 144 | func (p PlanService) List() *PlanListCaller { 145 | return &PlanListCaller{ 146 | service: p.service, 147 | } 148 | } 149 | 150 | // PlanListCaller はプランのリスト取得に使用する構造体です。 151 | type PlanListCaller struct { 152 | service *Service 153 | limit int 154 | offset int 155 | since int 156 | until int 157 | } 158 | 159 | // Limit はリストの要素数の最大値を設定します(1-100) 160 | func (c *PlanListCaller) Limit(limit int) *PlanListCaller { 161 | c.limit = limit 162 | return c 163 | } 164 | 165 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 166 | func (c *PlanListCaller) Offset(offset int) *PlanListCaller { 167 | c.offset = offset 168 | return c 169 | } 170 | 171 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 172 | func (c *PlanListCaller) Since(since time.Time) *PlanListCaller { 173 | c.since = int(since.Unix()) 174 | return c 175 | } 176 | 177 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 178 | func (c *PlanListCaller) Until(until time.Time) *PlanListCaller { 179 | c.until = int(until.Unix()) 180 | return c 181 | } 182 | 183 | // Do は指定されたクエリーを元にプランのリストを配列で取得します。 184 | func (c *PlanListCaller) Do() ([]*PlanResponse, bool, error) { 185 | body, err := c.service.queryList("/plans", c.limit, c.offset, c.since, c.until) 186 | if err != nil { 187 | return nil, false, err 188 | } 189 | raw := &listResponseParser{} 190 | err = json.Unmarshal(body, raw) 191 | if err != nil { 192 | return nil, false, err 193 | } 194 | result := make([]*PlanResponse, len(raw.Data)) 195 | for i, rawPlan := range raw.Data { 196 | plan := &PlanResponse{} 197 | json.Unmarshal(rawPlan, plan) 198 | plan.service = c.service 199 | result[i] = plan 200 | } 201 | return result, raw.HasMore, nil 202 | } 203 | 204 | // PlanResponse はPlanService.はPlanService.Listで返されるプランを表す構造体です 205 | type PlanResponse struct { 206 | ID string // 一意なオブジェクトを示す文字列 207 | LiveMode bool // 本番環境かどうか 208 | CreatedAt time.Time // このプラン作成時のタイムスタンプ 209 | Amount int // プラン金額 210 | Currency string // 3文字のISOコード(現状 “jpy” のみサポート) 211 | Interval string // 課金周期(現状"month"のみサポート) 212 | Name string // プラン名 213 | TrialDays int // トライアル日数 214 | BillingDay int // 課金日(1-31) 215 | Metadata map[string]string // メタデータ 216 | 217 | service *Service 218 | } 219 | 220 | type planResponseParser struct { 221 | Amount int `json:"amount"` 222 | BillingDay int `json:"billing_day"` 223 | CreatedEpoch int `json:"created"` 224 | Currency string `json:"currency"` 225 | ID string `json:"id"` 226 | Interval string `json:"interval"` 227 | LiveMode bool `json:"livemode"` 228 | Name string `json:"name"` 229 | Object string `json:"object"` 230 | TrialDays int `json:"trial_days"` 231 | Metadata map[string]string `json:"metadata"` 232 | } 233 | 234 | // Update はプラン情報を更新します。 235 | func (p *PlanResponse) Update(name string) error { 236 | body, err := p.service.Plan.update(p.ID, name) 237 | if err != nil { 238 | return err 239 | } 240 | return json.Unmarshal(body, p) 241 | } 242 | 243 | // Delete はプランを削除します。 244 | func (p *PlanResponse) Delete() error { 245 | return p.service.Plan.Delete(p.ID) 246 | } 247 | 248 | // UnmarshalJSON はJSONパース用の内部APIです。 249 | func (p *PlanResponse) UnmarshalJSON(b []byte) error { 250 | raw := planResponseParser{} 251 | err := json.Unmarshal(b, &raw) 252 | if err == nil && raw.Object == "plan" { 253 | p.Amount = raw.Amount 254 | p.BillingDay = raw.BillingDay 255 | p.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 256 | p.Currency = raw.Currency 257 | p.ID = raw.ID 258 | p.Interval = raw.Interval 259 | p.LiveMode = raw.LiveMode 260 | p.Name = raw.Name 261 | p.TrialDays = raw.TrialDays 262 | p.Metadata = raw.Metadata 263 | return nil 264 | } 265 | rawError := errorResponse{} 266 | err = json.Unmarshal(b, &rawError) 267 | if err == nil && rawError.Error.Status != 0 { 268 | return &rawError.Error 269 | } 270 | 271 | return nil 272 | } 273 | -------------------------------------------------------------------------------- /v1/plan_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var planResponseJSON = []byte(`{ 10 | "amount": 500, 11 | "billing_day": null, 12 | "created": 1433127983, 13 | "currency": "jpy", 14 | "id": "pln_45dd3268a18b2837d52861716260", 15 | "interval": "month", 16 | "livemode": false, 17 | "name": null, 18 | "object": "plan", 19 | "trial_days": 30 20 | }`) 21 | 22 | var planListResponseJSON = []byte(` 23 | { 24 | "count": 1, 25 | "data": [ 26 | { 27 | "amount": 500, 28 | "billing_day": null, 29 | "created": 1433127983, 30 | "currency": "jpy", 31 | "id": "pln_45dd3268a18b2837d52861716260", 32 | "interval": "month", 33 | "livemode": false, 34 | "name": null, 35 | "object": "plan", 36 | "trial_days": 30 37 | } 38 | ], 39 | "object": "list", 40 | "has_more": true, 41 | "url": "/v1/customers/cus_4df4b5ed720933f4fb9e28857517/cards" 42 | } 43 | `) 44 | 45 | var planErrorResponseJSON = []byte(` 46 | { 47 | "error": { 48 | "message": "There is no plan with ID: dummy", 49 | "param": "id", 50 | "status": 404, 51 | "type": "client_error" 52 | } 53 | } 54 | `) 55 | 56 | func TestParsePlanResponseJSON(t *testing.T) { 57 | plan := &PlanResponse{} 58 | err := json.Unmarshal(planResponseJSON, plan) 59 | 60 | if err != nil { 61 | t.Errorf("err should be nil, but %v", err) 62 | } 63 | if plan.CreatedAt.Unix() != 1433127983 { 64 | t.Errorf("plan.Created should be '1433127983', but '%d'", plan.CreatedAt.Unix()) 65 | } 66 | } 67 | 68 | func TestPlanCreate(t *testing.T) { 69 | mock, transport := NewMockClient(200, planResponseJSON) 70 | service := New("api-key", mock) 71 | plan, err := service.Plan.Create(Plan{ 72 | Amount: 500, 73 | Currency: "jpy", 74 | Interval: "month", 75 | }) 76 | if transport.URL != "https://api.pay.jp/v1/plans" { 77 | t.Errorf("URL is wrong: %s", transport.URL) 78 | } 79 | if transport.Method != "POST" { 80 | t.Errorf("Method should be POST, but %s", transport.Method) 81 | } 82 | if err != nil { 83 | t.Errorf("err should be nil, but %v", err) 84 | return 85 | } 86 | if plan == nil { 87 | t.Error("plan should not be nil") 88 | } else if plan.Amount != 500 { 89 | t.Errorf("plan.Amount should be 500, but %d.", plan.Amount) 90 | } 91 | } 92 | 93 | func TestPlanRetrieve(t *testing.T) { 94 | mock, transport := NewMockClient(200, planResponseJSON) 95 | service := New("api-key", mock) 96 | plan, err := service.Plan.Retrieve("pln_45dd3268a18b2837d52861716260") 97 | if transport.URL != "https://api.pay.jp/v1/plans/pln_45dd3268a18b2837d52861716260" { 98 | t.Errorf("URL is wrong: %s", transport.URL) 99 | } 100 | if transport.Method != "GET" { 101 | t.Errorf("Method should be GET, but %s", transport.Method) 102 | } 103 | if err != nil { 104 | t.Errorf("err should be nil, but %v", err) 105 | return 106 | } else if plan == nil { 107 | t.Error("plan should not be nil") 108 | } else if plan.Amount != 500 { 109 | t.Errorf("parse error: plan.Amount should be 500, but %d.", plan.Amount) 110 | } 111 | } 112 | 113 | func TestPlanGetError(t *testing.T) { 114 | mock, _ := NewMockClient(200, planErrorResponseJSON) 115 | service := New("api-key", mock) 116 | plan, err := service.Plan.Retrieve("pln_45dd3268a18b2837d52861716260") 117 | if err == nil { 118 | t.Error("err should not be nil") 119 | } 120 | if plan != nil { 121 | t.Errorf("plan should be nil, but %v", plan) 122 | } 123 | } 124 | 125 | func TestPlanUpdate(t *testing.T) { 126 | mock, transport := NewMockClient(200, planResponseJSON) 127 | service := New("api-key", mock) 128 | plan, err := service.Plan.Update("pln_45dd3268a18b2837d52861716260", "new name") 129 | if transport.URL != "https://api.pay.jp/v1/plans/pln_45dd3268a18b2837d52861716260" { 130 | t.Errorf("URL is wrong: %s", transport.URL) 131 | } 132 | if transport.Method != "POST" { 133 | t.Errorf("Method should be POST, but %s", transport.Method) 134 | } 135 | if err != nil { 136 | t.Errorf("err should be nil, but %v", err) 137 | return 138 | } 139 | if plan == nil { 140 | t.Error("plan should not be nil") 141 | } else if plan.Amount != 500 { 142 | t.Errorf("parse error: plan.Amount should be 500, but %d.", plan.Amount) 143 | } 144 | } 145 | 146 | func TestPlanUpdate2(t *testing.T) { 147 | mock, transport := NewMockClient(200, planResponseJSON) 148 | service := New("api-key", mock) 149 | plan, err := service.Plan.Retrieve("pln_45dd3268a18b2837d52861716260") 150 | if plan == nil { 151 | t.Error("plan should not be nil") 152 | return 153 | } 154 | err = plan.Update("new name") 155 | if err != nil { 156 | t.Errorf("err should be nil, but %v", err) 157 | } 158 | if transport.URL != "https://api.pay.jp/v1/plans/pln_45dd3268a18b2837d52861716260" { 159 | t.Errorf("URL is wrong: %s", transport.URL) 160 | } 161 | if transport.Method != "POST" { 162 | t.Errorf("Method should be POST, but %s", transport.Method) 163 | } 164 | } 165 | 166 | func TestPlanDelete(t *testing.T) { 167 | mock, transport := NewMockClient(200, planResponseJSON) 168 | service := New("api-key", mock) 169 | err := service.Plan.Delete("pln_45dd3268a18b2837d52861716260") 170 | if transport.URL != "https://api.pay.jp/v1/plans/pln_45dd3268a18b2837d52861716260" { 171 | t.Errorf("URL is wrong: %s", transport.URL) 172 | } 173 | if transport.Method != "DELETE" { 174 | t.Errorf("Method should be DELETE, but %s", transport.Method) 175 | } 176 | if err != nil { 177 | t.Errorf("err should be nil, but %v", err) 178 | } 179 | } 180 | 181 | func TestPlanDelete2(t *testing.T) { 182 | mock, transport := NewMockClient(200, planResponseJSON) 183 | service := New("api-key", mock) 184 | plan, err := service.Plan.Retrieve("pln_45dd3268a18b2837d52861716260") 185 | if plan == nil { 186 | t.Error("plan should not be nil") 187 | return 188 | } 189 | err = plan.Delete() 190 | if err != nil { 191 | t.Errorf("err should be nil, but %v", err) 192 | } 193 | if transport.URL != "https://api.pay.jp/v1/plans/pln_45dd3268a18b2837d52861716260" { 194 | t.Errorf("URL is wrong: %s", transport.URL) 195 | } 196 | if transport.Method != "DELETE" { 197 | t.Errorf("Method should be DELETE, but %s", transport.Method) 198 | } 199 | } 200 | 201 | func TestPlanList(t *testing.T) { 202 | mock, transport := NewMockClient(200, planListResponseJSON) 203 | service := New("api-key", mock) 204 | plans, hasMore, err := service.Plan.List(). 205 | Limit(10). 206 | Offset(15). 207 | Since(time.Unix(1455328095, 0)). 208 | Until(time.Unix(1455500895, 0)).Do() 209 | if transport.URL != "https://api.pay.jp/v1/plans?limit=10&offset=15&since=1455328095&until=1455500895" { 210 | t.Errorf("URL is wrong: %s", transport.URL) 211 | } 212 | if transport.Method != "GET" { 213 | t.Errorf("Method should be GET, but %s", transport.Method) 214 | } 215 | if err != nil { 216 | t.Errorf("err should be nil, but %v", err) 217 | return 218 | } 219 | if !hasMore { 220 | t.Error("parse error: hasMore") 221 | } 222 | if len(plans) != 1 { 223 | t.Error("parse error: plans") 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /v1/subscription.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // SubscriptionStatus は定期購読のステータスを表すEnumです。 14 | type SubscriptionStatus int 15 | 16 | const ( 17 | noSubscriptionStatus SubscriptionStatus = iota 18 | // SubscriptionActive はアクティブ状態を表す定数 19 | SubscriptionActive 20 | // SubscriptionTrial はトライアル状態を表す定数 21 | SubscriptionTrial 22 | // SubscriptionCanceled はキャンセル状態を表す定数 23 | SubscriptionCanceled 24 | // SubscriptionPaused は停止状態を表す定数 25 | SubscriptionPaused 26 | ) 27 | 28 | func (s SubscriptionStatus) status() interface{} { 29 | switch s { 30 | case SubscriptionActive: 31 | return "active" 32 | case SubscriptionTrial: 33 | return "trial" 34 | case SubscriptionCanceled: 35 | return "canceled" 36 | case SubscriptionPaused: 37 | return "paused" 38 | } 39 | return nil 40 | } 41 | 42 | // SubscriptionService は月単位で定期的な支払い処理を行うサービスです。顧客IDとプランIDを指定して生成します。 43 | // 44 | // stauts=SubscriptionTrial の場合は支払いは行われず、status=SubscriptionActive の場合のみ支払いが行われます。 45 | // 46 | // 支払い処理は、はじめに定期課金を生成した瞬間に行われ、そこを基準にして定期的な支払いが行われていきます。 47 | // 定期課金は、顧客に複数紐付けるができ、生成した定期課金は停止・再開・キャンセル・削除することができます。 48 | type SubscriptionService struct { 49 | service *Service 50 | } 51 | 52 | func newSubscriptionService(service *Service) *SubscriptionService { 53 | return &SubscriptionService{ 54 | service: service, 55 | } 56 | } 57 | 58 | // Subscription はSubscribeやUpdateの引数を設定するのに使用する構造体です。 59 | type Subscription struct { 60 | TrialEndAt time.Time // トライアルの終了時期 61 | SkipTrial interface{} // トライアルをしない(bool) 62 | PlanID interface{} // プランID(string) 63 | NextCyclePlanID interface{} // 次サイクルから適用するプランID(string, 更新時のみ設定可能) 64 | Prorate interface{} // 日割り課金をするかどうか(bool) 65 | Metadata map[string]string // メタデータ 66 | } 67 | 68 | // Subscribe は顧客IDとプランIDを指定して、定期課金を開始することができます。 69 | // TrialEndを指定することで、プラン情報を上書きするトライアル設定も可能です。 70 | // 最初の支払いは定期課金作成時に実行されます。 71 | // 支払い実行日(BillingDay)が指定されているプランの場合は日割り設定(Prorate)を有効化しない限り、 72 | // 作成時よりもあとの支払い実行日に最初の課金が行われます。またトライアル設定がある場合は、 73 | // トライアル終了時に支払い処理が行われ、そこを基準にして定期課金が開始されます。 74 | func (s SubscriptionService) Subscribe(customerID string, subscription Subscription) (*SubscriptionResponse, error) { 75 | var errors []string 76 | planID, ok := subscription.PlanID.(string) 77 | if !ok || planID == "" { 78 | errors = append(errors, "PlanID is required, but empty.") 79 | } 80 | var defaultTime time.Time 81 | skipTrial, ok := subscription.SkipTrial.(bool) 82 | if subscription.TrialEndAt != defaultTime && ok { 83 | errors = append(errors, "TrialEndAt and SkipTrial are exclusive.") 84 | } 85 | if len(errors) != 0 { 86 | return nil, fmt.Errorf("Subscription.Subscribe() parameter error: %s", strings.Join(errors, ", ")) 87 | } 88 | qb := newRequestBuilder() 89 | qb.Add("customer", customerID) 90 | qb.Add("plan", subscription.PlanID) 91 | if subscription.TrialEndAt != defaultTime { 92 | qb.Add("trial_end", strconv.Itoa(int(subscription.TrialEndAt.Unix()))) 93 | } else if ok && skipTrial { 94 | qb.Add("trial_end", "now") 95 | } 96 | qb.Add("prorate", subscription.Prorate) 97 | qb.AddMetadata(subscription.Metadata) 98 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions", qb.Reader()) 99 | if err != nil { 100 | return nil, err 101 | } 102 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 103 | request.Header.Add("Authorization", s.service.apiKey) 104 | 105 | body, err := respToBody(s.service.Client.Do(request)) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 110 | } 111 | 112 | // Retrieve subscription object. 特定の定期課金情報を取得します。 113 | func (s SubscriptionService) Retrieve(customerID, subscriptionID string) (*SubscriptionResponse, error) { 114 | body, err := s.service.retrieve("/customers/" + customerID + "/subscriptions/" + subscriptionID) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 119 | } 120 | 121 | func (s SubscriptionService) update(subscriptionID string, subscription Subscription) ([]byte, error) { 122 | var defaultTime time.Time 123 | _, ok := subscription.SkipTrial.(bool) 124 | if subscription.TrialEndAt != defaultTime && ok { 125 | return nil, errors.New("Subscription.Update() parameter error: TrialEndAt and SkipTrial are exclusive") 126 | } 127 | qb := newRequestBuilder() 128 | qb.Add("next_cycle_plan", subscription.NextCyclePlanID) 129 | qb.Add("plan", subscription.PlanID) 130 | if subscription.TrialEndAt != defaultTime { 131 | qb.Add("trial_end", strconv.Itoa(int(subscription.TrialEndAt.Unix()))) 132 | } else if subscription.SkipTrial == true { 133 | qb.Add("trial_end", "now") 134 | } 135 | qb.Add("prorate", subscription.Prorate) 136 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+subscriptionID, qb.Reader()) 137 | if err != nil { 138 | return nil, err 139 | } 140 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 141 | request.Header.Add("Authorization", s.service.apiKey) 142 | return parseResponseError(s.service.Client.Do(request)) 143 | } 144 | 145 | // Update はトライアル期間を新たに設定したり、プランの変更を行うことができます。 146 | // 147 | // トライアル期間を更新する場合、トライアル期間終了時に支払い処理が行われ、 148 | // そこを基準としてプランに沿った周期で定期課金が再開されます。 149 | // このトライアル期間を利用すれば、定期課金の開始日を任意の日にずらすこともできます。 150 | // また SkipTrial=true とする事により、トライアル期間中の定期課金を即時開始できます。 151 | // 152 | // プランを変更する場合は、 PlanID に新しいプランのIDを指定してください。 153 | // 同時に Prorate=true とする事により、 日割り課金を有効化できます。 154 | func (s SubscriptionService) Update(subscriptionID string, subscription Subscription) (*SubscriptionResponse, error) { 155 | body, err := s.update(subscriptionID, subscription) 156 | if err != nil { 157 | return nil, err 158 | } 159 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 160 | } 161 | 162 | // Pause は引き落としの失敗やカードが不正である、また定期課金を停止したい場合はこのリクエストで定期購入を停止させます。 163 | // 164 | // 定期課金を停止させると、再開されるまで引き落とし処理は一切行われません。 165 | func (s SubscriptionService) Pause(subscriptionID string) (*SubscriptionResponse, error) { 166 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+subscriptionID+"/pause", nil) 167 | if err != nil { 168 | return nil, err 169 | } 170 | request.Header.Add("Authorization", s.service.apiKey) 171 | body, err := respToBody(s.service.Client.Do(request)) 172 | if err != nil { 173 | return nil, err 174 | } 175 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 176 | } 177 | 178 | // Resume は停止もしくはキャンセル状態の定期課金を再開させます。 179 | // トライアル日数が残っていて再開日がトライアル終了日時より前の場合、 180 | // トライアル状態で定期課金が再開されます。 181 | // 182 | // TrialEndを指定することで、トライアル終了日を任意の日時に再指定する事ができます。 183 | // 184 | // 支払いの失敗が原因で停止状態にある定期課金の再開時は未払い分の支払いを行います。 185 | // 186 | // 未払い分の支払いに失敗すると、定期課金は再開されません。 この場合は、有効なカードを顧客のデフォルトカードにセットしてから、 187 | // 再度定期課金の再開を行ってください。 188 | // 189 | // またProrate を指定することで、日割り課金を有効化することができます。 日割り課金が有効な場合は、 190 | // 再開日より課金日までの日数分で課金額を日割りします。 191 | func (s SubscriptionService) Resume(subscriptionID string, subscription Subscription) (*SubscriptionResponse, error) { 192 | var defaultTime time.Time 193 | qb := newRequestBuilder() 194 | if subscription.TrialEndAt != defaultTime { 195 | qb.Add("trial_end", strconv.Itoa(int(subscription.TrialEndAt.Unix()))) 196 | } 197 | qb.Add("prorate", subscription.Prorate) 198 | 199 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+subscriptionID+"/resume", qb.Reader()) 200 | if err != nil { 201 | return nil, err 202 | } 203 | request.Header.Add("Authorization", s.service.apiKey) 204 | body, err := respToBody(s.service.Client.Do(request)) 205 | if err != nil { 206 | return nil, err 207 | } 208 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 209 | } 210 | 211 | // Cancel は定期課金をキャンセルし、現在の周期の終了日をもって定期課金を終了させます。 212 | // 213 | // 終了日以前であれば、定期課金の再開リクエスト(Resume)を行うことで、 214 | // キャンセルを取り消すことができます。終了日をむかえた定期課金は、 215 | // 自動的に削除されますのでご注意ください。 216 | func (s SubscriptionService) Cancel(subscriptionID string) (*SubscriptionResponse, error) { 217 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+subscriptionID+"/cancel", nil) 218 | if err != nil { 219 | return nil, err 220 | } 221 | request.Header.Add("Authorization", s.service.apiKey) 222 | body, err := respToBody(s.service.Client.Do(request)) 223 | if err != nil { 224 | return nil, err 225 | } 226 | return parseSubscription(s.service, body, &SubscriptionResponse{}) 227 | } 228 | 229 | // Delete は定期課金をすぐに削除します。次回以降の課金は行われずに、一度削除した定期課金は、 230 | // 再び戻すことができません。 231 | func (s SubscriptionService) Delete(subscriptionID string) error { 232 | request, err := http.NewRequest("DELETE", s.service.apiBase+"/subscriptions/"+subscriptionID, nil) 233 | if err != nil { 234 | return err 235 | } 236 | request.Header.Add("Authorization", s.service.apiKey) 237 | _, err = parseResponseError(s.service.Client.Do(request)) 238 | return err 239 | } 240 | 241 | // List は顧客の定期課金リストを取得します。リストは、直近で生成された順番に取得されます。 242 | func (s SubscriptionService) List() *SubscriptionListCaller { 243 | return &SubscriptionListCaller{ 244 | service: s.service, 245 | } 246 | } 247 | 248 | func parseSubscription(service *Service, body []byte, result *SubscriptionResponse) (*SubscriptionResponse, error) { 249 | err := json.Unmarshal(body, result) 250 | if err != nil { 251 | return nil, err 252 | } 253 | result.service = service 254 | return result, nil 255 | } 256 | 257 | // SubscriptionResponse はSubscriptionService.GetやSubscriptionService.Listで返される 258 | // 定期購読情報持つ構造体です。 259 | type SubscriptionResponse struct { 260 | ID string // sub_で始まる一意なオブジェクトを示す文字列 261 | LiveMode bool // 本番環境かどうか 262 | CreatedAt time.Time // この定期課金作成時のタイムスタンプ 263 | StartAt time.Time // この定期課金開始時のタイムスタンプ 264 | CustomerID string // この定期課金を購読している顧客のID 265 | Plan Plan // この定期課金のプラン情報 266 | NextCyclePlan *Plan // この定期課金の次のサイクルから適用されるプラン情報 267 | Status SubscriptionStatus // この定期課金の現在の状態 268 | Prorate bool // 日割り課金が有効かどうか 269 | CurrentPeriodStartAt time.Time // 現在の購読期間開始時のタイムスタンプ 270 | CurrentPeriodEndAt time.Time // 現在の購読期間終了時のタイムスタンプ 271 | TrialStartAt time.Time // トライアル期間開始時のタイムスタンプ 272 | TrialEndAt time.Time // トライアル期間終了時のタイムスタンプ 273 | PausedAt time.Time // 定期課金が停止状態になった時のタイムスタンプ 274 | CanceledAt time.Time // 定期課金がキャンセル状態になった時のタイムスタンプ 275 | ResumedAt time.Time // 停止またはキャンセル状態の定期課金が有効状態になった時のタイムスタンプ 276 | Metadata map[string]string // メタデータ 277 | 278 | service *Service 279 | } 280 | 281 | type subscriptionResponseParser struct { 282 | CanceledEpoch int `json:"canceled_at"` 283 | CreatedEpoch int `json:"created"` 284 | CurrentPeriodEndEpoch int `json:"current_period_end"` 285 | CurrentPeriodStartEpoch int `json:"current_period_start"` 286 | Customer string `json:"customer"` 287 | ID string `json:"id"` 288 | LiveMode bool `json:"livemode"` 289 | Object string `json:"object"` 290 | PausedEpoch int `json:"paused_at"` 291 | Plan json.RawMessage `json:"plan"` 292 | NextCyclePlan json.RawMessage `json:"next_cycle_plan"` 293 | Prorate bool `json:"prorate"` 294 | ResumedEpoch int `json:"resumed_at"` 295 | StartEpoch int `json:"start"` 296 | Status string `json:"status"` 297 | TrialEndEpoch int `json:"trial_end"` 298 | TrialStartEpoch int `json:"trial_start"` 299 | Metadata map[string]string `json:"metadata"` 300 | } 301 | 302 | // Update はトライアル期間を新たに設定したり、プランの変更を行うことができます。 303 | func (s *SubscriptionResponse) Update(subscription Subscription) error { 304 | body, err := s.service.Subscription.update(s.ID, subscription) 305 | if err != nil { 306 | return err 307 | } 308 | _, err = parseSubscription(s.service, body, s) 309 | return err 310 | } 311 | 312 | // Pause は引き落としの失敗やカードが不正である、また定期課金を停止したい場合はこのリクエストで定期購入を停止させます。 313 | func (s *SubscriptionResponse) Pause() error { 314 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+s.ID+"/pause", nil) 315 | if err != nil { 316 | return err 317 | } 318 | request.Header.Add("Authorization", s.service.apiKey) 319 | body, err := respToBody(s.service.Client.Do(request)) 320 | if err != nil { 321 | return err 322 | } 323 | _, err = parseSubscription(s.service, body, s) 324 | return err 325 | } 326 | 327 | // Resume は停止もしくはキャンセル状態の定期課金を再開させます。 328 | func (s *SubscriptionResponse) Resume(subscription Subscription) error { 329 | var defaultTime time.Time 330 | qb := newRequestBuilder() 331 | if subscription.TrialEndAt != defaultTime { 332 | qb.Add("trial_end", strconv.Itoa(int(subscription.TrialEndAt.Unix()))) 333 | } 334 | qb.Add("prorate", subscription.Prorate) 335 | 336 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+s.ID+"/resume", qb.Reader()) 337 | if err != nil { 338 | return err 339 | } 340 | request.Header.Add("Authorization", s.service.apiKey) 341 | body, err := respToBody(s.service.Client.Do(request)) 342 | if err != nil { 343 | return err 344 | } 345 | _, err = parseSubscription(s.service, body, s) 346 | return err 347 | } 348 | 349 | // Cancel は定期課金をキャンセルし、現在の周期の終了日をもって定期課金を終了させます。 350 | func (s *SubscriptionResponse) Cancel() error { 351 | request, err := http.NewRequest("POST", s.service.apiBase+"/subscriptions/"+s.ID+"/cancel", nil) 352 | if err != nil { 353 | return err 354 | } 355 | request.Header.Add("Authorization", s.service.apiKey) 356 | body, err := respToBody(s.service.Client.Do(request)) 357 | if err != nil { 358 | return err 359 | } 360 | _, err = parseSubscription(s.service, body, s) 361 | return err 362 | } 363 | 364 | // Delete は定期課金をすぐに削除します。次回以降の課金は行われずに、一度削除した定期課金は、 365 | // 再び戻すことができません。 366 | func (s *SubscriptionResponse) Delete() error { 367 | request, err := http.NewRequest("DELETE", s.service.apiBase+"/subscriptions/"+s.ID, nil) 368 | if err != nil { 369 | return err 370 | } 371 | request.Header.Add("Authorization", s.service.apiKey) 372 | _, err = parseResponseError(s.service.Client.Do(request)) 373 | return err 374 | } 375 | 376 | // UnmarshalJSON はJSONパース用の内部APIです。 377 | func (s *SubscriptionResponse) UnmarshalJSON(b []byte) error { 378 | raw := subscriptionResponseParser{} 379 | err := json.Unmarshal(b, &raw) 380 | if err == nil && raw.Object == "subscription" { 381 | s.CanceledAt = time.Unix(int64(raw.CanceledEpoch), 0) 382 | s.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 383 | s.CurrentPeriodEndAt = time.Unix(int64(raw.CurrentPeriodEndEpoch), 0) 384 | s.CurrentPeriodStartAt = time.Unix(int64(raw.CurrentPeriodStartEpoch), 0) 385 | s.CustomerID = raw.Customer 386 | s.ID = raw.ID 387 | s.LiveMode = raw.LiveMode 388 | s.PausedAt = time.Unix(int64(raw.PausedEpoch), 0) 389 | json.Unmarshal(raw.Plan, &s.Plan) 390 | json.Unmarshal(raw.NextCyclePlan, &s.NextCyclePlan) 391 | s.Prorate = raw.Prorate 392 | s.ResumedAt = time.Unix(int64(raw.ResumedEpoch), 0) 393 | s.StartAt = time.Unix(int64(raw.StartEpoch), 0) 394 | switch raw.Status { 395 | case "active": 396 | s.Status = SubscriptionActive 397 | case "trial": 398 | s.Status = SubscriptionTrial 399 | case "canceled": 400 | s.Status = SubscriptionCanceled 401 | case "paused": 402 | s.Status = SubscriptionPaused 403 | } 404 | s.TrialEndAt = time.Unix(int64(raw.TrialEndEpoch), 0) 405 | s.TrialStartAt = time.Unix(int64(raw.TrialStartEpoch), 0) 406 | s.Metadata = raw.Metadata 407 | return nil 408 | } 409 | rawError := errorResponse{} 410 | err = json.Unmarshal(b, &rawError) 411 | if err == nil && rawError.Error.Status != 0 { 412 | return &rawError.Error 413 | } 414 | 415 | return nil 416 | } 417 | 418 | // SubscriptionListCaller はリスト取得に使用する構造体です。 419 | type SubscriptionListCaller struct { 420 | service *Service 421 | customerID string 422 | limit int 423 | offset int 424 | since int 425 | until int 426 | planID string 427 | } 428 | 429 | // Limit はリストの要素数の最大値を設定します(1-100) 430 | func (c *SubscriptionListCaller) Limit(limit int) *SubscriptionListCaller { 431 | c.limit = limit 432 | return c 433 | } 434 | 435 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 436 | func (c *SubscriptionListCaller) Offset(offset int) *SubscriptionListCaller { 437 | c.offset = offset 438 | return c 439 | } 440 | 441 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 442 | func (c *SubscriptionListCaller) Since(since time.Time) *SubscriptionListCaller { 443 | c.since = int(since.Unix()) 444 | return c 445 | } 446 | 447 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 448 | func (c *SubscriptionListCaller) Until(until time.Time) *SubscriptionListCaller { 449 | c.until = int(until.Unix()) 450 | return c 451 | } 452 | 453 | // PlanID はプランIDで結果を絞ります 454 | func (c *SubscriptionListCaller) PlanID(planID string) *SubscriptionListCaller { 455 | c.planID = planID 456 | return c 457 | } 458 | 459 | // Do は指定されたクエリーを元に顧客のリストを配列で取得します。 460 | func (c *SubscriptionListCaller) Do() ([]*SubscriptionResponse, bool, error) { 461 | var url string 462 | if c.customerID == "" { 463 | url = "/subscriptions" 464 | } else { 465 | url = "/customers/" + c.customerID + "/subscriptions" 466 | } 467 | body, err := c.service.queryList(url, c.limit, c.offset, c.since, c.until) 468 | if err != nil { 469 | return nil, false, err 470 | } 471 | raw := &listResponseParser{} 472 | err = json.Unmarshal(body, raw) 473 | if err != nil { 474 | return nil, false, err 475 | } 476 | result := make([]*SubscriptionResponse, len(raw.Data)) 477 | for i, rawSubscription := range raw.Data { 478 | subscription := &SubscriptionResponse{} 479 | json.Unmarshal(rawSubscription, subscription) 480 | result[i] = subscription 481 | } 482 | return result, raw.HasMore, nil 483 | } 484 | -------------------------------------------------------------------------------- /v1/subscription_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var subscriptionResponseJSON = []byte(` 10 | { 11 | "canceled_at": null, 12 | "created": 1433127983, 13 | "current_period_end": 1435732422, 14 | "current_period_start": 1433140422, 15 | "customer": "cus_4df4b5ed720933f4fb9e28857517", 16 | "id": "sub_567a1e44562932ec1a7682d746e0", 17 | "livemode": false, 18 | "object": "subscription", 19 | "paused_at": null, 20 | "next_cycle_plan": { 21 | "amount": 1000, 22 | "billing_day": null, 23 | "created": 1432965398, 24 | "currency": "jpy", 25 | "id": "next_plan", 26 | "interval": "month", 27 | "name": "next plan", 28 | "object": "plan", 29 | "metadata": {}, 30 | "trial_days": 0 31 | }, 32 | "plan": { 33 | "amount": 1000, 34 | "billing_day": null, 35 | "created": 1432965397, 36 | "currency": "jpy", 37 | "id": "pln_9589006d14aad86aafeceac06b60", 38 | "interval": "month", 39 | "name": "test plan", 40 | "object": "plan", 41 | "metadata": {}, 42 | "trial_days": 0 43 | }, 44 | "resumed_at": null, 45 | "start": 1433140422, 46 | "status": "active", 47 | "trial_end": null, 48 | "trial_start": null, 49 | "metadata": {}, 50 | "prorate": false 51 | } 52 | `) 53 | 54 | var nextCyclePlanNullResponseJSON = []byte(` 55 | { 56 | "canceled_at": null, 57 | "created": 1433127983, 58 | "current_period_end": 1435732422, 59 | "current_period_start": 1433140422, 60 | "customer": "cus_4df4b5ed720933f4fb9e28857517", 61 | "id": "sub_567a1e44562932ec1a7682d746e0", 62 | "livemode": false, 63 | "object": "subscription", 64 | "paused_at": null, 65 | "next_cycle_plan": null, 66 | "plan": { 67 | "amount": 1000, 68 | "billing_day": null, 69 | "created": 1432965397, 70 | "currency": "jpy", 71 | "id": "pln_9589006d14aad86aafeceac06b60", 72 | "interval": "month", 73 | "name": "test plan", 74 | "object": "plan", 75 | "metadata": {}, 76 | "trial_days": 0 77 | }, 78 | "resumed_at": null, 79 | "start": 1433140422, 80 | "status": "active", 81 | "trial_end": null, 82 | "trial_start": null, 83 | "metadata": {}, 84 | "prorate": false 85 | } 86 | `) 87 | 88 | var subscriptionListResponseJSON = []byte(` 89 | { 90 | "count": 1, 91 | "data": [ 92 | { 93 | "canceled_at": null, 94 | "created": 1433127983, 95 | "current_period_end": 1435732422, 96 | "current_period_start": 1433140422, 97 | "customer": "cus_4df4b5ed720933f4fb9e28857517", 98 | "id": "sub_567a1e44562932ec1a7682d746e0", 99 | "livemode": false, 100 | "object": "subscription", 101 | "paused_at": null, 102 | "next_cycle_plan": null, 103 | "plan": { 104 | "amount": 1000, 105 | "billing_day": null, 106 | "created": 1432965397, 107 | "currency": "jpy", 108 | "id": "pln_9589006d14aad86aafeceac06b60", 109 | "interval": "month", 110 | "name": "test plan", 111 | "object": "plan", 112 | "metadata": {}, 113 | "trial_days": 0 114 | }, 115 | "resumed_at": null, 116 | "start": 1433140422, 117 | "status": "active", 118 | "trial_end": null, 119 | "trial_start": null, 120 | "metadata": {}, 121 | "prorate": false 122 | } 123 | ], 124 | "has_more": true, 125 | "object": "list", 126 | "url": "/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions" 127 | } 128 | `) 129 | 130 | func TestParseSubscriptionResponseJSON(t *testing.T) { 131 | subscription := &SubscriptionResponse{} 132 | err := json.Unmarshal(subscriptionResponseJSON, subscription) 133 | 134 | if err != nil { 135 | t.Errorf("err should be nil, but %v", err) 136 | } 137 | if subscription.ID != "sub_567a1e44562932ec1a7682d746e0" { 138 | t.Errorf("subscription.ID should be 'sub_567a1e44562932ec1a7682d746e0', but '%s'", subscription.ID) 139 | } 140 | if subscription.Plan.Amount != 1000 { 141 | t.Errorf("subscription.Plan.Amount should be 1000 but %d", subscription.Plan.Amount) 142 | } 143 | if subscription.NextCyclePlan.ID != "next_plan" { 144 | t.Errorf("subscription.NextCyclePlan.ID is invalid. got: '%s'", subscription.NextCyclePlan.ID) 145 | } 146 | if subscription.NextCyclePlan.Currency != "jpy" { 147 | t.Errorf("subscription.NextCyclePlan.Currency is invalid. got: '%s'", subscription.NextCyclePlan.Currency) 148 | } 149 | if subscription.NextCyclePlan.Interval != "month" { 150 | t.Errorf("subscription.NextCyclePlan.Interval is invalid. got: '%s'", subscription.NextCyclePlan.Interval) 151 | } 152 | if subscription.NextCyclePlan.Name != "next plan" { 153 | t.Errorf("subscription.NextCyclePlan.Name is invalid. got: '%s'", subscription.NextCyclePlan.Name) 154 | } 155 | if subscription.NextCyclePlan.TrialDays != 0 { 156 | t.Errorf("subscription.NextCyclePlan.TrialDays is invalid. got: '%d'", subscription.NextCyclePlan.TrialDays) 157 | } 158 | if len(subscription.NextCyclePlan.Metadata) != 0 { 159 | t.Errorf("The length of subscription.NextCyclePlan.Metadata is invalid") 160 | } 161 | } 162 | 163 | func TestCustomerGetSubscription(t *testing.T) { 164 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 165 | service := New("api-key", mock) 166 | subscription, err := service.Customer.GetSubscription("cus_121673955bd7aa144de5a8f6c262", "sub_567a1e44562932ec1a7682d746e0") 167 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions/sub_567a1e44562932ec1a7682d746e0" { 168 | t.Errorf("URL is wrong: %s", transport.URL) 169 | } 170 | if transport.Method != "GET" { 171 | t.Errorf("Method should be GET, but %s", transport.Method) 172 | } 173 | if err != nil { 174 | t.Errorf("err should be nil, but %v", err) 175 | return 176 | } else if subscription == nil { 177 | t.Error("subscription should not be nil") 178 | } else if subscription.Plan.Amount != 1000 { 179 | t.Errorf("subscription.Plan.Amount should be 1000 but %d", subscription.Plan.Amount) 180 | } 181 | } 182 | 183 | func TestCustomerListSubscription(t *testing.T) { 184 | mock, transport := NewMockClient(200, subscriptionListResponseJSON) 185 | service := New("api-key", mock) 186 | subscriptions, hasMore, err := service.Customer.ListSubscription("cus_121673955bd7aa144de5a8f6c262"). 187 | Limit(10). 188 | Offset(15). 189 | Since(time.Unix(1455328095, 0)). 190 | Until(time.Unix(1455500895, 0)).Do() 191 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions?limit=10&offset=15&since=1455328095&until=1455500895" { 192 | t.Errorf("URL is wrong: %s", transport.URL) 193 | } 194 | if transport.Method != "GET" { 195 | t.Errorf("Method should be GET, but %s", transport.Method) 196 | } 197 | if err != nil { 198 | t.Errorf("err should be nil, but %v", err) 199 | return 200 | } 201 | if !hasMore { 202 | t.Error("parse error: hasMore") 203 | } 204 | for i, subscription := range subscriptions { 205 | if i != 0 { 206 | t.Error("parse error: List length") 207 | } 208 | if subscription.NextCyclePlan != nil { 209 | t.Error("parse error: next_cycle_plan") 210 | } 211 | } 212 | } 213 | 214 | func TestSubscriptionCreate(t *testing.T) { 215 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 216 | service := New("api-key", mock) 217 | subscription, err := service.Subscription.Subscribe("cus_4df4b5ed720933f4fb9e28857517", Subscription{ 218 | PlanID: "pln_9589006d14aad86aafeceac06b60", 219 | }) 220 | if transport.URL != "https://api.pay.jp/v1/subscriptions" { 221 | t.Errorf("URL is wrong: %s", transport.URL) 222 | } 223 | if transport.Method != "POST" { 224 | t.Errorf("Method should be POST, but %s", transport.Method) 225 | } 226 | if err != nil { 227 | t.Errorf("err should be nil, but %v", err) 228 | return 229 | } 230 | if subscription == nil { 231 | t.Error("subscription should not be nil") 232 | } else if subscription.Plan.ID != "pln_9589006d14aad86aafeceac06b60" { 233 | t.Errorf("subscription.Plan.ID is wrong: %s.", subscription.Plan.ID) 234 | } 235 | } 236 | 237 | func TestSubscriptionRetrieve(t *testing.T) { 238 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 239 | service := New("api-key", mock) 240 | subscription, err := service.Subscription.Retrieve("cus_121673955bd7aa144de5a8f6c262", "sub_567a1e44562932ec1a7682d746e0") 241 | if transport.URL != "https://api.pay.jp/v1/customers/cus_121673955bd7aa144de5a8f6c262/subscriptions/sub_567a1e44562932ec1a7682d746e0" { 242 | t.Errorf("URL is wrong: %s", transport.URL) 243 | } 244 | if transport.Method != "GET" { 245 | t.Errorf("Method should be GET, but %s", transport.Method) 246 | } 247 | if err != nil { 248 | t.Errorf("err should be nil, but %v", err) 249 | return 250 | } else if subscription == nil { 251 | t.Error("subscription should not be nil") 252 | } else if subscription.Plan.Amount != 1000 { 253 | t.Errorf("subscription.Plan.Amount should be 1000 but %d", subscription.Plan.Amount) 254 | } 255 | } 256 | 257 | func TestSubscriptionUpdate(t *testing.T) { 258 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 259 | service := New("api-key", mock) 260 | subscription, err := service.Subscription.Update("sub_567a1e44562932ec1a7682d746e0", Subscription{ 261 | PlanID: "pln_9589006d14aad86aafeceac06b60", 262 | NextCyclePlanID: "next_plan", 263 | }) 264 | if transport.URL != "https://api.pay.jp/v1/subscriptions/sub_567a1e44562932ec1a7682d746e0" { 265 | t.Errorf("URL is wrong: %s", transport.URL) 266 | } 267 | if transport.Method != "POST" { 268 | t.Errorf("Method should be POST, but %s", transport.Method) 269 | } 270 | if err != nil { 271 | t.Errorf("err should be nil, but %v", err) 272 | return 273 | } 274 | if subscription == nil { 275 | t.Error("subscription should not be nil") 276 | } else if subscription.Plan.ID != "pln_9589006d14aad86aafeceac06b60" { 277 | t.Errorf("subscription.Plan.ID is wrong: %s.", subscription.Plan.ID) 278 | } else if subscription.NextCyclePlan.ID != "next_plan" { 279 | t.Errorf("subscription.NextCyclePlan.ID is wrong: %s.", subscription.NextCyclePlan.ID) 280 | } 281 | 282 | mock2, transport2 := NewMockClient(200, nextCyclePlanNullResponseJSON) 283 | service2 := New("api-key", mock2) 284 | newSubscr, err := service2.Subscription.Update("sub_567a1e44562932ec1a7682d746e0", Subscription{ 285 | NextCyclePlanID: "", 286 | }) 287 | if transport2.Method != "POST" { 288 | t.Errorf("Method should be POST, but %s", transport2.Method) 289 | } 290 | if newSubscr.NextCyclePlan != nil { 291 | t.Errorf("subscription.NextCyclePlan is not nil") 292 | } 293 | } 294 | 295 | func TestSubscriptionPause(t *testing.T) { 296 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 297 | service := New("api-key", mock) 298 | subscription, err := service.Subscription.Pause("sub_567a1e44562932ec1a7682d746e0") 299 | if transport.URL != "https://api.pay.jp/v1/subscriptions/sub_567a1e44562932ec1a7682d746e0/pause" { 300 | t.Errorf("URL is wrong: %s", transport.URL) 301 | } 302 | if transport.Method != "POST" { 303 | t.Errorf("Method should be POST, but %s", transport.Method) 304 | } 305 | if err != nil { 306 | t.Errorf("err should be nil, but %v", err) 307 | return 308 | } 309 | if subscription == nil { 310 | t.Error("subscription should not be nil") 311 | } else if subscription.Plan.ID != "pln_9589006d14aad86aafeceac06b60" { 312 | t.Errorf("subscription.Plan.ID is wrong: %s.", subscription.Plan.ID) 313 | } 314 | } 315 | 316 | func TestSubscriptionResume(t *testing.T) { 317 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 318 | service := New("api-key", mock) 319 | subscription, err := service.Subscription.Resume("sub_567a1e44562932ec1a7682d746e0", Subscription{}) 320 | if transport.URL != "https://api.pay.jp/v1/subscriptions/sub_567a1e44562932ec1a7682d746e0/resume" { 321 | t.Errorf("URL is wrong: %s", transport.URL) 322 | } 323 | if transport.Method != "POST" { 324 | t.Errorf("Method should be POST, but %s", transport.Method) 325 | } 326 | if err != nil { 327 | t.Errorf("err should be nil, but %v", err) 328 | return 329 | } 330 | if subscription == nil { 331 | t.Error("subscription should not be nil") 332 | } else if subscription.Plan.ID != "pln_9589006d14aad86aafeceac06b60" { 333 | t.Errorf("subscription.Plan.ID is wrong: %s.", subscription.Plan.ID) 334 | } 335 | } 336 | 337 | func TestSubscriptionCancel(t *testing.T) { 338 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 339 | service := New("api-key", mock) 340 | subscription, err := service.Subscription.Cancel("sub_567a1e44562932ec1a7682d746e0") 341 | if transport.URL != "https://api.pay.jp/v1/subscriptions/sub_567a1e44562932ec1a7682d746e0/cancel" { 342 | t.Errorf("URL is wrong: %s", transport.URL) 343 | } 344 | if transport.Method != "POST" { 345 | t.Errorf("Method should be POST, but %s", transport.Method) 346 | } 347 | if err != nil { 348 | t.Errorf("err should be nil, but %v", err) 349 | return 350 | } 351 | if subscription == nil { 352 | t.Error("subscription should not be nil") 353 | } else if subscription.Plan.ID != "pln_9589006d14aad86aafeceac06b60" { 354 | t.Errorf("subscription.Plan.ID is wrong: %s.", subscription.Plan.ID) 355 | } 356 | } 357 | 358 | func TestSubscriptionDelete(t *testing.T) { 359 | mock, transport := NewMockClient(200, subscriptionResponseJSON) 360 | service := New("api-key", mock) 361 | err := service.Subscription.Delete("sub_567a1e44562932ec1a7682d746e0") 362 | if transport.URL != "https://api.pay.jp/v1/subscriptions/sub_567a1e44562932ec1a7682d746e0" { 363 | t.Errorf("URL is wrong: %s", transport.URL) 364 | } 365 | if transport.Method != "DELETE" { 366 | t.Errorf("Method should be DELETE, but %s", transport.Method) 367 | } 368 | if err != nil { 369 | t.Errorf("err should be nil, but %v", err) 370 | return 371 | } 372 | } 373 | 374 | func TestSubscriptionList(t *testing.T) { 375 | mock, transport := NewMockClient(200, subscriptionListResponseJSON) 376 | service := New("api-key", mock) 377 | subscriptions, hasMore, err := service.Subscription.List(). 378 | Limit(10). 379 | Offset(15). 380 | Since(time.Unix(1455328095, 0)). 381 | Until(time.Unix(1455500895, 0)).Do() 382 | if transport.URL != "https://api.pay.jp/v1/subscriptions?limit=10&offset=15&since=1455328095&until=1455500895" { 383 | t.Errorf("URL is wrong: %s", transport.URL) 384 | } 385 | if transport.Method != "GET" { 386 | t.Errorf("Method should be GET, but %s", transport.Method) 387 | } 388 | if err != nil { 389 | t.Errorf("err should be nil, but %v", err) 390 | return 391 | } 392 | if !hasMore { 393 | t.Error("parse error: hasMore") 394 | } 395 | if len(subscriptions) != 1 { 396 | t.Error("parse error: plans") 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /v1/token.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // TokenService はカード情報を代替するトークンオブジェクトを扱います。 12 | // 13 | // トークンは、カード番号やCVCなどのセキュアなデータを隠しつつも、カードと同じように扱うことができます。 14 | // 15 | // 顧客にカードを登録するときや、支払い処理を行うときにカード代わりとして使用します。 16 | // 17 | // 一度使用したトークンは再び使用することはできませんが、 18 | // 顧客にカードを登録すれば、顧客IDを支払い手段として用いることで、 19 | // 何度でも同じカードで支払い処理ができるようになります。 20 | type TokenService struct { 21 | service *Service 22 | } 23 | 24 | func newTokenService(service *Service) *TokenService { 25 | return &TokenService{ 26 | service: service, 27 | } 28 | } 29 | 30 | func parseToken(data []byte, err error) (*TokenResponse, error) { 31 | if err != nil { 32 | return nil, err 33 | } 34 | result := &TokenResponse{} 35 | err = json.Unmarshal(data, result) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return result, nil 40 | } 41 | 42 | // Create メソッドカード情報を指定して、トークンを生成します。 43 | // 44 | // トークンはサーバーサイドからのリクエストでも生成可能ですが、通常は チェックアウトや 45 | // payjp.js を利用して、ブラウザ経由でパブリックキーとカード情報を指定して生成します。 46 | // トークンは二度以上使用することができません。 47 | // 48 | // チェックアウトやpayjp.jsを使ったトークン化の実装方法については チュートリアル - 49 | // カード情報のトークン化(https://pay.jp/docs/cardtoken)をご覧ください。 50 | // 51 | // Card構造体で引数を設定しますが、Number/ExpMonth/ExpYearが必須パラメータです。 52 | func (t TokenService) Create(card Card) (*TokenResponse, error) { 53 | var errors []string 54 | if card.Number == nil { 55 | errors = append(errors, "Number is required") 56 | } 57 | if card.ExpMonth == nil { 58 | errors = append(errors, "ExpMonth is required") 59 | } 60 | if card.ExpYear == nil { 61 | errors = append(errors, "ExpYear is required") 62 | } 63 | if len(errors) != 0 { 64 | return nil, fmt.Errorf("payjp.Token.Create() parameter error: %s", strings.Join(errors, ", ")) 65 | } 66 | qb := newRequestBuilder() 67 | qb.AddCard(card) 68 | 69 | request, err := http.NewRequest("POST", t.service.apiBase+"/tokens", qb.Reader()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 74 | request.Header.Add("Authorization", t.service.apiKey) 75 | 76 | return parseToken(respToBody(t.service.Client.Do(request))) 77 | } 78 | 79 | // Retrieve token object. 特定のトークン情報を取得します。 80 | func (t TokenService) Retrieve(id string) (*TokenResponse, error) { 81 | return parseToken(t.service.retrieve("/tokens/" + id)) 82 | } 83 | 84 | // TokenResponse はToken.Create(), Token.Retrieve()が返す構造体です。 85 | type TokenResponse struct { 86 | Card CardResponse // クレジットカードの情報 87 | CreatedAt time.Time // このトークン作成時間 88 | ID string // tok_で始まる一意なオブジェクトを示す文字列 89 | LiveMode bool // 本番環境かどうか 90 | Used bool // トークンが使用済みかどうか 91 | } 92 | 93 | type tokenResponseParser struct { 94 | Card json.RawMessage `json:"card"` 95 | CreatedEpoch int `json:"created"` 96 | ID string `json:"id"` 97 | LiveMode bool `json:"livemode"` 98 | Object string `json:"object"` 99 | Used bool `json:"used"` 100 | CreatedAt time.Time 101 | } 102 | 103 | // UnmarshalJSON はJSONパース用の内部APIです。 104 | func (t *TokenResponse) UnmarshalJSON(b []byte) error { 105 | raw := tokenResponseParser{} 106 | err := json.Unmarshal(b, &raw) 107 | if err == nil && raw.Object == "token" { 108 | json.Unmarshal(raw.Card, &t.Card) 109 | t.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 110 | t.ID = raw.ID 111 | t.LiveMode = raw.LiveMode 112 | t.Used = raw.Used 113 | return nil 114 | } 115 | rawError := errorResponse{} 116 | err = json.Unmarshal(b, &rawError) 117 | if err == nil && rawError.Error.Status != 0 { 118 | return &rawError.Error 119 | } 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /v1/token_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | var tokenResponseJSON = []byte(` 9 | { 10 | "card": { 11 | "address_city": null, 12 | "address_line1": null, 13 | "address_line2": null, 14 | "address_state": null, 15 | "address_zip": null, 16 | "address_zip_check": "unchecked", 17 | "brand": "Visa", 18 | "country": null, 19 | "created": 1442290383, 20 | "customer": null, 21 | "cvc_check": "passed", 22 | "exp_month": 2, 23 | "exp_year": 2020, 24 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 25 | "id": "car_e3ccd4e0959f45e7c75bacc4be90", 26 | "last4": "4242", 27 | "name": null, 28 | "object": "card" 29 | }, 30 | "created": 1442290383, 31 | "id": "tok_5ca06b51685e001723a2c3b4aeb4", 32 | "livemode": false, 33 | "object": "token", 34 | "used": false 35 | } 36 | `) 37 | 38 | func TestParseTokenResponseJSON(t *testing.T) { 39 | token := &TokenResponse{} 40 | err := json.Unmarshal(tokenResponseJSON, token) 41 | 42 | if err != nil { 43 | t.Errorf("err should be nil, but %v", err) 44 | } 45 | if token.ID != "tok_5ca06b51685e001723a2c3b4aeb4" { 46 | t.Errorf("token.Id should be 'tok_5ca06b51685e001723a2c3b4aeb4', but '%s'", token.ID) 47 | } 48 | } 49 | 50 | func TestTokenCreate(t *testing.T) { 51 | mock, transport := NewMockClient(200, tokenResponseJSON) 52 | service := New("api-key", mock) 53 | token, err := service.Token.Create(Card{ 54 | Number: "4242424242424242", 55 | ExpMonth: 2, 56 | ExpYear: 2020, 57 | }) 58 | if transport.URL != "https://api.pay.jp/v1/tokens" { 59 | t.Errorf("URL is wrong: %s", transport.URL) 60 | } 61 | if transport.Method != "POST" { 62 | t.Errorf("Method should be POST, but %s", transport.Method) 63 | } 64 | if err != nil { 65 | t.Errorf("err should be nil, but %v", err) 66 | return 67 | } 68 | if token == nil { 69 | t.Error("plan should not be nil") 70 | } else if token.Card.ExpYear != 2020 { 71 | t.Errorf("token.Card.ExpYear should be 2020, but %d.", token.Card.ExpYear) 72 | } 73 | } 74 | 75 | func TestTokenRetrieve(t *testing.T) { 76 | mock, transport := NewMockClient(200, tokenResponseJSON) 77 | service := New("api-key", mock) 78 | token, err := service.Token.Retrieve("tok_5ca06b51685e001723a2c3b4aeb4") 79 | if transport.URL != "https://api.pay.jp/v1/tokens/tok_5ca06b51685e001723a2c3b4aeb4" { 80 | t.Errorf("URL is wrong: %s", transport.URL) 81 | } 82 | if transport.Method != "GET" { 83 | t.Errorf("Method should be GET, but %s", transport.Method) 84 | } 85 | if err != nil { 86 | t.Errorf("err should be nil, but %v", err) 87 | return 88 | } else if token == nil { 89 | t.Error("plan should not be nil") 90 | } else if token.ID != "tok_5ca06b51685e001723a2c3b4aeb4" { 91 | t.Errorf("parse error: plan.Amount should be tok_5ca06b51685e001723a2c3b4aeb4, but %s.", token.ID) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /v1/transfer.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | "time" 7 | ) 8 | 9 | // TransferStatus は入金状態を示すステータスです 10 | type TransferStatus int 11 | 12 | const ( 13 | noTransferStatus TransferStatus = iota 14 | // TransferPending は支払い前のステータスを表す定数 15 | TransferPending 16 | // TransferPaid は支払い済みのステータスを表す定数 17 | TransferPaid 18 | // TransferFailed は支払い失敗のステータスを表す定数 19 | TransferFailed 20 | // TransferRecombination は組戻ステータスを表す定数 21 | TransferRecombination 22 | // TransferCarriedOver は入金繰り越しを表す定数 23 | TransferCarriedOver 24 | // TransferStop は入金停止を表す定数 25 | TransferStop 26 | ) 27 | 28 | func (t TransferStatus) status() interface{} { 29 | switch t { 30 | case TransferPending: 31 | return "pending" 32 | case TransferPaid: 33 | return "paid" 34 | case TransferFailed: 35 | return "failed" 36 | case TransferRecombination: 37 | return "recombination" 38 | case TransferCarriedOver: 39 | return "carried_over" 40 | case TransferStop: 41 | return "stop" 42 | } 43 | return nil 44 | } 45 | 46 | // TransferService は入金に関するサービスです。 47 | // 48 | // 入金は毎月15日と月末に締め、翌月15日と月末に入金されます。入金は、締め日までのデータがそれぞれ生成されます。 49 | type TransferService struct { 50 | service *Service 51 | } 52 | 53 | func newTransferService(service *Service) *TransferService { 54 | return &TransferService{ 55 | service: service, 56 | } 57 | } 58 | 59 | // Retrieve transfer object. 入金情報を取得します。 60 | func (t TransferService) Retrieve(transferID string) (*TransferResponse, error) { 61 | body, err := t.service.retrieve("/transfers/" + transferID) 62 | if err != nil { 63 | return nil, err 64 | } 65 | result := &TransferResponse{} 66 | err = json.Unmarshal(body, result) 67 | if err != nil { 68 | return nil, err 69 | } 70 | result.service = t.service 71 | return result, nil 72 | } 73 | 74 | // List は入金リストを取得します。リストは、直近で生成された順番に取得されます。 75 | func (t TransferService) List() *TransferListCaller { 76 | return &TransferListCaller{ 77 | status: noTransferStatus, 78 | service: t.service, 79 | } 80 | } 81 | 82 | // TransferListCaller は支払いのリスト取得に使用する構造体です。 83 | type TransferListCaller struct { 84 | service *Service 85 | limit int 86 | offset int 87 | since int 88 | until int 89 | sinceSheduledDate int 90 | untilSheduledDate int 91 | status TransferStatus 92 | } 93 | 94 | // Limit はリストの要素数の最大値を設定します(1-100) 95 | func (c *TransferListCaller) Limit(limit int) *TransferListCaller { 96 | c.limit = limit 97 | return c 98 | } 99 | 100 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 101 | func (c *TransferListCaller) Offset(offset int) *TransferListCaller { 102 | c.offset = offset 103 | return c 104 | } 105 | 106 | // SinceSheduledDate は入金予定日がここに指定したタイムスタンプ以降のデータのみ取得します 107 | func (c *TransferListCaller) SinceSheduledDate(sinceSheduledDate time.Time) *TransferListCaller { 108 | c.sinceSheduledDate = int(sinceSheduledDate.Unix()) 109 | return c 110 | } 111 | 112 | // UntilSheduledDate は入金予定日がここに指定したタイムスタンプ以前のデータのみ取得します 113 | func (c *TransferListCaller) UntilSheduledDate(untilSheduledDate time.Time) *TransferListCaller { 114 | c.untilSheduledDate = int(untilSheduledDate.Unix()) 115 | return c 116 | } 117 | 118 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 119 | func (c *TransferListCaller) Since(since time.Time) *TransferListCaller { 120 | c.since = int(since.Unix()) 121 | return c 122 | } 123 | 124 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 125 | func (c *TransferListCaller) Until(until time.Time) *TransferListCaller { 126 | c.until = int(until.Unix()) 127 | return c 128 | } 129 | 130 | // Status はここで指定されたステータスのデータを取得します 131 | func (c *TransferListCaller) Status(status TransferStatus) *TransferListCaller { 132 | c.status = status 133 | return c 134 | } 135 | 136 | // Do は指定されたクエリーを元に入金のリストを配列で取得します。 137 | func (c *TransferListCaller) Do() ([]*TransferResponse, bool, error) { 138 | body, err := c.service.queryTransferList("/transfers", c.limit, c.offset, c.since, c.until, c.sinceSheduledDate, c.untilSheduledDate, func(values *url.Values) bool { 139 | if c.status != noTransferStatus { 140 | values.Add("status", c.status.status().(string)) 141 | return true 142 | } 143 | return false 144 | }) 145 | if err != nil { 146 | return nil, false, err 147 | } 148 | raw := &listResponseParser{} 149 | err = json.Unmarshal(body, raw) 150 | if err != nil { 151 | return nil, false, err 152 | } 153 | result := make([]*TransferResponse, len(raw.Data)) 154 | for i, rawCharge := range raw.Data { 155 | charge := &TransferResponse{} 156 | json.Unmarshal(rawCharge, charge) 157 | charge.service = c.service 158 | result[i] = charge 159 | } 160 | return result, raw.HasMore, nil 161 | } 162 | 163 | // ChargeList は支払いは入金内訳リストを取得します。リストは、直近で生成された順番に取得されます。 164 | func (t TransferService) ChargeList(transferID string) *TransferChargeListCaller { 165 | return &TransferChargeListCaller{ 166 | service: t.service, 167 | transferID: transferID, 168 | } 169 | } 170 | 171 | // TransferChargeListCaller は入金内訳のリスト取得に使用する構造体です。 172 | type TransferChargeListCaller struct { 173 | service *Service 174 | transferID string 175 | limit int 176 | offset int 177 | since int 178 | until int 179 | customerID string 180 | } 181 | 182 | // Limit はリストの要素数の最大値を設定します(1-100) 183 | func (c *TransferChargeListCaller) Limit(limit int) *TransferChargeListCaller { 184 | c.limit = limit 185 | return c 186 | } 187 | 188 | // Offset は取得するリストの先頭要素のインデックスのオフセットを設定します 189 | func (c *TransferChargeListCaller) Offset(offset int) *TransferChargeListCaller { 190 | c.offset = offset 191 | return c 192 | } 193 | 194 | // Since はここに指定したタイムスタンプ以降に作成されたデータを取得します 195 | func (c *TransferChargeListCaller) Since(since time.Time) *TransferChargeListCaller { 196 | c.since = int(since.Unix()) 197 | return c 198 | } 199 | 200 | // Until はここに指定したタイムスタンプ以前に作成されたデータを取得します 201 | func (c *TransferChargeListCaller) Until(until time.Time) *TransferChargeListCaller { 202 | c.until = int(until.Unix()) 203 | return c 204 | } 205 | 206 | // CustomerID はここに指定した顧客IDを持つデータを取得します 207 | func (c *TransferChargeListCaller) CustomerID(ID string) *TransferChargeListCaller { 208 | c.customerID = ID 209 | return c 210 | } 211 | 212 | // Do は指定されたクエリーを元に入金内訳のリストを配列で取得します。 213 | func (c *TransferChargeListCaller) Do() ([]*ChargeResponse, bool, error) { 214 | path := "/transfers/" + c.transferID + "/charges" 215 | body, err := c.service.queryList(path, c.limit, c.offset, c.since, c.until, func(values *url.Values) bool { 216 | if c.customerID != "" { 217 | values.Add("customer", c.customerID) 218 | return true 219 | } 220 | return false 221 | }) 222 | if err != nil { 223 | return nil, false, err 224 | } 225 | raw := &listResponseParser{} 226 | err = json.Unmarshal(body, raw) 227 | if err != nil { 228 | return nil, false, err 229 | } 230 | result := make([]*ChargeResponse, len(raw.Data)) 231 | for i, rawCharge := range raw.Data { 232 | transfer := &ChargeResponse{} 233 | json.Unmarshal(rawCharge, transfer) 234 | transfer.service = c.service 235 | result[i] = transfer 236 | } 237 | return result, raw.HasMore, nil 238 | } 239 | 240 | // TransferResponse はTransferService.Get、TransferService.Listによって返される、 241 | // 入金状態を示す構造体です。 242 | type TransferResponse struct { 243 | ID string // tr_で始まる一意なオブジェクトを示す文字列 244 | LiveMode bool // 本番環境かどうか 245 | CreatedAt time.Time // この入金作成時のタイムスタンプ 246 | Amount int // 入金予定額 247 | CarriedBalance int // 繰越金 248 | Currency string // 3文字のISOコード(現状 “jpy” のみサポート) 249 | Status TransferStatus // この入金の処理状態 250 | Charges []*ChargeResponse // この入金に含まれる支払いのリスト 251 | ScheduledDate string // 入金予定日 252 | Summary struct { 253 | ChargeCount int // 支払い総数 254 | ChargeFee int // 支払い手数料 255 | ChargeGross int // 総売上 256 | Net int // 差引額 257 | RefundAmount int // 返金総額 258 | RefundCount int // 返金総数 259 | DisputeAmount int // チャージバックにより相殺された金額の合計 260 | DisputeCount int // チャージバック対象となったchargeの個数 261 | } // この入金に関する集計情報 262 | Description string // 概要 263 | TermStartAt time.Time // 集計期間開始時のタイムスタンプ 264 | TermEndAt time.Time // 集計期間終了時のタイムスタンプ 265 | TransferAmount int // 入金額 266 | TransferDate string // 入金日 267 | 268 | service *Service 269 | } 270 | 271 | type transferResponseParser struct { 272 | Amount int `json:"amount"` 273 | CarriedBalance int `json:"carried_balance"` 274 | Charges listResponseParser `json:"charges"` 275 | CreatedEpoch int `json:"created"` 276 | Currency string `json:"currency"` 277 | Description string `json:"description"` 278 | ID string `json:"id"` 279 | LiveMode bool `json:"livemode"` 280 | Object string `json:"object"` 281 | ScheduledDate string `json:"scheduled_date"` 282 | Status string `json:"status"` 283 | Summary struct { 284 | ChargeCount int `json:"charge_count"` 285 | ChargeFee int `json:"charge_fee"` 286 | ChargeGross int `json:"charge_gross"` 287 | Net int `json:"net"` 288 | RefundAmount int `json:"refund_amount"` 289 | RefundCount int `json:"refund_count"` 290 | DisputeAmount int `json:"dispute_amount"` 291 | DisputeCount int `json:"dispute_count"` 292 | } `json:"summary"` 293 | TermEndEpoch int `json:"term_end"` 294 | TermStartEpoch int `json:"term_start"` 295 | TransferAmount int `json:"transfer_amount"` 296 | TransferDate string `json:"transfer_date"` 297 | } 298 | 299 | type transfer TransferResponse 300 | 301 | // UnmarshalJSON はJSONパース用の内部APIです。 302 | func (t *TransferResponse) UnmarshalJSON(b []byte) error { 303 | raw := transferResponseParser{} 304 | err := json.Unmarshal(b, &raw) 305 | if err == nil && raw.Object == "transfer" { 306 | t.Amount = raw.Amount 307 | t.CarriedBalance = raw.CarriedBalance 308 | t.CreatedAt = time.Unix(int64(raw.CreatedEpoch), 0) 309 | t.Currency = raw.Currency 310 | t.Description = raw.Description 311 | t.ID = raw.ID 312 | t.LiveMode = raw.LiveMode 313 | t.ScheduledDate = raw.ScheduledDate 314 | switch raw.Status { 315 | case "pending": 316 | t.Status = TransferPending 317 | case "paid": 318 | t.Status = TransferPaid 319 | case "failed": 320 | t.Status = TransferFailed 321 | case "recombination": 322 | t.Status = TransferRecombination 323 | case "carried_over": 324 | t.Status = TransferCarriedOver 325 | case "stop": 326 | t.Status = TransferStop 327 | } 328 | t.Summary.ChargeCount = raw.Summary.ChargeCount 329 | t.Summary.ChargeFee = raw.Summary.ChargeFee 330 | t.Summary.ChargeGross = raw.Summary.ChargeGross 331 | t.Summary.Net = raw.Summary.Net 332 | t.Summary.RefundAmount = raw.Summary.RefundAmount 333 | t.Summary.RefundCount = raw.Summary.RefundCount 334 | t.Summary.DisputeAmount = raw.Summary.DisputeAmount 335 | t.Summary.DisputeCount = raw.Summary.DisputeCount 336 | t.TermEndAt = time.Unix(int64(raw.TermEndEpoch), 0) 337 | t.TermStartAt = time.Unix(int64(raw.TermStartEpoch), 0) 338 | t.TransferAmount = raw.TransferAmount 339 | t.TransferDate = raw.TransferDate 340 | t.Charges = make([]*ChargeResponse, len(raw.Charges.Data)) 341 | for i, rawCharge := range raw.Charges.Data { 342 | charge := &ChargeResponse{} 343 | json.Unmarshal(rawCharge, charge) 344 | t.Charges[i] = charge 345 | } 346 | 347 | return nil 348 | } 349 | rawError := errorResponse{} 350 | err = json.Unmarshal(b, &rawError) 351 | if err == nil && rawError.Error.Status != 0 { 352 | return &rawError.Error 353 | } 354 | 355 | return nil 356 | } 357 | -------------------------------------------------------------------------------- /v1/transfer_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var chargeListJSONStr = ` 10 | { 11 | "count": 1, 12 | "data": [ 13 | { 14 | "amount": 1000, 15 | "amount_refunded": 0, 16 | "captured": true, 17 | "captured_at": 1441706750, 18 | "card": { 19 | "address_city": null, 20 | "address_line1": null, 21 | "address_line2": null, 22 | "address_state": null, 23 | "address_zip": null, 24 | "address_zip_check": "unchecked", 25 | "brand": "Visa", 26 | "country": null, 27 | "created": 1441706750, 28 | "customer": "cus_b92b879e60f62b532d6756ae12af", 29 | "cvc_check": "unchecked", 30 | "exp_month": 5, 31 | "exp_year": 2018, 32 | "fingerprint": "e1d8225886e3a7211127df751c86787f", 33 | "id": "car_93e59e9a9714134ef639865e2b9e", 34 | "last4": "4242", 35 | "name": null, 36 | "object": "card" 37 | }, 38 | "created": 1441706750, 39 | "currency": "jpy", 40 | "customer": "cus_b92b879e60f62b532d6756ae12af", 41 | "description": null, 42 | "expired_at": null, 43 | "failure_code": null, 44 | "failure_message": null, 45 | "id": "ch_60baaf2dc8f3e35684ebe2031a6e0", 46 | "livemode": false, 47 | "object": "charge", 48 | "paid": true, 49 | "refund_reason": null, 50 | "refunded": false, 51 | "subscription": null 52 | } 53 | ], 54 | "has_more": false, 55 | "object": "list", 56 | "url": "/v1/transfers/tr_8f0c0fe2c9f8a47f9d18f03959ba1/charges" 57 | }` 58 | var transferChargeListResponseJSON = []byte(chargeListJSONStr) 59 | 60 | func makeTransferJSONStr(t TransferStatus) string { 61 | return ` 62 | { 63 | "amount": 1000, 64 | "carried_balance": null, 65 | "charges": ` + chargeListJSONStr + `, 66 | "created": 1438354800, 67 | "currency": "jpy", 68 | "description": null, 69 | "id": "tr_8f0c0fe2c9f8a47f9d18f03959ba1", 70 | "livemode": false, 71 | "object": "transfer", 72 | "scheduled_date": "2015-09-16", 73 | "status": "` + t.status().(string) + `", 74 | "summary": { 75 | "charge_count": 1, 76 | "charge_fee": 0, 77 | "charge_gross": 1000, 78 | "net": 1000, 79 | "refund_amount": 0, 80 | "refund_count": 0, 81 | "dispute_amount": 0, 82 | "dispute_count": 0 83 | }, 84 | "term_end": 1439650800, 85 | "term_start": 1438354800, 86 | "transfer_amount": null, 87 | "transfer_date": null 88 | }` 89 | } 90 | 91 | var transferJSONStr = makeTransferJSONStr(TransferPending) 92 | 93 | var transferResponseJSON = []byte(transferJSONStr) 94 | 95 | var transferListJSONStr = ` 96 | { 97 | "count": 1, 98 | "data": [` + transferJSONStr + 99 | `], 100 | "has_more": false, 101 | "object": "list", 102 | "url": "/v1/transfers" 103 | }` 104 | var transferListResponseJSON = []byte(transferListJSONStr) 105 | 106 | func TestParseTransferResponseJSON(t *testing.T) { 107 | transfer := &TransferResponse{} 108 | err := json.Unmarshal(transferResponseJSON, transfer) 109 | 110 | if err != nil { 111 | t.Errorf("err should be nil, but %v", err) 112 | } 113 | if transfer.ID != "tr_8f0c0fe2c9f8a47f9d18f03959ba1" { 114 | t.Errorf("transfer.ID should be 'tr_8f0c0fe2c9f8a47f9d18f03959ba1', but '%s'", transfer.ID) 115 | } 116 | if transfer.Charges[0].ID != "ch_60baaf2dc8f3e35684ebe2031a6e0" { 117 | t.Errorf("Charge.ID should be 'ch_60baaf2dc8f3e35684ebe2031a6e0', but '%s'", transfer.Charges[0].ID) 118 | } 119 | if transfer.Status.status() != "pending" { 120 | t.Errorf("transfer.Status should be 'pending', but '%s'", transfer.Status.status()) 121 | } 122 | } 123 | 124 | func TestParseTransferStatusResponseJSON(t *testing.T) { 125 | transfer := &TransferResponse{} 126 | 127 | response := []byte(makeTransferJSONStr(TransferRecombination)) 128 | err := json.Unmarshal(response, transfer) 129 | if err != nil && transfer.Status.status() != "recombination" { 130 | t.Errorf("bad value: err=%v,status=%v", err, transfer.Status.status()) 131 | } 132 | 133 | response = []byte(makeTransferJSONStr(TransferCarriedOver)) 134 | err = json.Unmarshal(response, transfer) 135 | if err != nil && transfer.Status.status() != "carried_over" { 136 | t.Errorf("bad value: err=%v,status=%v", err, transfer.Status.status()) 137 | } 138 | 139 | response = []byte(makeTransferJSONStr(TransferStop)) 140 | err = json.Unmarshal(response, transfer) 141 | if err != nil || transfer.Status.status() != "stop" { 142 | t.Errorf("bad value: err=%v,status=%v", err, transfer.Status.status()) 143 | } 144 | } 145 | 146 | func TestTransferRetrieve(t *testing.T) { 147 | mock, transport := NewMockClient(200, transferResponseJSON) 148 | service := New("api-key", mock) 149 | transfer, err := service.Transfer.Retrieve("tr_8f0c0fe2c9f8a47f9d18f03959ba1") 150 | if transport.URL != "https://api.pay.jp/v1/transfers/tr_8f0c0fe2c9f8a47f9d18f03959ba1" { 151 | t.Errorf("URL is wrong: %s", transport.URL) 152 | } 153 | if transport.Method != "GET" { 154 | t.Errorf("Method should be GET, but %s", transport.Method) 155 | } 156 | if err != nil { 157 | t.Errorf("err should be nil, but %v", err) 158 | return 159 | } else if transfer == nil { 160 | t.Error("transfer should not be nil") 161 | } else if transfer.Summary.ChargeGross != 1000 { 162 | t.Errorf("transfer.Summary.ChargeGross should be 1000 but %d", transfer.Summary.ChargeGross) 163 | } 164 | } 165 | 166 | func TestTransferList(t *testing.T) { 167 | mock, transport := NewMockClient(200, transferListResponseJSON) 168 | service := New("api-key", mock) 169 | subscriptions, hasMore, err := service.Transfer.List(). 170 | Limit(10). 171 | Offset(15). 172 | Since(time.Unix(1455328095, 0)). 173 | Until(time.Unix(1455500895, 0)).Do() 174 | if transport.URL != "https://api.pay.jp/v1/transfers?limit=10&offset=15&since=1455328095&until=1455500895" { 175 | t.Errorf("URL is wrong: %s", transport.URL) 176 | } 177 | if transport.Method != "GET" { 178 | t.Errorf("Method should be GET, but %s", transport.Method) 179 | } 180 | if err != nil { 181 | t.Errorf("err should be nil, but %v", err) 182 | return 183 | } 184 | if hasMore { 185 | t.Error("parse error: hasMore") 186 | } 187 | if len(subscriptions) != 1 { 188 | t.Error("parse error: plans") 189 | } 190 | } 191 | 192 | func TestTransferChargeList(t *testing.T) { 193 | mock, transport := NewMockClient(200, transferChargeListResponseJSON) 194 | service := New("api-key", mock) 195 | subscriptions, hasMore, err := service.Transfer.ChargeList("tr_8f0c0fe2c9f8a47f9d18f03959ba1"). 196 | Limit(10). 197 | Offset(15). 198 | Since(time.Unix(1455328095, 0)). 199 | Until(time.Unix(1455500895, 0)). 200 | CustomerID("cus_b92b879e60f62b532d6756ae12af").Do() 201 | if transport.URL != "https://api.pay.jp/v1/transfers/tr_8f0c0fe2c9f8a47f9d18f03959ba1/charges?customer=cus_b92b879e60f62b532d6756ae12af&limit=10&offset=15&since=1455328095&until=1455500895" { 202 | t.Errorf("URL is wrong: %s", transport.URL) 203 | } 204 | if transport.Method != "GET" { 205 | t.Errorf("Method should be GET, but %s", transport.Method) 206 | } 207 | if err != nil { 208 | t.Errorf("err should be nil, but %v", err) 209 | return 210 | } 211 | if hasMore { 212 | t.Error("parse error: hasMore") 213 | } 214 | if len(subscriptions) != 1 { 215 | t.Error("parse error: plans") 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /v1/util.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | ) 12 | 13 | type requestBuilder struct { 14 | buffer *bytes.Buffer 15 | delimiter byte 16 | hasValue bool 17 | } 18 | 19 | func newRequestBuilder() *requestBuilder { 20 | return &requestBuilder{ 21 | buffer: &bytes.Buffer{}, 22 | delimiter: '&', 23 | } 24 | } 25 | 26 | func (qb *requestBuilder) Add(key string, value interface{}) { 27 | if value == nil { 28 | return 29 | } 30 | var valueString string 31 | s, ok := value.(string) 32 | if ok { 33 | valueString = url.QueryEscape(s) 34 | } else { 35 | b, ok := value.(bool) 36 | if ok { 37 | if b { 38 | valueString = "true" 39 | } else { 40 | valueString = "false" 41 | } 42 | } else { 43 | valueString = strconv.Itoa(value.(int)) 44 | } 45 | } 46 | if qb.hasValue { 47 | qb.buffer.WriteByte(qb.delimiter) 48 | } 49 | qb.hasValue = true 50 | qb.buffer.WriteString(key) 51 | qb.buffer.WriteByte('=') 52 | qb.buffer.WriteString(valueString) 53 | } 54 | 55 | func (qb *requestBuilder) AddCard(card Card) { 56 | qb.Add("card[number]", card.Number) 57 | qb.Add("card[exp_month]", card.ExpMonth) 58 | qb.Add("card[exp_year]", card.ExpYear) 59 | qb.Add("card[cvc]", card.CVC) 60 | qb.Add("card[address_state]", card.AddressState) 61 | qb.Add("card[address_city]", card.AddressCity) 62 | qb.Add("card[address_line1]", card.AddressLine1) 63 | qb.Add("card[address_line2]", card.AddressLine2) 64 | qb.Add("card[address_zip]", card.AddressZip) 65 | qb.Add("card[country]", card.Country) 66 | qb.Add("card[name]", card.Name) 67 | qb.AddMetadata(card.Metadata) 68 | } 69 | 70 | func (qb *requestBuilder) AddMetadata(metadata map[string]string) { 71 | for key, value := range metadata { 72 | qb.Add("metadata["+key+"]", value) 73 | } 74 | } 75 | 76 | func (qb *requestBuilder) Reader() io.Reader { 77 | return qb.buffer 78 | } 79 | 80 | func respToBody(resp *http.Response, err error) ([]byte, error) { 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer resp.Body.Close() 85 | return ioutil.ReadAll(resp.Body) 86 | } 87 | 88 | type listResponseParser struct { 89 | Count int `json:"count"` 90 | Data []json.RawMessage `json:"data"` 91 | HasMore bool `json:"has_more"` 92 | Object string `json:"object"` 93 | URL string `json:"url"` 94 | } 95 | 96 | type listParser listResponseParser 97 | 98 | func (p *listResponseParser) UnmarshalJSON(b []byte) error { 99 | raw := listParser{} 100 | err := json.Unmarshal(b, &raw) 101 | if err == nil && raw.Object == "list" { 102 | *p = listResponseParser(raw) 103 | return nil 104 | } 105 | rawError := errorResponse{} 106 | err = json.Unmarshal(b, &rawError) 107 | if err == nil && rawError.Error.Status != 0 { 108 | return &rawError.Error 109 | } 110 | 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /v1/util_test.go: -------------------------------------------------------------------------------- 1 | package payjp 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | func NewMockClient(status int, response []byte) (*http.Client, *MockTransport) { 10 | transport := &MockTransport{ 11 | responses: []*responsePair{&responsePair{status, response}}, 12 | } 13 | return &http.Client{ 14 | Transport: transport, 15 | }, transport 16 | } 17 | 18 | type responsePair struct { 19 | status int 20 | response []byte 21 | } 22 | 23 | type MockTransport struct { 24 | responses []*responsePair 25 | index int 26 | URL string 27 | Method string 28 | } 29 | 30 | // Implement http.RoundTripper 31 | func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 32 | t.URL = req.URL.String() 33 | t.Method = req.Method 34 | // Create mocked http.Response 35 | responseSet := t.responses[t.index] 36 | t.index++ 37 | if t.index == len(t.responses) { 38 | t.index-- 39 | } 40 | response := &http.Response{ 41 | Header: make(http.Header), 42 | Request: req, 43 | StatusCode: responseSet.status, 44 | } 45 | response.Body = ioutil.NopCloser(bytes.NewReader(responseSet.response)) 46 | return response, nil 47 | } 48 | 49 | func (t *MockTransport) AddResponse(status int, body []byte) { 50 | t.responses = append(t.responses, &responsePair{ 51 | status: status, 52 | response: body, 53 | }) 54 | } 55 | --------------------------------------------------------------------------------