From c94bfcfdfa32e2e3ae7bc74d21a27c00bc693740 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Wed, 1 Oct 2014 20:02:45 +0200 Subject: [PATCH 01/20] Added .travis.yml and minor changes in README --- .travis.yml | 11 +++++++++++ README.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0af309f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: go + +go: + - 1.1 + - 1.2 + - 1.3 + - release + - tip + +script: + - go test -v diff --git a/README.md b/README.md index 4e67c69..81ff38a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Coverage Status](https://coveralls.io/repos/fundary/paypal/badge.png)](https://coveralls.io/r/fundary/paypal) [![GoDoc](https://godoc.org/github.com/fundary/paypal?status.svg)](https://godoc.org/github.com/fundary/paypal) -This is a client for the Paypal REST API ([https://developer.paypal.com/webapps/developer/docs/api/](https://developer.paypal.com/webapps/developer/docs/api/) +A Go client for the Paypal REST API ([https://developer.paypal.com/webapps/developer/docs/api/](https://developer.paypal.com/webapps/developer/docs/api/)) ## Goals From 62f7be79210ff8ac309327d360143e1d8f08db93 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Thu, 2 Oct 2014 11:13:32 +0200 Subject: [PATCH 02/20] Added link to Paypal documentation for payment endpoint --- payment.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/payment.go b/payment.go index d52b13f..a46d9d3 100644 --- a/payment.go +++ b/payment.go @@ -2,6 +2,8 @@ package paypal import "fmt" +// https://developer.paypal.com/webapps/developer/docs/api/#payments + type ( CreatePaymentResp struct { *Payment From 7af1e63da366c7dd0aac3ec018ec6d6d0f9d7428 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Thu, 2 Oct 2014 11:18:22 +0200 Subject: [PATCH 03/20] Added installing goconvey before running tests in Travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0af309f..120cae5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,8 @@ go: - release - tip +before_install: + - go get github.com/smartystreets/goconvey + script: - go test -v From 48bebb45ec29f28cf74979e9ab419c1a798dafe0 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Fri, 3 Oct 2014 10:32:12 +0200 Subject: [PATCH 04/20] Added billing plans and agreements endpoints --- billing.go | 121 ++++++++++++++++++++++++ billingagreementtype.go | 200 ++++++++++++++++++++++++++++++++++++++++ billingplantype.go | 131 ++++++++++++++++++++++++++ paymenttype.go | 8 +- 4 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 billing.go create mode 100644 billingagreementtype.go create mode 100644 billingplantype.go diff --git a/billing.go b/billing.go new file mode 100644 index 0000000..a4290a6 --- /dev/null +++ b/billing.go @@ -0,0 +1,121 @@ +package paypal + +import "fmt" + +// https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements + +type ( + ListBillingPlansResp struct { + Plans []Plan `json:"plans"` + } +) + +// CreateBillingPlan creates an empty billing plan. By default, a created billing +// plan is in a CREATED state. A user cannot subscribe to the billing plan +// unless it has been set to the ACTIVE state. +func (c *Client) CreateBillingPlan(p *Plan) (*Plan, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), p) + if err != nil { + return nil, err + } + + v := &Plan{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// UpdateBillingPlan updates data of an existing billing plan. The state of a plan +// must be PlanStateActive before a billing agreement is created +func (c *Client) UpdateBillingPlan(p *Plan) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), struct { + Path string `json:"path"` + Value *Plan `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: p, + OP: PatchOperationReplace, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + if err != nil { + return err + } + + return nil +} + +// GetBillingPlan returns details about a specific billing plan +func (c *Client) GetBillingPlan(planID string) (*Plan, error) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, planID), nil) + if err != nil { + return nil, err + } + + v := &Plan{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// ListBillingPlans returns billing plans based on their current state: created +// active or deactivated +func (c *Client) ListBillingPlans(filter map[string]string) ([]Plan, error) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), nil) + if err != nil { + return nil, err + } + + if filter != nil { + q := req.URL.Query() + + for k, v := range filter { + q.Set(k, v) + } + + req.URL.RawQuery = q.Encode() + } + + var v ListBillingPlansResp + + err = c.SendWithAuth(req, &v) + if err != nil { + return nil, err + } + + return v.Plans, nil +} + +// CreateAgreement creates a billing agreement for the buyer. The EC token generates, +// and the buyer must click an approval URL. Through the approval URL, you obtain +// buyer details and the shipping address. After buyer approval, call the execute +// URL to create the billing agreement in the system. +func (c *Client) CreateAgreement(a *Agreement) (*Agreement, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements", c.APIBase), a) + if err != nil { + return nil, err + } + + v := &Agreement{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} diff --git a/billingagreementtype.go b/billingagreementtype.go new file mode 100644 index 0000000..57d7fbd --- /dev/null +++ b/billingagreementtype.go @@ -0,0 +1,200 @@ +package paypal + +import "time" + +// https://developer.paypal.com/webapps/developer/docs/api/#common-billing-agreements-objects + +var ( + PaymentCardTypeVisa PaymentCardType = "VISA" + PaymentCardTypeAmex PaymentCardType = "AMEX" + PaymentCardTypeSolo PaymentCardType = "SOLO" + PaymentCardTypeJCB PaymentCardType = "JCB" + PaymentCardTypeStar PaymentCardType = "STAR" + PaymentCardTypeDelta PaymentCardType = "DELTA" + PaymentCardTypeDiscover PaymentCardType = "DISCOVER" + PaymentCardTypeSwitch PaymentCardType = "SWITCH" + PaymentCardTypeMaestro PaymentCardType = "MAESTRO" + PaymentCardTypeCBNationale PaymentCardType = "CB_NATIONALE" + PaymentCardTypeConfinoga PaymentCardType = "CONFINOGA" + PaymentCardTypeCofidis PaymentCardType = "COFIDIS" + PaymentCardTypeElectron PaymentCardType = "ELECTRON" + PaymentCardTypeCetelem PaymentCardType = "CETELEM" + PaymentCardTypeChinaUnionPay PaymentCardType = "CHINA_UNION_PAY" + PaymentCardTypeMasterCard PaymentCardType = "MASTERCARD" + + PaymentCardStatusExpired PaymentCardStatus = "EXPIRED" + PaymentCardStatusActive PaymentCardStatus = "ACTIVE" + + CreditTypeBillMeLater CreditType = "BILL_ME_LATER" + CreditTypePaypalExtrasMasterCard CreditType = "PAYPAL_EXTRAS_MASTERCARD" + CreditTypeEbayMasterCard CreditType = "EBAY_MASTERCARD" + CreditTypePaypalSmartConnect CreditType = "PAYPAL_SMART_CONNECT" +) + +type ( + PaymentCardType string + PaymentCardStatus string + CreditType string + + // Agreement maps to agreement object + Agreement struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"desription"` + StartDate *time.Time `json:"start_date"` + Payer *AgreementPayer `json:"payer"` + ShippingAddress *Address `json:"shipping_address,omitempty"` + OverrideMerchantPreferences *MerchantPreferences `json:"override_merchant_preferences,omitempty"` + OverrideChargeModels []ChargeModels `json:"override_charge_models,omitempty"` + Plan *Plan `json:"plan"` + CreateTime *time.Time `json:"create_time"` + UpdateTime *time.Time `json:"update_time"` + Links []Links `json:"links"` + } + + // AgreementPayer maps to the payer object in Billing Agreements + AgreementPayer struct { + PaymentMethod PaymentMethod `json:"payment_method"` + FundingInstruments []AgreementFundingInstrument `json:"funding_instruments"` + FundingOptionID string `json:"funding_option_id"` + PayerInfo *AgreementPayerInfo `json:"payer_info"` + } + + // AgreementFundingInstrument maps to the funding_instrument object in Billing Agreements + AgreementFundingInstrument struct { + CreditCard *AgreementCreditCard `json:"credit_card"` + CreditCardToken *CreditCardToken `json:"credit_card_token"` + PaymentCard *PaymentCard `json:"payment_card"` + PaymentCardToken *PaymentCardToken `json:"payment_card_token"` + BankAccount string `json:"bank_account"` + BankAccountToken *BankToken `json:"bank_token"` + Credit *Credit `json:"credit"` + } + + // AgreementCreditCard maps to the credit_card object in Billing Agreements + AgreementCreditCard struct { + ID string `json:"id,omitempty"` + Number string `json:"number"` + Type CreditCardType `json:"type"` + ExpireMonth string `json:"expire_month"` + ExpireYear string `json:"expire_year"` + CVV2 string `json:"cvv2,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + State CreditCardState `json:"state,omitempty"` + ValidUntil string `json:"valid_until,omitempty"` + Links []Links `json:"links,omitempty"` + } + + // PaymentCard maps to payment_card object + PaymentCard struct { + ID string `json:"id"` + Number string `json:"number"` + Type PaymentCardType `json:"type"` + ExpireMonth string `json:"expire_month"` + ExpireYear string `json:"expire_year"` + StartMonth string `json:"start_month,omitempty"` + StartYear string `json:"start_year,omitempty"` + CVV2 string `json:"cvv2,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + ExternalCustomerID string `json:"external_customer_id"` + Status PaymentCardStatus `json:"status,omitempty"` + ValidUntil string `json:"valid_until,omitempty"` + Links []Links `json:"links,omitempty"` + } + + // PaymentCardToken maps to payment_card_token object + // A resource representing a payment card that can be used to fund a payment. + PaymentCardToken struct { + PaymentCardID string `json:"payment_card_id"` + ExternalCustomerID string `json:"external_customer_id"` + Last4 string `json:"last4"` + Type PaymentCardType `json:"type"` + ExpireMonth string `json:"expire_month"` + ExpireYear string `json:"expire_year"` + } + + // BankToken maps to bank_token object + // A resource representing a bank that can be used to fund a payment. + BankToken struct { + BankID string `json:"bank_id"` + ExternalCustomerID string `json:"external_customer_id"` + MandateReferenceNumber string `json:"mandate_reference_number,omitempty"` + } + + // Credit maps to credit object + // A resource representing a credit instrument. + Credit struct { + ID string `json:"id"` + Type CreditType `json:"type"` + Terms string `json:"terms"` + } + + // AgreementPayerInfo maps to payer_info object in billing agreement + // A resource representing information about a Payer. + AgreementPayerInfo struct { + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + PayerID string `json:"payer_id,omitempty"` + Phone string `json:"phone,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + ShippingAddress *AgreementShippingAddress `json:"shipping_address,omitempty"` + } + + // AgreementShippingAddress maps to shipping_address object in billing agreement + // Extended Address object used as shipping address in a payment. + AgreementShippingAddress struct { + ID string `json:"id"` + RecipientName string `json:"recipient_name"` + DefaultAddress bool `json:"default_address"` + Line1 string `json:"line1"` + Line2 string `json:"line2,omitempty"` + City string `json:"city"` + CountryCode string `json:"country_code"` + PostalCode string `json:"postal_code,omitempty"` + State string `json:"state,omitempty"` + Phone string `json:"phone,omitempty"` + } + + // OverrideChargeModel maps to overridec_charge_model object + // A resource representing an override_charge_model to be used during creation + // of the agreement. + OverrideChargeModels struct { + ChargeID string `json:"charge_id"` + Amount *Currency `json:"amount"` + } + + // AgreementStateDescriptor maps to agreement_state_descriptor object + // Description of the current state of the agreement. + AgreementStateDescriptor struct { + Note string `json:"note,omitempty"` + Amount *Currency `json:"amount"` + } + + // AgreementTransactons maps to agreement_transactions object + // A resource representing agreement_transactions that is returned during a + // transaction search. + AgreementTransactions struct { + AgreementTransactionList []AgreementTransaction `json:"agreement_transaction_list"` + } + + // AgreementTransaction maps to agreement_transaction object + // A resource representing an agreement_transaction that is returned during + // a transaction search. + AgreementTransaction struct { + TransactionID string `json:"transaction_id"` + Status string `json:"status"` + TransactionType string `json:"transaction_type"` + Amount *Currency `json:"amount"` + FeeAmount *Currency `json:"fee_amount"` + NetAmount *Currency `json:"net_amount"` + PayerEmail string `json:"payer_email"` + PayerName string `json:"payer_name"` + TimeUpdated string `json:"time_updated"` + TimeZone string `json:"time_zone"` + } +) diff --git a/billingplantype.go b/billingplantype.go new file mode 100644 index 0000000..3020640 --- /dev/null +++ b/billingplantype.go @@ -0,0 +1,131 @@ +package paypal + +import "time" + +// https://developer.paypal.com/webapps/developer/docs/api/#plan-object + +var ( + PlanTypeFixed PlanType = "FIXED" + PlanTypeInfinite PlanType = "INFINITE" + + PlanStateCreated PlanState = "CREATED" + PlanStateActive PlanState = "ACTIVE" + PlanStateInactive PlanState = "INACTIVE" + + PaymentDefinitionTypeTrial PaymentDefinitionType = "TRIAL" + PaymentDefinitionTypeRegular PaymentDefinitionType = "REGULAR" + + ChargeModelsTypeShipping ChargeModelsType = "shipping" + ChargeModelsTypeTax ChargeModelsType = "tax" + + PatchOperationAdd PatchOperation = "add" + PatchOperationRemove PatchOperation = "remove" + PatchOperationReplace PatchOperation = "replace" + PatchOperationMove PatchOperation = "move" + PatchOperationCopy PatchOperation = "copy" + PatchOperationTest PatchOperation = "test" +) + +type ( + PlanType string + PlanState string + PaymentDefinitionType string + TermType string + ChargeModelsType string + PatchOperation string + + // Plan maps to plan object + Plan struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type PlanType `json:"type"` + State PlanState `json:"state"` + Payee *Payee `json:"payee"` + CreateTime *time.Time `json:"create_time"` + UpdateTime *time.Time `json:"update_time"` + PaymentDefinitions []PaymentDefinition `json:"payment_definitions"` + Terms []Terms `json:"terms"` + MerchantPreferences *MerchantPreferences `json:"merchant_preferences,omitempty"` + Links []Links `json:"links"` + } + + // Payee maps to payee object + Payee struct { + Email string `json:"email"` + MerchantID string `json:"merchant_id"` + Phone *Phone `json:"phone,omitempty"` + AdditionalProperties string `json:"additional_properties,omitempty"` + } + + // Phone maps to phone object + Phone struct { + CountryCode string `json:"country_code"` + NationalNumber string `json:"national_number"` + Extension string `json:"extension,omitempty"` + } + + // PaymentDefinition maps to payment_definition object + PaymentDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + Type PaymentDefinitionType `json:"type"` + FrequencyInterval string `json:"frequency_interval"` + Frequency string `json:"frequency"` + Cycles string `json:"cycles"` + Amount *Currency `json:"amount"` + ChargeModels []ChargeModels `json:"charge_models,omitempty"` + } + + // Currency maps to currency object + // Base object for all financial value related fields (balance, payment due, etc.) + Currency struct { + Currency string `json:"currency"` + Value string `json:"value"` + } + + // ChargeModels maps to charge_models object + ChargeModels struct { + ID string `json:"id"` + Type ChargeModelsType `json:"type"` + Amount *Currency `json:"amount"` + } + + // Terms maps to terms object + Terms struct { + ID string `json:"id"` + Type TermType `json:"type"` + MaxBillingAmount *Currency `json:"max_billing_amount"` + Occurrences string `json:"occurrences"` + AmountRange *Currency `json:"amount_range"` + BuyerEditable string `json:"buyer_editable"` + } + + // MerchantPreferences maps to merchant_preferences boject + MerchantPreferences struct { + ID string `json:"id"` + SetupFee *Currency `json:"setup_fee,omitempty"` + CancelURL string `json:"cancel_url"` + ReturnURL string `json:"return_url"` + NotifyURL string `json:"notify_url"` + MaxFailAttemps string `json:"max_fail_attemps,omitempty"` // Default is 0, which is unlimited + AutoBillAmount string `json:"auto_bill_amount,omitempty,omitempty"` + InitialFailAmountAction string `json:"initial_fail_amount_action,omitempty"` + AcceptedPaymentType string `json:"accepted_payment_type"` + CharSet string `json:"char_set"` + } + + // PatchRequest maps to patch_request object + PatchRequest struct { + OP PatchOperation `json:"op"` + Path string `json:"path"` + Value string `json:"value"` + From string `json:"from"` + } + + // PlanList maps to plan_list object + PlanList struct { + Plans []Plan `json:"plans"` + Links []Links `json:"links"` + } +) diff --git a/paymenttype.go b/paymenttype.go index 5f792c0..cdc7981 100644 --- a/paymenttype.go +++ b/paymenttype.go @@ -19,6 +19,11 @@ var ( CaptureStateRefunded CaptureState = "refunded" CaptureStatePartiallyRefunded CaptureState = "partially_refunded" + CreditCardTypeVisa CreditCardType = "visa" + CreditCardTypeMastercard CreditCardType = "mastercard" + CreditCardTypeDiscover CreditCardType = "discover" + CreditCardTypeAmex CreditCardType = "amex" + CreditCardStateExpired CreditCardState = "expired" CreditCardStateOK CreditCardState = "ok" @@ -96,6 +101,7 @@ var ( type ( AuthorizationState string CaptureState string + CreditCardType string CreditCardState string OrderState string PendingReason string @@ -188,7 +194,7 @@ type ( ID string `json:"id,omitempty"` PayerID string `json:"payer_id,omitempty"` Number string `json:"number"` - Type string `json:"type"` + Type CreditCardType `json:"type"` ExpireMonth string `json:"expire_month"` ExpireYear string `json:"expire_year"` CVV2 string `json:"cvv2,omitempty"` From 0097c64daca45638de576b5d0bfe3a345b572f56 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Fri, 3 Oct 2014 10:38:57 +0200 Subject: [PATCH 05/20] Updated README with building badge and roadmap --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81ff38a..a1c1a57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Payment REST API Go client -[![Coverage Status](https://coveralls.io/repos/fundary/paypal/badge.png)](https://coveralls.io/r/fundary/paypal) [![GoDoc](https://godoc.org/github.com/fundary/paypal?status.svg)](https://godoc.org/github.com/fundary/paypal) +[![Coverage Status](https://coveralls.io/repos/fundary/paypal/badge.png)](https://coveralls.io/r/fundary/paypal) [![Build Status](https://travis-ci.org/fundary/paypal.svg?branch=develop)](https://travis-ci.org/fundary/paypal) [![GoDoc](https://godoc.org/github.com/fundary/paypal?status.svg)](https://godoc.org/github.com/fundary/paypal) A Go client for the Paypal REST API ([https://developer.paypal.com/webapps/developer/docs/api/](https://developer.paypal.com/webapps/developer/docs/api/)) @@ -75,7 +75,7 @@ PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] go te - [x] [Payments - Refunds](https://developer.paypal.com/webapps/developer/docs/api/#refunds) - [x] [Payments - Authorizations](https://developer.paypal.com/webapps/developer/docs/api/#authorizations) - [x] [Payments - Captures](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) -- [ ] [Payments - Billing Plans and Agreements](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) +- [x] [Payments - Billing Plans and Agreements](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) - [ ] [Payments - Order](https://developer.paypal.com/webapps/developer/docs/api/#orders) - [ ] [Vault](https://developer.paypal.com/webapps/developer/docs/api/#vault) - [ ] [Identity](https://developer.paypal.com/webapps/developer/docs/api/#identity) From bdd80db592759ef1efd279eda480571114d88274 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 6 Oct 2014 12:54:57 +0200 Subject: [PATCH 06/20] Added billing agreemnt endpoints --- billing.go | 176 ++++++++++++++++++++++++++++++++++++++++++++- billingplantype.go | 7 -- commontype.go | 7 ++ 3 files changed, 182 insertions(+), 8 deletions(-) diff --git a/billing.go b/billing.go index a4290a6..84c12fa 100644 --- a/billing.go +++ b/billing.go @@ -32,7 +32,7 @@ func (c *Client) CreateBillingPlan(p *Plan) (*Plan, error) { // UpdateBillingPlan updates data of an existing billing plan. The state of a plan // must be PlanStateActive before a billing agreement is created func (c *Client) UpdateBillingPlan(p *Plan) error { - req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), struct { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, p.ID), struct { Path string `json:"path"` Value *Plan `json:"value"` OP PatchOperation `json:"op"` @@ -119,3 +119,177 @@ func (c *Client) CreateAgreement(a *Agreement) (*Agreement, error) { return v, nil } + +// ExecuteAgreement executes an agreement after the buyer approves it. +func (c *Client) ExecuteAgreement(paymentID string) (*Agreement, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/agreement-execute", c.APIBase, paymentID), nil) + if err != nil { + return nil, err + } + + v := &Agreement{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// UpdateAgreement updates an agreement +func (c *Client) UpdateAgreement(a *Agreement) error { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-agreements/%s", c.APIBase, a.ID), struct { + Path string `json:"path"` + Value *Agreement `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: a, + OP: PatchOperationReplace, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + if err != nil { + return err + } + + return nil +} + +// GetAgreement returns an agreement +func (c *Client) GetAgreement(agreementID string) (*Agreement, error) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-agreements/%s", c.APIBase, agreementID), nil) + if err != nil { + return nil, err + } + + v := &Agreement{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// SuspendAgreement suspends an agreement +func (c *Client) SuspendAgreement(agreementID, note string) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/suspend", c.APIBase, agreementID), struct { + Note string `json:"note"` + }{ + Note: note, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + + return err +} + +// ReactivateAgreement reactivate an agreement +func (c *Client) ReactivateAgreement(agreementID, note string) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/re-activate", c.APIBase, agreementID), struct { + Note string `json:"note"` + }{ + Note: note, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + + return err +} + +// SearchAgreementTransactions searches for transactions within a billing agreement +func (c *Client) SearchAgreementTransactions(agreementID string, filter map[string]string) (*AgreementTransactions, error) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-agreements/%s/transaction", c.APIBase, agreementID), nil) + if err != nil { + return nil, err + } + + if filter != nil { + q := req.URL.Query() + + for k, v := range filter { + q.Set(k, v) + } + + req.URL.RawQuery = q.Encode() + } + + v := &AgreementTransactions{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// CancelAgreement cancels an agreement +func (c *Client) CancelAgreement(agreementID, note string) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/cancel", c.APIBase, agreementID), struct { + Note string `json:"note"` + }{ + Note: note, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + + return err +} + +// SetAgreementBalance sets the outstanding amount of an agreement +func (c *Client) SetAgreementBalance(agreementID string, currency *Currency) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/set-balance", c.APIBase, agreementID), currency) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + + return err +} + +// BillAgreementBalance bills the outstanding amount of an agreement +func (c *Client) BillAgreementBalance(agreementID string, currency *Currency, note string) error { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/bill-balance", c.APIBase, agreementID), struct { + Note string `json:"note"` + Amount *Currency `json:"amount"` + }{ + Note: note, + Amount: currency, + }) + if err != nil { + return err + } + + v := &struct{}{} + + err = c.SendWithAuth(req, v) + + return err +} diff --git a/billingplantype.go b/billingplantype.go index 3020640..0a837f1 100644 --- a/billingplantype.go +++ b/billingplantype.go @@ -77,13 +77,6 @@ type ( ChargeModels []ChargeModels `json:"charge_models,omitempty"` } - // Currency maps to currency object - // Base object for all financial value related fields (balance, payment due, etc.) - Currency struct { - Currency string `json:"currency"` - Value string `json:"value"` - } - // ChargeModels maps to charge_models object ChargeModels struct { ID string `json:"id"` diff --git a/commontype.go b/commontype.go index a937a70..591a03f 100644 --- a/commontype.go +++ b/commontype.go @@ -12,4 +12,11 @@ type ( Enctype string `json:"enctype"` // Schema HyperSchema `json:"schema"` } + + // Currency maps to currency object + // Base object for all financial value related fields (balance, payment due, etc.) + Currency struct { + Currency string `json:"currency"` + Value string `json:"value"` + } ) From 49684d550d2092ddc6e140856a4d9038bbbd5e6e Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 6 Oct 2014 13:46:22 +0200 Subject: [PATCH 07/20] Added orders endpoint --- order.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 order.go diff --git a/order.go b/order.go new file mode 100644 index 0000000..ca98d89 --- /dev/null +++ b/order.go @@ -0,0 +1,92 @@ +package paypal + +import "fmt" + +// https://developer.paypal.com/webapps/developer/docs/api/#orders + +// GetOrder returns details about an order +func (c *Client) GetOrder(orderID string) (*Order, error) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/orders/%s", c.APIBase, orderID), nil) + if err != nil { + return nil, err + } + + v := &Order{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// AuthorizeOrder authorizes an order +func (c *Client) AuthorizeOrder(orderID, string, amount *Amount) (*Authorization, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/authorize", c.APIBase, orderID), struct { + Amount *Amount `json:"amount"` + }{ + Amount: amount, + }) + if err != nil { + return nil, err + } + + v := &Authorization{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// CaptureOrder captures a payment on an order. To use this call, an original payment +// must specify an "intent" of "order" +func (c *Client) CaptureOrder(orderID, string, amount *Amount, isFinal bool) (*Capture, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/capture", c.APIBase, orderID), struct { + Amount *Amount `json:"amount"` + IsFinalCapture bool `json:"is_final_capture"` + }{ + Amount: amount, + IsFinalCapture: isFinal, + }) + if err != nil { + return nil, err + } + + v := &Capture{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// VoidOrder voids an existing order. An order cannot be voided if payment +// has already been partially or fully captured +func (c *Client) VoidOrder(orderID string) (*Order, error) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/do-void", c.APIBase, orderID), nil) + if err != nil { + return nil, err + } + + v := &Order{} + + err = c.SendWithAuth(req, v) + if err != nil { + return nil, err + } + + return v, nil +} + +// RefundOrder refunds an exsting captured order. This only works after the +// order amount is captured. A refund cannot be made if the order is not captured. +// Alias for RefundCapture +func (c *Client) RefundOrder(captureID string, a *Amount) (*Refund, error) { + return c.RefundCapture(captureID, a) +} From cd963b2400232fdbb6dfabc32d3a4d80b20ae0b5 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Tue, 7 Oct 2014 11:58:29 +0200 Subject: [PATCH 08/20] Return *http.Response for each method to allow inspection of headers and status code --- authorization.go | 45 ++++++++-------- billing.go | 135 ++++++++++++++++++++++++----------------------- capture.go | 25 +++++---- e2e_test.go | 8 +-- order.go | 47 +++++++++-------- payment.go | 45 ++++++++-------- paypal.go | 16 +++--- refund.go | 15 +++--- sale.go | 21 ++++---- vault.go | 85 +++++++++++++++++++++++++++++ 10 files changed, 273 insertions(+), 169 deletions(-) create mode 100644 vault.go diff --git a/authorization.go b/authorization.go index 416751b..0ac6750 100644 --- a/authorization.go +++ b/authorization.go @@ -1,6 +1,9 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // https://developer.paypal.com/webapps/developer/docs/api/#authorizations @@ -11,79 +14,79 @@ type ( ) // GetAuthorization returns an authorization by ID -func (c *Client) GetAuthorization(authID string) (*Authorization, error) { +func (c *Client) GetAuthorization(authID string) (*Authorization, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/authorization/%s", c.APIBase, authID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Authorization{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // CaptureAuthorization captures and process an existing authorization. // To use this method, the original payment must have Intent set to PaymentIntentAuthorize -func (c *Client) CaptureAuthorization(authID string, a *Amount, isFinalCapture bool) (*Capture, error) { +func (c *Client) CaptureAuthorization(authID string, a *Amount, isFinalCapture bool) (*Capture, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/authorization/%s/capture", c.APIBase, authID), struct { Amount *Amount `json:"amount"` IsFinalCapture bool `json:"is_final_capture"` }{a, isFinalCapture}) if err != nil { - return nil, err + return nil, err, nil } v := &Capture{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // VoidAuthorization voids a previously authorized payment. A fully // captured authorization cannot be voided -func (c *Client) VoidAuthorization(authID string) (*Authorization, error) { +func (c *Client) VoidAuthorization(authID string) (*Authorization, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/authorization/%s/void", c.APIBase, authID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Authorization{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // ReauthorizeAuthorization reauthorize a Paypal account payment. Paypal recommends // that a payment should be reauthorized after the initial 3-day honor period to // ensure that funds are still available. Only paypal account payments can be re- // authorized -func (c *Client) ReauthorizeAuthorization(authID string, a *Amount) (*Authorization, error) { +func (c *Client) ReauthorizeAuthorization(authID string, a *Amount) (*Authorization, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/authorization/%s/reauthorize", c.APIBase, authID), struct { Amount *Amount `json:"amount"` }{a}) if err != nil { - return nil, err + return nil, err, nil } v := &Authorization{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } diff --git a/billing.go b/billing.go index 84c12fa..6bdebac 100644 --- a/billing.go +++ b/billing.go @@ -1,6 +1,9 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements @@ -13,25 +16,25 @@ type ( // CreateBillingPlan creates an empty billing plan. By default, a created billing // plan is in a CREATED state. A user cannot subscribe to the billing plan // unless it has been set to the ACTIVE state. -func (c *Client) CreateBillingPlan(p *Plan) (*Plan, error) { +func (c *Client) CreateBillingPlan(p *Plan) (*Plan, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), p) if err != nil { - return nil, err + return nil, err, nil } v := &Plan{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // UpdateBillingPlan updates data of an existing billing plan. The state of a plan // must be PlanStateActive before a billing agreement is created -func (c *Client) UpdateBillingPlan(p *Plan) error { +func (c *Client) UpdateBillingPlan(p *Plan) (error, *http.Response) { req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, p.ID), struct { Path string `json:"path"` Value *Plan `json:"value"` @@ -42,42 +45,42 @@ func (c *Client) UpdateBillingPlan(p *Plan) error { OP: PatchOperationReplace, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return err + return err, resp } - return nil + return nil, resp } // GetBillingPlan returns details about a specific billing plan -func (c *Client) GetBillingPlan(planID string) (*Plan, error) { +func (c *Client) GetBillingPlan(planID string) (*Plan, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, planID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Plan{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // ListBillingPlans returns billing plans based on their current state: created // active or deactivated -func (c *Client) ListBillingPlans(filter map[string]string) ([]Plan, error) { +func (c *Client) ListBillingPlans(filter map[string]string) ([]Plan, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), nil) if err != nil { - return nil, err + return nil, err, nil } if filter != nil { @@ -92,53 +95,53 @@ func (c *Client) ListBillingPlans(filter map[string]string) ([]Plan, error) { var v ListBillingPlansResp - err = c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, &v) if err != nil { - return nil, err + return nil, err, resp } - return v.Plans, nil + return v.Plans, nil, resp } // CreateAgreement creates a billing agreement for the buyer. The EC token generates, // and the buyer must click an approval URL. Through the approval URL, you obtain // buyer details and the shipping address. After buyer approval, call the execute // URL to create the billing agreement in the system. -func (c *Client) CreateAgreement(a *Agreement) (*Agreement, error) { +func (c *Client) CreateAgreement(a *Agreement) (*Agreement, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements", c.APIBase), a) if err != nil { - return nil, err + return nil, err, nil } v := &Agreement{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // ExecuteAgreement executes an agreement after the buyer approves it. -func (c *Client) ExecuteAgreement(paymentID string) (*Agreement, error) { +func (c *Client) ExecuteAgreement(paymentID string) (*Agreement, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/agreement-execute", c.APIBase, paymentID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Agreement{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // UpdateAgreement updates an agreement -func (c *Client) UpdateAgreement(a *Agreement) error { +func (c *Client) UpdateAgreement(a *Agreement) (error, *http.Response) { req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-agreements/%s", c.APIBase, a.ID), struct { Path string `json:"path"` Value *Agreement `json:"value"` @@ -149,77 +152,77 @@ func (c *Client) UpdateAgreement(a *Agreement) error { OP: PatchOperationReplace, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return err + return err, resp } - return nil + return nil, resp } // GetAgreement returns an agreement -func (c *Client) GetAgreement(agreementID string) (*Agreement, error) { +func (c *Client) GetAgreement(agreementID string) (*Agreement, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-agreements/%s", c.APIBase, agreementID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Agreement{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // SuspendAgreement suspends an agreement -func (c *Client) SuspendAgreement(agreementID, note string) error { +func (c *Client) SuspendAgreement(agreementID, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/suspend", c.APIBase, agreementID), struct { Note string `json:"note"` }{ Note: note, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) - return err + return err, resp } // ReactivateAgreement reactivate an agreement -func (c *Client) ReactivateAgreement(agreementID, note string) error { +func (c *Client) ReactivateAgreement(agreementID, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/re-activate", c.APIBase, agreementID), struct { Note string `json:"note"` }{ Note: note, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) - return err + return err, resp } // SearchAgreementTransactions searches for transactions within a billing agreement -func (c *Client) SearchAgreementTransactions(agreementID string, filter map[string]string) (*AgreementTransactions, error) { +func (c *Client) SearchAgreementTransactions(agreementID string, filter map[string]string) (*AgreementTransactions, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-agreements/%s/transaction", c.APIBase, agreementID), nil) if err != nil { - return nil, err + return nil, err, nil } if filter != nil { @@ -234,48 +237,48 @@ func (c *Client) SearchAgreementTransactions(agreementID string, filter map[stri v := &AgreementTransactions{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // CancelAgreement cancels an agreement -func (c *Client) CancelAgreement(agreementID, note string) error { +func (c *Client) CancelAgreement(agreementID, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/cancel", c.APIBase, agreementID), struct { Note string `json:"note"` }{ Note: note, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) - return err + return err, resp } // SetAgreementBalance sets the outstanding amount of an agreement -func (c *Client) SetAgreementBalance(agreementID string, currency *Currency) error { +func (c *Client) SetAgreementBalance(agreementID string, currency *Currency) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/set-balance", c.APIBase, agreementID), currency) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) - return err + return err, resp } // BillAgreementBalance bills the outstanding amount of an agreement -func (c *Client) BillAgreementBalance(agreementID string, currency *Currency, note string) error { +func (c *Client) BillAgreementBalance(agreementID string, currency *Currency, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements/%s/bill-balance", c.APIBase, agreementID), struct { Note string `json:"note"` Amount *Currency `json:"amount"` @@ -284,12 +287,12 @@ func (c *Client) BillAgreementBalance(agreementID string, currency *Currency, no Amount: currency, }) if err != nil { - return err + return err, nil } v := &struct{}{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) - return err + return err, resp } diff --git a/capture.go b/capture.go index d9c0169..f47b424 100644 --- a/capture.go +++ b/capture.go @@ -1,42 +1,45 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // https://developer.paypal.com/webapps/developer/docs/api/#captures // GetCapture returns details about a captured payment -func (c *Client) GetCapture(captureID string) (*Capture, error) { +func (c *Client) GetCapture(captureID string) (*Capture, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/capture/%s", c.APIBase, captureID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Capture{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // RefundCapture refund a captured payment. For partial refunds, a lower // Amount object can be passed in. -func (c *Client) RefundCapture(captureID string, a *Amount) (*Refund, error) { +func (c *Client) RefundCapture(captureID string, a *Amount) (*Refund, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/capture/%s/refund", c.APIBase, captureID), struct { Amount *Amount `json:"amount"` }{a}) if err != nil { - return nil, err + return nil, err, nil } v := &Refund{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } diff --git a/e2e_test.go b/e2e_test.go index 5bda15f..9b782e0 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -54,7 +54,7 @@ func TestPayment(t *testing.T) { Payer: &payer, Transactions: []Transaction{transaction}, } - newPaymentResp, err := client.CreatePayment(payment) + newPaymentResp, err, _ := client.CreatePayment(payment) So(err, ShouldBeNil) So(newPaymentResp.Intent, ShouldEqual, PaymentIntentSale) @@ -70,7 +70,7 @@ func TestPayment(t *testing.T) { // }) Convey("Fetching the newly created payment should return valid results", func() { - payment, err := client.GetPayment(newPaymentResp.ID) + payment, err, _ := client.GetPayment(newPaymentResp.ID) So(err, ShouldBeNil) So(payment.ID, ShouldEqual, newPaymentResp.ID) @@ -80,7 +80,7 @@ func TestPayment(t *testing.T) { Convey("With the sale endpoints", func() { Convey("Fetching an existing sale should return valid data", func() { - sale, err := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) + sale, err, _ := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) So(err, ShouldBeNil) So(sale.ID, ShouldEqual, newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) @@ -118,7 +118,7 @@ func TestPayment(t *testing.T) { }) Convey("List payments should include the newly created payment", func() { - payments, err := client.ListPayments(map[string]string{ + payments, err, _ := client.ListPayments(map[string]string{ "count": "10", "sort_by": "create_time", }) diff --git a/order.go b/order.go index ca98d89..b978fb7 100644 --- a/order.go +++ b/order.go @@ -1,50 +1,53 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // https://developer.paypal.com/webapps/developer/docs/api/#orders // GetOrder returns details about an order -func (c *Client) GetOrder(orderID string) (*Order, error) { +func (c *Client) GetOrder(orderID string) (*Order, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/orders/%s", c.APIBase, orderID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Order{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // AuthorizeOrder authorizes an order -func (c *Client) AuthorizeOrder(orderID, string, amount *Amount) (*Authorization, error) { +func (c *Client) AuthorizeOrder(orderID, string, amount *Amount) (*Authorization, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/authorize", c.APIBase, orderID), struct { Amount *Amount `json:"amount"` }{ Amount: amount, }) if err != nil { - return nil, err + return nil, err, nil } v := &Authorization{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // CaptureOrder captures a payment on an order. To use this call, an original payment // must specify an "intent" of "order" -func (c *Client) CaptureOrder(orderID, string, amount *Amount, isFinal bool) (*Capture, error) { +func (c *Client) CaptureOrder(orderID, string, amount *Amount, isFinal bool) (*Capture, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/capture", c.APIBase, orderID), struct { Amount *Amount `json:"amount"` IsFinalCapture bool `json:"is_final_capture"` @@ -53,40 +56,40 @@ func (c *Client) CaptureOrder(orderID, string, amount *Amount, isFinal bool) (*C IsFinalCapture: isFinal, }) if err != nil { - return nil, err + return nil, err, nil } v := &Capture{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // VoidOrder voids an existing order. An order cannot be voided if payment // has already been partially or fully captured -func (c *Client) VoidOrder(orderID string) (*Order, error) { +func (c *Client) VoidOrder(orderID string) (*Order, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/do-void", c.APIBase, orderID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Order{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // RefundOrder refunds an exsting captured order. This only works after the // order amount is captured. A refund cannot be made if the order is not captured. // Alias for RefundCapture -func (c *Client) RefundOrder(captureID string, a *Amount) (*Refund, error) { +func (c *Client) RefundOrder(captureID string, a *Amount) (*Refund, error, *http.Response) { return c.RefundCapture(captureID, a) } diff --git a/payment.go b/payment.go index a46d9d3..b368988 100644 --- a/payment.go +++ b/payment.go @@ -1,6 +1,9 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // https://developer.paypal.com/webapps/developer/docs/api/#payments @@ -23,24 +26,24 @@ type ( ) // CreatePayment creates a payment in Paypal -func (c *Client) CreatePayment(p Payment) (*CreatePaymentResp, error) { +func (c *Client) CreatePayment(p Payment) (*CreatePaymentResp, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/payment", c.APIBase), p) if err != nil { - return nil, err + return nil, err, nil } v := &CreatePaymentResp{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // ExecutePayment completes an approved Paypal payment that has been approved by the payer -func (c *Client) ExecutePayment(paymentID, payerID string, transactions []Transaction) (*ExecutePaymentResp, error) { +func (c *Client) ExecutePayment(paymentID, payerID string, transactions []Transaction) (*ExecutePaymentResp, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/payment/%s/execute", c.APIBase, paymentID), struct { PayerID string `json:"payer_id"` Transactions []Transaction `json:"transactions"` @@ -49,41 +52,41 @@ func (c *Client) ExecutePayment(paymentID, payerID string, transactions []Transa transactions, }) if err != nil { - return nil, err + return nil, err, nil } v := &ExecutePaymentResp{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // GetPayment fetches a payment in Paypal -func (c *Client) GetPayment(id string) (*Payment, error) { +func (c *Client) GetPayment(id string) (*Payment, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/payment/%s", c.APIBase, id), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Payment{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // ListPayments retrieve payments resources from Paypal -func (c *Client) ListPayments(filter map[string]string) ([]Payment, error) { +func (c *Client) ListPayments(filter map[string]string) ([]Payment, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/payment/", c.APIBase), nil) if err != nil { - return nil, err + return nil, err, nil } if filter != nil { @@ -98,10 +101,10 @@ func (c *Client) ListPayments(filter map[string]string) ([]Payment, error) { var v ListPaymentsResp - err = c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, &v) if err != nil { - return nil, err + return nil, err, resp } - return v.Payments, nil + return v.Payments, nil, resp } diff --git a/paypal.go b/paypal.go index 871b879..da641f0 100644 --- a/paypal.go +++ b/paypal.go @@ -102,7 +102,7 @@ func (c *Client) GetAccessToken() (*TokenResp, error) { req.Header.Set("Content-type", "application/x-www-form-urlencoded") t := TokenResp{} - err = c.Send(req, &t) + _, err = c.Send(req, &t) if err == nil { t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn/2) * time.Second) } @@ -113,7 +113,7 @@ func (c *Client) GetAccessToken() (*TokenResp, error) { // Send makes a request to the API, the response body will be // unmarshaled into v, or if v is an io.Writer, the response will // be written to it without decoding -func (c *Client) Send(req *http.Request, v interface{}) error { +func (c *Client) Send(req *http.Request, v interface{}) (*http.Response, error) { // Set default headers req.Header.Set("Accept", "application/json") req.Header.Set("Accept-Language", "en_US") @@ -127,7 +127,7 @@ func (c *Client) Send(req *http.Request, v interface{}) error { resp, err := c.client.Do(req) if err != nil { - return err + return resp, err } defer resp.Body.Close() @@ -138,7 +138,7 @@ func (c *Client) Send(req *http.Request, v interface{}) error { json.Unmarshal(data, errResp) } - return errResp + return resp, errResp } if v != nil { @@ -147,22 +147,22 @@ func (c *Client) Send(req *http.Request, v interface{}) error { } else { err = json.NewDecoder(resp.Body).Decode(v) if err != nil { - return err + return resp, err } } } - return nil + return resp, nil } // SendWithAuth makes a request to the API and apply OAuth2 header automatically. // If the access token soon to be expired, it will try to get a new one before // making the main request -func (c *Client) SendWithAuth(req *http.Request, v interface{}) error { +func (c *Client) SendWithAuth(req *http.Request, v interface{}) (*http.Response, error) { if (c.Token == nil) || (c.Token.ExpiresAt.Before(time.Now())) { resp, err := c.GetAccessToken() if err != nil { - return err + return nil, err } c.Token = resp diff --git a/refund.go b/refund.go index b3c887d..a32e272 100644 --- a/refund.go +++ b/refund.go @@ -1,20 +1,23 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) // GetRefund returns a refund by ID -func (c *Client) GetRefund(refundID string) (*Refund, error) { +func (c *Client) GetRefund(refundID string) (*Refund, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/refund/%s", c.APIBase, refundID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Refund{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } diff --git a/sale.go b/sale.go index 3da2854..5687b05 100644 --- a/sale.go +++ b/sale.go @@ -2,6 +2,7 @@ package paypal import ( "fmt" + "net/http" ) type ( @@ -11,37 +12,37 @@ type ( ) // GetSales returns a sale by ID -func (c *Client) GetSale(saleID string) (*Sale, error) { +func (c *Client) GetSale(saleID string) (*Sale, error, *http.Response) { req, err := NewRequest("GET", fmt.Sprintf("%s/payments/sale/%s", c.APIBase, saleID), nil) if err != nil { - return nil, err + return nil, err, nil } v := &Sale{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } // RefundSale refunds a completed payment and accepts an optional // Amount struct. If Amount is provided, a partial refund is requested, // or else a full refund is made instead -func (c *Client) RefundSale(saleID string, a *Amount) (*Refund, error) { +func (c *Client) RefundSale(saleID string, a *Amount) (*Refund, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/sale/%s/refund", c.APIBase, saleID), &RefundReq{Amount: a}) if err != nil { - return nil, err + return nil, err, nil } v := &Refund{} - err = c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, v) if err != nil { - return nil, err + return nil, err, resp } - return v, nil + return v, nil, resp } diff --git a/vault.go b/vault.go new file mode 100644 index 0000000..1ededb1 --- /dev/null +++ b/vault.go @@ -0,0 +1,85 @@ +package paypal + +import ( + "fmt" + "net/http" +) + +// https://developer.paypal.com/webapps/developer/docs/api/#vault + +// StoreCreditCard stores credit card details with Paypal. To use the stored card, +// use the returned ID as CreditCardID within a CreditCardToken. Including a PayerID +// is also recommended +func (c *Client) StoreCreditCard(creditCard *CreditCard) (*CreditCard, error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/vault/credit-card", c.APIBase), creditCard) + if err != nil { + return nil, err, nil + } + + v := &CreditCard{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// DeleteStoredCreditCard delete details of a credit card. Note that even though +// a credit card is deleted, some limited information about that credit card is +// still provided when a sale is retrieved. +func (c *Client) DeleteStoredCreditCard(creditCardID string) (error, *http.Response) { + req, err := NewRequest("DELETE", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCardID), nil) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, v) + + return err, resp +} + +// GetStoredCreditCard returns details of a stored credit card. +func (c *Client) GetStoredCreditCard(creditCardID string) (*CreditCard, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCardID), nil) + if err != nil { + return nil, err, nil + } + + v := &CreditCard{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// UpdateStoredCreditCard modifies a stored credit card +func (c *Client) UpdateStoredCreditCard(creditCard *CreditCard) (*CreditCard, error, *http.Response) { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCard.ID), struct { + Path string `json:"path"` + Value *CreditCard `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: creditCard, + OP: PatchOperationReplace, + }) + if err != nil { + return nil, err, nil + } + + v := &CreditCard{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} From f0afa385a74bb83165a9bcd4cfa0a158f01b81aa Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Wed, 8 Oct 2014 10:34:51 +0200 Subject: [PATCH 09/20] Added endpoint for invoices --- commontype.go | 53 ++++++++++ invoicing.go | 256 +++++++++++++++++++++++++++++++++++++++++++++++ invoicingtype.go | 243 ++++++++++++++++++++++++++++++++++++++++++++ paymenttype.go | 48 +-------- 4 files changed, 555 insertions(+), 45 deletions(-) create mode 100644 invoicing.go create mode 100644 invoicingtype.go diff --git a/commontype.go b/commontype.go index 591a03f..1d352f6 100644 --- a/commontype.go +++ b/commontype.go @@ -1,6 +1,23 @@ package paypal +var ( + AddressTypeResidential AddressType = "residential" + AddressTypeBusiness AddressType = "business" + AddressTypeMailbox AddressType = "mailbox" + + CreditCardTypeVisa CreditCardType = "visa" + CreditCardTypeMastercard CreditCardType = "mastercard" + CreditCardTypeDiscover CreditCardType = "discover" + CreditCardTypeAmex CreditCardType = "amex" + + CreditCardStateExpired CreditCardState = "expired" + CreditCardStateOK CreditCardState = "ok" +) + type ( + AddressType string + CreditCardType string + CreditCardState string // Links maps to links object Links struct { @@ -19,4 +36,40 @@ type ( Currency string `json:"currency"` Value string `json:"value"` } + + // Address maps to address object + Address struct { + Line1 string `json:"line1"` + Line2 string `json:"line2,omitempty"` + City string `json:"city"` + CountryCode string `json:"country_code"` + PostalCode string `json:"postal_code,omitempty"` + State string `json:"state,omitempty"` + Phone string `json:"phone,omitempty"` + } + + // CreditCard maps to credit_card object + CreditCard struct { + ID string `json:"id,omitempty"` + PayerID string `json:"payer_id,omitempty"` + Number string `json:"number"` + Type CreditCardType `json:"type"` + ExpireMonth string `json:"expire_month"` + ExpireYear string `json:"expire_year"` + CVV2 string `json:"cvv2,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + State CreditCardState `json:"state,omitempty"` + ValidUntil string `json:"valid_until,omitempty"` + } + + // CreditCardToken maps to credit_card_token object + CreditCardToken struct { + CreditCardID string `json:"credit_card_id"` + PayerID string `json:"payer_id,omitempty"` + Last4 string `json:"last4,omitempty"` + ExpireYear string `json:"expire_year,omitempty"` + ExpireMonth string `json:"expire_month,omitempty"` + } ) diff --git a/invoicing.go b/invoicing.go new file mode 100644 index 0000000..58d4fed --- /dev/null +++ b/invoicing.go @@ -0,0 +1,256 @@ +package paypal + +import ( + "fmt" + "net/http" + "time" +) + +// https://developer.paypal.com/webapps/developer/docs/api/#invoicing + +// CreateInvoice creates an invoice in draft state. After an invoice is created +// with items array, it can be sent. +func (c *Client) CreateInvoice(i *Invoice) (*Invoice, error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices", c.APIBase), i) + if err != nil { + return nil, err, nil + } + + v := &Invoice{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// SendInvoice sends an invoice to the payer. An invoice cannot be sent unless it +// includes the item array +func (c *Client) SendInvoice(invoiceID string) (error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/send", c.APIBase, invoiceID), nil) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// UpdateInvoice updates an invoic +func (c *Client) UpdateInvoice(i *Invoice) (*Invoice, error, *http.Response) { + req, err := NewRequest("PUT", fmt.Sprintf("%s/invoicing/invoices/%s", c.APIBase, i.ID), i) + if err != nil { + return nil, err, nil + } + + v := &Invoice{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// GetInvoice returns the specific invoice +func (c *Client) GetInvoice(invoiceID string) (*Invoice, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/invoicing/invoices/%s", c.APIBase, invoiceID), nil) + if err != nil { + return nil, err, nil + } + + v := &Invoice{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// ListInvoices returns invoices that belong to the merchant who makes the call +func (c *Client) ListInvoices(filter map[string]string) ([]Invoice, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/invoicing/invoices", c.APIBase), nil) + if err != nil { + return nil, err, nil + } + + if filter != nil { + q := req.URL.Query() + + for k, v := range filter { + q.Set(k, v) + } + + req.URL.RawQuery = q.Encode() + } + + var v struct { + Invoices []Invoice `json:"invoices"` + } + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v.Invoices, nil, resp +} + +// SearchInvoices returns invoices that match the specificed criteria +func (c *Client) SearchInvoices(s *Search) ([]Invoice, error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/search", c.APIBase), s) + if err != nil { + return nil, err, nil + } + + var v struct { + Invoices []Invoice `json:"invoices"` + } + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v.Invoices, nil, resp +} + +// SendInvoiceReminder sends a reminder that a payment is due for an existing invoice. +func (c *Client) SendInvoiceReminder(invoiceID string, n *Notification) (error, *http.Response) { + // Do not pass in send_to_payer param + if n != nil { + n.SendToPayer = false + } + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/remind", c.APIBase, invoiceID), n) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// CancelInvoice cancels an invoice and (optionally) notifies the payer of the cancellation. +func (c *Client) CancelInvoice(invoiceID string, n *Notification) (error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/cancel", c.APIBase, invoiceID), n) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// DeleteInvoice deletes a draft invoice. Note that this call works for invoices in the draft state only. +// For invoices that have already been sent, it can be cancelled instead.. Once a draft invoice is +// deleted, it can no longer be used or retrieved, but its invoice number can be reuse. +func (c *Client) DeleteInvoice(invoiceID string) (error, *http.Response) { + req, err := NewRequest("DELETE", fmt.Sprintf("%s/invoicing/invoices/%s", c.APIBase, invoiceID), nil) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// GetInvoiceQRCode returns a QR code as PNG image, in base-64 encoded format. Before getting a QR code, +// an invoice must be created. It is recommended that to specify qrinvoice@paypal.com as the recipient +// email address in the billing_info object. (Use a customer email address only if you want the invoice +// to be emailed.). After the invoice has been created, it must be sent. This step is necessary to move +// the invoice from a draft state to a payable state. As stated above, if you specify +// qrinvoice@paypal.com as the recipient email address, the invoice will not be emailed. +func (c *Client) GetInvoiceQRCode(invoiceID string, width, height int) (string, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/invoicing/invoices/%s/qr-code?width=%s&height%s", c.APIBase, invoiceID, width, height), nil) + if err != nil { + return "", err, nil + } + + var v struct { + Image string `json:"image"` + } + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return "", err, resp + } + + return v.Image, nil, resp +} + +// RecordInvoicePayment marks an invoice as paid +func (c *Client) RecordInvoicePayment(invoiceID string, method PaymentDetailMethod, date *time.Time, note string) (error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/record-payment", c.APIBase, invoiceID), &struct { + Method PaymentDetailMethod `json:"method"` + Date *time.Time `json:"date"` + Note string `json:"note"` + }{ + Method: method, + Date: date, + Note: note, + }) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// RecordInvoiceRefund marks an invoice as refunded +func (c *Client) RecordInvoiceRefund(invoiceID string, date *time.Time, note string) (error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/record-refund", c.APIBase, invoiceID), &struct { + Date *time.Time `json:"date"` + Note string `json:"note"` + }{ + Date: date, + Note: note, + }) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} diff --git a/invoicingtype.go b/invoicingtype.go new file mode 100644 index 0000000..768167a --- /dev/null +++ b/invoicingtype.go @@ -0,0 +1,243 @@ +package paypal + +import "time" + +// https://developer.paypal.com/webapps/developer/docs/api/#common-invoicing-objects + +var ( + InvoiceStatusDraft InvoiceStatus = "DRAFT" + InvoiceStatusSent InvoiceStatus = "SENT" + InvoiceStatusPaid InvoiceStatus = "PAID" + InvoiceStatusMarkedAsPaid InvoiceStatus = "MARKED_AS_PAID" + InvoiceStatusCancelled InvoiceStatus = "CANCELLED" + InvoiceStatusRefunded InvoiceStatus = "REFUNDED" + InvoiceStatusPartiallyRefunded InvoiceStatus = "PARTIALLY_REFUNDED" + InvoiceStatusMarkedAsRefunded InvoiceStatus = "MARKED_AS_REFUNDED" + + BillingInfoLanguageDADK BillingInfoLanguage = "da_DK" + BillingInfoLanguageDEDE BillingInfoLanguage = "de_DE" + BillingInfoLanguageENAU BillingInfoLanguage = "en_AU" + BillingInfoLanguageENGB BillingInfoLanguage = "en_GB" + BillingInfoLanguageENUS BillingInfoLanguage = "en_US" + BillingInfoLanguageESES BillingInfoLanguage = "es_ES" + BillingInfoLanguageESXC BillingInfoLanguage = "es_XC" + BillingInfoLanguageFRCA BillingInfoLanguage = "fr_CA" + BillingInfoLanguageFRFR BillingInfoLanguage = "fr_FR" + BillingInfoLanguageFRXC BillingInfoLanguage = "fr_XC" + BillingInfoLanguageHEIL BillingInfoLanguage = "he_IL" + BillingInfoLanguageIDID BillingInfoLanguage = "id_ID" + BillingInfoLanguageITIT BillingInfoLanguage = "it_IT" + BillingInfoLanguageJAJP BillingInfoLanguage = "ja_JP" + BillingInfoLanguageNLNL BillingInfoLanguage = "nl_NL" + BillingInfoLanguageNONO BillingInfoLanguage = "no_NO" + BillingInfoLanguagePLPL BillingInfoLanguage = "pl_PL" + BillingInfoLanguagePTBR BillingInfoLanguage = "pt_BR" + BillingInfoLanguagePTPT BillingInfoLanguage = "pt_PT" + BillingInfoLanguageRURU BillingInfoLanguage = "ru_RU" + BillingInfoLanguageSVSE BillingInfoLanguage = "sv_SE" + BillingInfoLanguageTHTH BillingInfoLanguage = "th_TH" + BillingInfoLanguageTRTR BillingInfoLanguage = "tr_TR" + BillingInfoLanguageZHCN BillingInfoLanguage = "zh_CN" + BillingInfoLanguageZHHK BillingInfoLanguage = "zh_HK" + BillingInfoLanguageZHTW BillingInfoLanguage = "zh_TW" + BillingInfoLanguageZHXC BillingInfoLanguage = "zh_XC" + + PaymentTermTypeDueOnReceipt PaymentTermType = "DUE_ON_RECEIPT" + PaymentTermTypeNet10 PaymentTermType = "NET_10" + PaymentTermTypeNet15 PaymentTermType = "NET_15" + PaymentTermTypeNet30 PaymentTermType = "NET_30" + PaymentTermTypeNet45 PaymentTermType = "NET_45" + + PaymentDetailTypePaypal PaymentDetailType = "PAYPAL" + PaymentDetailTypeExternal PaymentDetailType = "EXTERNAL" + + PaymentDetailTransactionTypeSale PaymentDetailTransactionType = "SALE" + PaymentDetailTransactionTypeAuthorization PaymentDetailTransactionType = "AUTHORIZATION" + PaymentDetailTransactionTypeCapture PaymentDetailTransactionType = "CAPTURE" + + PaymentDetailMethodBankTransfer PaymentDetailMethod = "BANK_TRANSFER" + PaymentDetailMethodCash PaymentDetailMethod = "CASH" + PaymentDetailMethodCheck PaymentDetailMethod = "CHECK" + PaymentDetailMethodCreditCard PaymentDetailMethod = "CREDIT_CARD" + PaymentDetailMethodDebitCard PaymentDetailMethod = "DEBIT_CARD" + PaymentDetailMethodPaypal PaymentDetailMethod = "PAYPAL" + PaymentDetailMethodWireTransfer PaymentDetailMethod = "WIRE_TRANSFER" + PaymentDetailMethodOther PaymentDetailMethod = "OTHER" + + RefundDetailTypePaypal RefundDetailType = "PAYPAL" + RefundDetailTypeExternal RefundDetailType = "EXTERNAL" +) + +type ( + InvoiceStatus string + BillingInfoLanguage string + PaymentTermType string + PaymentDetailType string + PaymentDetailTransactionType string + PaymentDetailMethod string + RefundDetailType string + + // Invoice maps to invoice object + Invoice struct { + ID string `json:"id"` + Number string `json:"number,omitempty"` + URI string `json:"uri"` + Status InvoiceStatus `json:"status"` + MerchantInfo *MerchantInfo `json:"merchant_info"` + BillingInfo []BillingInfo `json:"billing_info"` + ShippingInfo *ShippingInfo `json:"shipping_info"` + Items []InvoiceItem `json:"items"` + InvoiceDate *time.Time `json:"invoice_date"` + PaymentTerm *PaymentTerm `json:"payment_term,omitempty"` + Discount *Cost `json:"discount,omitempty"` + ShippingCost *ShippingCost `json:"shipping_cost,omitempty"` + Custom *CustomAmount `json:"custom,omitempty"` + TaxCalculatedAfterDiscount bool `json:"tax_calculated_after_discount,omitempty"` + TaxInclusive bool `json:"tax_inclusive"` + Terms string `json:"terms,omitempty"` + Note string `json:"note,omitempty"` + MerchantMemo string `json:"merchant_memo,omitempty"` + LogoURL string `json:"logo_url,omitempty"` + TotalAmount *Currency `json:"total_amount"` + PaymentDetails []PaymentDetail `json:"payment_details"` + RefundDetails []RefundDetail `json:"refund_details"` + Metadata *Metadata `json:"metadata"` + } + + // InvoiceItem maps to invoice_item object + InvoiceItem struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Quantity float64 `json:"quantity"` + UnitPrice *Currency `json:"unit_price"` + Tax *Tax `json:"tax,omitempty"` + Date *time.Time `json:"date,omitempty"` + Discount *Cost `json:"discount,omitempty"` + } + + // MerchantInfo maps to merchant_info object + MerchantInfo struct { + Email string `json:"email"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Address *Address `json:"address,omitempty"` + BusinessName string `json:"business_name,omitempty"` + Phone *Phone `json:"phone,omitempty"` + Fax *Phone `json:"fax,omitempty"` + Website string `json:"website,omitempty"` + TaxID string `json:"tax_id,omitempty"` + AdditionalInfo string `json:"additional_info,omitempty"` + } + + // BillingInfo maps to billing_info object + BillingInfo struct { + Email string `json:"email"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BusinessName string `json:"business_name,omitempty"` + Address *Address `json:"address,omitempty"` + Language BillingInfoLanguage `json:"language,omitempty"` + AdditionalInfo string `json:"additional_info,omitempty"` + } + + // ShippingInfo maps to shipping_info object + ShippingInfo struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BusinessName string `json:"business_name,omitempty"` + Address *Address `json:"address,omitempty"` + } + + // PaymentTerm maps to payment_term object + PaymentTerm struct { + TermType PaymentTermType `json:"term_type"` + DueDate *time.Time `json:"due_date"` + } + + // Cost maps to cost object + Cost struct { + Percent int `json:"percent"` + Amount *Currency `json:"amount"` + } + + // ShippingCost maps to shipping_cost object + ShippingCost struct { + Amount *Currency `json:"amount"` + Tax *Tax `json:"tax"` + } + + // Tax maps to tax object + Tax struct { + ID string `json:"id"` + Name string `json:"name"` + Percent int `json:"percent"` + Amount *Currency `json:"amount"` + } + + // CustomAmount maps to custom_amount object + CustomAmount struct { + Label string `json:"label"` + Amount *Currency `json:"amount"` + } + + // PaymentDetail maps to payment_detail object + PaymentDetail struct { + Type PaymentDetailType `json:"type"` + TransactionID string `json:"transaction_id"` + TransactionType PaymentDetailTransactionType `json:"transaction_type"` + Date *time.Time `json:"date"` + Method PaymentDetailMethod `json:"method"` + Note string `json:"note,omitempty"` + } + + // RefundDetail maps to refund_detail object + RefundDetail struct { + Type RefundDetailType `json:"type"` + Date *time.Time `json:"date"` + Note string `json:"note,omitempty"` + } + + // Metadata maps to metadata object + Metadata struct { + CreatedDate *time.Time `json:"created_date"` + CreatedBy string `json:"created_by"` + CancelledDate *time.Time `json:"cancelled_date"` + CancelledBy string `json:"cancelled_by"` + LastUpdatedDate *time.Time `json:"last_updated_date"` + LastUpdatedBy string `json:"last_updated_by"` + FirstSentDate *time.Time `json:"first_sent_date"` + LastSentDate *time.Time `json:"last_sent_date"` + LastSentBy *time.Time `json:"last_sent_by"` + } + + // Search maps to search object. Invoice search parameters + Search struct { + Email string `json:"email,omitempty"` + RecipientFirstName string `json:"recipient_first_name,omitempty"` + RecipientLastName string `json:"recipient_last_name,omitempty"` + RecipientBusinessName string `json:"recipient_business_name,omitempty"` + Number string `json:"number,omitempty"` + Status InvoiceStatus `json:"status,omitempty"` + LowerTotalAmount *Currency `json:"lower_total_amount,omitempty"` + UpperTotalAmount *Currency `json:"upper_total_amount,omitempty"` + StartInvoiceDate *time.Time `json:"start_invoice_date,omitempty"` + EndInvoiceDate *time.Time `json:"end_invoice_date,omitempty"` + StartDueDate *time.Time `json:"start_due_date,omitempty"` + EndDueDate *time.Time `json:"end_due_date,omitempty"` + StartPaymentDate *time.Time `json:"start_payment_date,omitempty"` + EndPaymentDate *time.Time `json:"end_payment_date,omitempty"` + StartCreationDate *time.Time `json:"start_creation_date,omitempty"` + EndCreationDate *time.Time `json:"end_creation_date,omitempty"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + TotalCountRequired bool `json:"total_count_required,omitempty"` + } + + // Notification maps to notification object. Email/SMS notification + Notification struct { + Subject string `json:"subject,omitempty"` + Note string `json:"note,omitempty"` + SendToMerchant bool `json:"send_to_merchant"` + SendToPayer bool `json:"send_to_payer"` + } +) diff --git a/paymenttype.go b/paymenttype.go index cdc7981..5c2bd5a 100644 --- a/paymenttype.go +++ b/paymenttype.go @@ -19,14 +19,6 @@ var ( CaptureStateRefunded CaptureState = "refunded" CaptureStatePartiallyRefunded CaptureState = "partially_refunded" - CreditCardTypeVisa CreditCardType = "visa" - CreditCardTypeMastercard CreditCardType = "mastercard" - CreditCardTypeDiscover CreditCardType = "discover" - CreditCardTypeAmex CreditCardType = "amex" - - CreditCardStateExpired CreditCardState = "expired" - CreditCardStateOK CreditCardState = "ok" - OrderStatePending OrderState = "PENDING" OrderStateCompleted OrderState = "COMPLETED" OrderStateRefunded OrderState = "REFUNDED" @@ -75,10 +67,6 @@ var ( PaymentStateCanceled PaymentState = "canceled" PaymentStateExpired PaymentState = "expired" - AddressTypeResidential AddressType = "residential" - AddressTypeBusiness AddressType = "business" - AddressTypeMailbox AddressType = "mailbox" - PaymentIntentSale PaymentIntent = "sale" PaymentIntentAuthorize PaymentIntent = "authorize" PaymentIntentOrder PaymentIntent = "order" @@ -101,8 +89,6 @@ var ( type ( AuthorizationState string CaptureState string - CreditCardType string - CreditCardState string OrderState string PendingReason string ReasonCode string @@ -110,7 +96,6 @@ type ( ProtectionEligibilityType string TaxIDType string PaymentState string - AddressType string PaymentMethod string PayerStatus string PaymentIntent string @@ -119,15 +104,7 @@ type ( SalePaymentMode string // Address maps to address object - Address struct { - Line1 string `json:"line1"` - Line2 string `json:"line2,omitempty"` - City string `json:"city"` - CountryCode string `json:"country_code"` - PostalCode string `json:"postal_code,omitempty"` - State string `json:"state,omitempty"` - Phone string `json:"phone,omitempty"` - } + // See commontype.go // Amount maps to the amount object Amount struct { @@ -190,29 +167,10 @@ type ( } // CreditCard maps to credit_card object - CreditCard struct { - ID string `json:"id,omitempty"` - PayerID string `json:"payer_id,omitempty"` - Number string `json:"number"` - Type CreditCardType `json:"type"` - ExpireMonth string `json:"expire_month"` - ExpireYear string `json:"expire_year"` - CVV2 string `json:"cvv2,omitempty"` - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - BillingAddress *Address `json:"billing_address,omitempty"` - State CreditCardState `json:"state,omitempty"` - ValidUntil string `json:"valid_until,omitempty"` - } + // See commontype.go // CreditCardToken maps to credit_card_token object - CreditCardToken struct { - CreditCardID string `json:"credit_card_id"` - PayerID string `json:"payer_id,omitempty"` - Last4 string `json:"last4,omitempty"` - ExpireYear string `json:"expire_year,omitempty"` - ExpireMonth string `json:"expire_month,omitempty"` - } + // See commontype.go // FundingInstrument maps to funding_instrument object FundingInstrument struct { From 284bc8e9f791d3851abf7e81a22f15afc3a875da Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Wed, 8 Oct 2014 11:30:21 +0200 Subject: [PATCH 10/20] Added notification endpoint --- notification.go | 194 ++++++++++++++++++++++++++++++++++++++++++++ notificationtype.go | 46 +++++++++++ 2 files changed, 240 insertions(+) create mode 100644 notification.go create mode 100644 notificationtype.go diff --git a/notification.go b/notification.go new file mode 100644 index 0000000..1c5f267 --- /dev/null +++ b/notification.go @@ -0,0 +1,194 @@ +package paypal + +import ( + "fmt" + "net/http" +) + +// https://developer.paypal.com/webapps/developer/docs/api/#notifications + +// ListWebhookEventTypes returns a list of events types that are available to any +// webhook for subscription +func (c *Client) ListWebhookEventTypes() ([]EventType, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks-event-types", c.APIBase), nil) + if err != nil { + return nil, err, nil + } + + var v struct { + EventTypes []EventType `json:"event_types"` + } + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v.EventTypes, nil, resp +} + +// CreateWebhook creates a webhook. The maximum number of webhooks allowed to be registered +// is 10. +func (c *Client) CreateWebhook(w *Webhook) (*Webhook, error, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/notifications/webhooks", c.APIBase), w) + if err != nil { + return nil, err, nil + } + + v := &Webhook{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// GetWebhook returns a specific webhook. +func (c *Client) GetWebhook(webhookID string) (*Webhook, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks/%s", c.APIBase, webhookID), nil) + if err != nil { + return nil, err, nil + } + + v := &Webhook{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// ListEventTypesByWebhook returns a list of events types that are subscribed to a webhook +func (c *Client) ListEventTypesByWebhook(webhookID string) ([]EventType, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks/%s/event-types", c.APIBase, webhookID), nil) + if err != nil { + return nil, err, nil + } + + var v struct { + EventTypes []EventType `json:"event_types"` + } + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v.EventTypes, nil, resp +} + +// ListWebhooks returns all webhooks +func (c *Client) ListWebhooks() ([]Webhook, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks-event-types", c.APIBase), nil) + if err != nil { + return nil, err, nil + } + + var v struct { + Webhooks []Webhook `json:"webhooks"` + } + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v.Webhooks, nil, resp +} + +// UpdateWebhook updates a webhook +func (c *Client) UpdateWebhook(w *Webhook) (*Webhook, error, *http.Response) { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/notifications/webhooks/%s", c.APIBase, w.ID), struct { + Path string `json:"path"` + Value *Webhook `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: w, + OP: PatchOperationReplace, + }) + if err != nil { + return nil, err, nil + } + + v := &Webhook{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// DeleteWebhook delets a webhook +func (c *Client) DeleteWebhook(webhookID string) (error, *http.Response) { + req, err := NewRequest("DELETE", fmt.Sprintf("%s/notifications/webhooks/%s", c.APIBase, webhookID), nil) + if err != nil { + return err, nil + } + + v := &struct{}{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return err, resp + } + + return nil, resp +} + +// GetWebhookEvent returns a webhook event +func (c *Client) GetWebhookEvent(eventID string) (*Event, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks-events/%s", c.APIBase, eventID), nil) + if err != nil { + return nil, err, nil + } + + v := &Event{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// SearchWebhookEvents searches for all webhook events +func (c *Client) SearchWebhookEvents(s *WebhookEventSearch) (*EventList, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks-events", c.APIBase), s) + if err != nil { + return nil, err, nil + } + + v := &EventList{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// ResendWebhookEvent resend the event notification +func (c *Client) ResendWebhookEvent(eventID string) (*Event, error, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/notifications/webhooks-events/%s/resend", c.APIBase, eventID), nil) + if err != nil { + return nil, err, nil + } + + v := &Event{} + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} diff --git a/notificationtype.go b/notificationtype.go new file mode 100644 index 0000000..58c27e1 --- /dev/null +++ b/notificationtype.go @@ -0,0 +1,46 @@ +package paypal + +import "time" + +// https://developer.paypal.com/webapps/developer/docs/api/#common-notifications-objects + +type ( + // EventList maps to event_list object. List of Webhooks event resources + EventList struct { + Events []Event `json:"events"` + Count int `json:"count"` + Links []Links `json:"links"` + } + + // EventType maps to event_type object. Contaisn the information for a Webhooks event-type + EventType struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + } + + // Webhook maps to webhook object. Represents Webhook resource + Webhook struct { + ID string `json:"id,omitempty"` + URL string `json:"url"` + EventTypes []EventType `json:"event_types"` + Links []Links `json:"links,omitempty"` + } + + // Event maps to event object. Represents a Webhooks event + Event struct { + ID string `json:"id,omitempty"` + CreateTime *time.Time `json:"create_time,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + EventType string `json:"event_type,omitempty"` + Summary string `json:"summary,omitempty"` + Resource interface{} `json:"resource,omitempty"` + Links []Links `json:"links,omitempty"` + } + + // WebhookEventSearch is search parameters for webhook events + WebhookEventSearch struct { + PageSize int `json:"page_size,omitempty"` + StartTime *time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + } +) From 83e3fc7c981c538dc6ee41bb12b22d5138ff24d5 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Thu, 9 Oct 2014 05:34:20 +0200 Subject: [PATCH 11/20] Added tests for Vault endpoint and refactored how errors are handled from responses --- billing.go | 28 ++++--------- e2e_test.go => e2epayment_test.go | 0 e2evault_test.go | 68 +++++++++++++++++++++++++++++++ invoicing.go | 24 +++-------- paypal.go | 1 + vault.go | 8 ++-- 6 files changed, 85 insertions(+), 44 deletions(-) rename e2e_test.go => e2epayment_test.go (100%) create mode 100644 e2evault_test.go diff --git a/billing.go b/billing.go index 6bdebac..677ac85 100644 --- a/billing.go +++ b/billing.go @@ -48,9 +48,7 @@ func (c *Client) UpdateBillingPlan(p *Plan) (error, *http.Response) { return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -155,9 +153,7 @@ func (c *Client) UpdateAgreement(a *Agreement) (error, *http.Response) { return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -193,9 +189,7 @@ func (c *Client) SuspendAgreement(agreementID, note string) (error, *http.Respon return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } @@ -211,9 +205,7 @@ func (c *Client) ReactivateAgreement(agreementID, note string) (error, *http.Res return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } @@ -256,9 +248,7 @@ func (c *Client) CancelAgreement(agreementID, note string) (error, *http.Respons return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } @@ -270,9 +260,7 @@ func (c *Client) SetAgreementBalance(agreementID string, currency *Currency) (er return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } @@ -290,9 +278,7 @@ func (c *Client) BillAgreementBalance(agreementID string, currency *Currency, no return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } diff --git a/e2e_test.go b/e2epayment_test.go similarity index 100% rename from e2e_test.go rename to e2epayment_test.go diff --git a/e2evault_test.go b/e2evault_test.go new file mode 100644 index 0000000..5a02a3f --- /dev/null +++ b/e2evault_test.go @@ -0,0 +1,68 @@ +package paypal + +import ( + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestVault(t *testing.T) { + withContext(func(client *Client) { + Convey("With the vault endpoint", t, func() { + + Convey("Storing a credit card with valid data should be successful", func() { + creditCard := CreditCard{ + PayerID: "user12345", + Type: CreditCardTypeVisa, + Number: "4417119669820331", + ExpireMonth: "11", + ExpireYear: "2018", + FirstName: "Betsy", + LastName: "Buyer", + } + + newCreditCard, err, resp := client.StoreCreditCard(&creditCard) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newCreditCard.ID, ShouldNotBeNil) + So(newCreditCard.Number, ShouldEqual, "xxxxxxxxxxxx0331") + + Convey("Retrieving the stored credit card should be successful", func() { + creditCard, err, resp := client.GetStoredCreditCard(newCreditCard.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(creditCard, ShouldResemble, newCreditCard) + + // Skip this test for now as it seems the endpoint does not behave as specified in documentation + SkipConvey("Updating the stored credit card should updates the data", func() { + newCreditCard.FirstName = "Carol" + creditCard, err, resp := client.UpdateStoredCreditCard(newCreditCard.ID, newCreditCard) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(creditCard.FirstName, ShouldEqual, newCreditCard.FirstName) + }) + + Convey("Deleting the stored credit card should be successful", func() { + err, resp := client.DeleteStoredCreditCard(newCreditCard.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + + Convey("Retrieving the deleted credit card should not return any data", func() { + creditCard, err, resp := client.GetStoredCreditCard(newCreditCard.ID) + + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + So(err, ShouldNotBeNil) + So(creditCard, ShouldEqual, nil) + }) + }) + }) + + }) + }) + }) +} diff --git a/invoicing.go b/invoicing.go index 58d4fed..fb88400 100644 --- a/invoicing.go +++ b/invoicing.go @@ -34,9 +34,7 @@ func (c *Client) SendInvoice(invoiceID string) (error, *http.Response) { return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -137,9 +135,7 @@ func (c *Client) SendInvoiceReminder(invoiceID string, n *Notification) (error, return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -154,9 +150,7 @@ func (c *Client) CancelInvoice(invoiceID string, n *Notification) (error, *http. return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -173,9 +167,7 @@ func (c *Client) DeleteInvoice(invoiceID string) (error, *http.Response) { return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -222,9 +214,7 @@ func (c *Client) RecordInvoicePayment(invoiceID string, method PaymentDetailMeth return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } @@ -245,9 +235,7 @@ func (c *Client) RecordInvoiceRefund(invoiceID string, date *time.Time, note str return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, &v) + resp, err := c.SendWithAuth(req, nil) if err != nil { return err, resp } diff --git a/paypal.go b/paypal.go index da641f0..a9d6978 100644 --- a/paypal.go +++ b/paypal.go @@ -134,6 +134,7 @@ func (c *Client) Send(req *http.Request, v interface{}) (*http.Response, error) if c := resp.StatusCode; c < 200 || c > 299 { errResp := &ErrorResponse{Response: resp} data, err := ioutil.ReadAll(resp.Body) + log.Println(string(data)) if err == nil && len(data) > 0 { json.Unmarshal(data, errResp) } diff --git a/vault.go b/vault.go index 1ededb1..300954e 100644 --- a/vault.go +++ b/vault.go @@ -35,9 +35,7 @@ func (c *Client) DeleteStoredCreditCard(creditCardID string) (error, *http.Respo return err, nil } - v := &struct{}{} - - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, nil) return err, resp } @@ -60,8 +58,8 @@ func (c *Client) GetStoredCreditCard(creditCardID string) (*CreditCard, error, * } // UpdateStoredCreditCard modifies a stored credit card -func (c *Client) UpdateStoredCreditCard(creditCard *CreditCard) (*CreditCard, error, *http.Response) { - req, err := NewRequest("PATCH", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCard.ID), struct { +func (c *Client) UpdateStoredCreditCard(creditCardID string, creditCard *CreditCard) (*CreditCard, error, *http.Response) { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCardID), struct { Path string `json:"path"` Value *CreditCard `json:"value"` OP PatchOperation `json:"op"` From 6491d55ddcde18f8fc8bceaf37a02af22c32148e Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Thu, 9 Oct 2014 06:01:06 +0200 Subject: [PATCH 12/20] Updated readme --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a1c1a57..d47f160 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Payment REST API Go client -[![Coverage Status](https://coveralls.io/repos/fundary/paypal/badge.png)](https://coveralls.io/r/fundary/paypal) [![Build Status](https://travis-ci.org/fundary/paypal.svg?branch=develop)](https://travis-ci.org/fundary/paypal) [![GoDoc](https://godoc.org/github.com/fundary/paypal?status.svg)](https://godoc.org/github.com/fundary/paypal) +[![Build Status](https://travis-ci.org/fundary/paypal.svg?branch=develop)](https://travis-ci.org/fundary/paypal) [![GoDoc](https://godoc.org/github.com/fundary/paypal?status.svg)](https://godoc.org/github.com/fundary/paypal) A Go client for the Paypal REST API ([https://developer.paypal.com/webapps/developer/docs/api/](https://developer.paypal.com/webapps/developer/docs/api/)) @@ -42,7 +42,7 @@ func main() { client := paypal.NewClient(clientID, secret, paypal.APIBaseLive) - payments, err := client.ListPayments(map[string]string{ + payments, err, _ := client.ListPayments(map[string]string{ "count": "10", "sort_by": "create_time", }) @@ -76,8 +76,9 @@ PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] go te - [x] [Payments - Authorizations](https://developer.paypal.com/webapps/developer/docs/api/#authorizations) - [x] [Payments - Captures](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) - [x] [Payments - Billing Plans and Agreements](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) -- [ ] [Payments - Order](https://developer.paypal.com/webapps/developer/docs/api/#orders) -- [ ] [Vault](https://developer.paypal.com/webapps/developer/docs/api/#vault) +- [x] [Payments - Order](https://developer.paypal.com/webapps/developer/docs/api/#orders) +- [x] [Vault](https://developer.paypal.com/webapps/developer/docs/api/#vault) - [ ] [Identity](https://developer.paypal.com/webapps/developer/docs/api/#identity) -- [ ] [Invoicing](https://developer.paypal.com/webapps/developer/docs/api/#invoicing) +- [x] [Invoicing](https://developer.paypal.com/webapps/developer/docs/api/#invoicing) - [ ] [Payment Experience](https://developer.paypal.com/webapps/developer/docs/api/#payment-experience) +- [x] [Notifications](https://developer.paypal.com/webapps/developer/docs/api/#notifications) From 7cb6fd9b494ed95adcd474bc38bd8b3bc39dcbf8 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Thu, 9 Oct 2014 10:50:03 +0200 Subject: [PATCH 13/20] Added Paypal date type --- billingagreementtype.go | 2 +- date.go | 36 +++++++++++++++++++++++++ invoicingtype.go | 58 ++++++++++++++++++++--------------------- notificationtype.go | 2 +- paypal.go | 10 +++---- 5 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 date.go diff --git a/billingagreementtype.go b/billingagreementtype.go index 57d7fbd..6b77b96 100644 --- a/billingagreementtype.go +++ b/billingagreementtype.go @@ -41,7 +41,7 @@ type ( ID string `json:"id"` Name string `json:"name"` Description string `json:"desription"` - StartDate *time.Time `json:"start_date"` + StartDate *Date `json:"start_date"` Payer *AgreementPayer `json:"payer"` ShippingAddress *Address `json:"shipping_address,omitempty"` OverrideMerchantPreferences *MerchantPreferences `json:"override_merchant_preferences,omitempty"` diff --git a/date.go b/date.go new file mode 100644 index 0000000..4cc7fa7 --- /dev/null +++ b/date.go @@ -0,0 +1,36 @@ +package paypal + +import ( + "errors" + "fmt" + "time" +) + +const ( + ISO8601 = "2006-01-02 MST" +) + +type Date struct { + time.Time +} + +// String returns the formatted date +func (d Date) String() string { + return d.Time.Format(ISO8601) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Date) MarshalJSON() ([]byte, error) { + if y := d.Year(); y < 0 || y >= 10000 { + // ISO8601 is clear that years are 4 digits exactly. + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + return []byte(d.Format(`"` + ISO8601 + `"`)), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Date) UnmarshalJSON(data []byte) (err error) { + fmt.Println(string(data)) + d.Time, err = time.Parse(`"`+ISO8601+`"`, string(data)) + return +} diff --git a/invoicingtype.go b/invoicingtype.go index 768167a..d910fa3 100644 --- a/invoicingtype.go +++ b/invoicingtype.go @@ -1,7 +1,5 @@ package paypal -import "time" - // https://developer.paypal.com/webapps/developer/docs/api/#common-invoicing-objects var ( @@ -87,7 +85,7 @@ type ( BillingInfo []BillingInfo `json:"billing_info"` ShippingInfo *ShippingInfo `json:"shipping_info"` Items []InvoiceItem `json:"items"` - InvoiceDate *time.Time `json:"invoice_date"` + InvoiceDate *Date `json:"invoice_date"` PaymentTerm *PaymentTerm `json:"payment_term,omitempty"` Discount *Cost `json:"discount,omitempty"` ShippingCost *ShippingCost `json:"shipping_cost,omitempty"` @@ -106,13 +104,13 @@ type ( // InvoiceItem maps to invoice_item object InvoiceItem struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Quantity float64 `json:"quantity"` - UnitPrice *Currency `json:"unit_price"` - Tax *Tax `json:"tax,omitempty"` - Date *time.Time `json:"date,omitempty"` - Discount *Cost `json:"discount,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Quantity float64 `json:"quantity"` + UnitPrice *Currency `json:"unit_price"` + Tax *Tax `json:"tax,omitempty"` + Date *Date `json:"date,omitempty"` + Discount *Cost `json:"discount,omitempty"` } // MerchantInfo maps to merchant_info object @@ -151,7 +149,7 @@ type ( // PaymentTerm maps to payment_term object PaymentTerm struct { TermType PaymentTermType `json:"term_type"` - DueDate *time.Time `json:"due_date"` + DueDate *Date `json:"due_date"` } // Cost maps to cost object @@ -185,7 +183,7 @@ type ( Type PaymentDetailType `json:"type"` TransactionID string `json:"transaction_id"` TransactionType PaymentDetailTransactionType `json:"transaction_type"` - Date *time.Time `json:"date"` + Date *Date `json:"date"` Method PaymentDetailMethod `json:"method"` Note string `json:"note,omitempty"` } @@ -193,21 +191,21 @@ type ( // RefundDetail maps to refund_detail object RefundDetail struct { Type RefundDetailType `json:"type"` - Date *time.Time `json:"date"` + Date *Date `json:"date"` Note string `json:"note,omitempty"` } // Metadata maps to metadata object Metadata struct { - CreatedDate *time.Time `json:"created_date"` - CreatedBy string `json:"created_by"` - CancelledDate *time.Time `json:"cancelled_date"` - CancelledBy string `json:"cancelled_by"` - LastUpdatedDate *time.Time `json:"last_updated_date"` - LastUpdatedBy string `json:"last_updated_by"` - FirstSentDate *time.Time `json:"first_sent_date"` - LastSentDate *time.Time `json:"last_sent_date"` - LastSentBy *time.Time `json:"last_sent_by"` + CreatedDate *Date `json:"created_date"` + CreatedBy string `json:"created_by"` + CancelledDate *Date `json:"cancelled_date"` + CancelledBy string `json:"cancelled_by"` + LastUpdatedDate *Date `json:"last_updated_date"` + LastUpdatedBy string `json:"last_updated_by"` + FirstSentDate *Date `json:"first_sent_date"` + LastSentDate *Date `json:"last_sent_date"` + LastSentBy *Date `json:"last_sent_by"` } // Search maps to search object. Invoice search parameters @@ -220,14 +218,14 @@ type ( Status InvoiceStatus `json:"status,omitempty"` LowerTotalAmount *Currency `json:"lower_total_amount,omitempty"` UpperTotalAmount *Currency `json:"upper_total_amount,omitempty"` - StartInvoiceDate *time.Time `json:"start_invoice_date,omitempty"` - EndInvoiceDate *time.Time `json:"end_invoice_date,omitempty"` - StartDueDate *time.Time `json:"start_due_date,omitempty"` - EndDueDate *time.Time `json:"end_due_date,omitempty"` - StartPaymentDate *time.Time `json:"start_payment_date,omitempty"` - EndPaymentDate *time.Time `json:"end_payment_date,omitempty"` - StartCreationDate *time.Time `json:"start_creation_date,omitempty"` - EndCreationDate *time.Time `json:"end_creation_date,omitempty"` + StartInvoiceDate *Date `json:"start_invoice_date,omitempty"` + EndInvoiceDate *Date `json:"end_invoice_date,omitempty"` + StartDueDate *Date `json:"start_due_date,omitempty"` + EndDueDate *Date `json:"end_due_date,omitempty"` + StartPaymentDate *Date `json:"start_payment_date,omitempty"` + EndPaymentDate *Date `json:"end_payment_date,omitempty"` + StartCreationDate *Date `json:"start_creation_date,omitempty"` + EndCreationDate *Date `json:"end_creation_date,omitempty"` Page int `json:"page,omitempty"` PageSize int `json:"page_size,omitempty"` TotalCountRequired bool `json:"total_count_required,omitempty"` diff --git a/notificationtype.go b/notificationtype.go index 58c27e1..ba3d1af 100644 --- a/notificationtype.go +++ b/notificationtype.go @@ -29,7 +29,7 @@ type ( // Event maps to event object. Represents a Webhooks event Event struct { ID string `json:"id,omitempty"` - CreateTime *time.Time `json:"create_time,omitempty"` + CreateTime *Date `json:"create_time,omitempty"` ResourceType string `json:"resource_type,omitempty"` EventType string `json:"event_type,omitempty"` Summary string `json:"summary,omitempty"` diff --git a/paypal.go b/paypal.go index a9d6978..03bfadd 100644 --- a/paypal.go +++ b/paypal.go @@ -36,11 +36,11 @@ type ( // HTTP response that caused this error Response *http.Response `json:"-"` - Name string `json:"name"` - DebugID string `json:"debug_id"` - Message string `json:"message"` - InformationLink string `json:"information_link"` - Details ErrorDetails `json:"details"` + Name string `json:"name"` + DebugID string `json:"debug_id"` + Message string `json:"message"` + InformationLink string `json:"information_link"` + Details []ErrorDetails `json:"details"` } // ErrorDetails map to error_details object From c1d06a38634d417b9afdb45213ad59f7f120a0d5 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Sun, 12 Oct 2014 23:01:17 +0200 Subject: [PATCH 14/20] Added tests for invoice and billing --- billing.go | 18 ++-- billingagreementtype.go | 64 +++++++------- billingplantype.go | 70 ++++++++------- commontype.go | 22 +++++ date.go | 36 -------- datetime.go | 83 +++++++++++++++++ e2ebilling_test.go | 136 ++++++++++++++++++++++++++++ e2einvoice_test.go | 191 ++++++++++++++++++++++++++++++++++++++++ e2enotification_test.go | 60 +++++++++++++ e2evault_test.go | 6 +- invoicing.go | 20 ++--- invoicingtype.go | 83 ++++++++--------- notification.go | 2 +- paypal.go | 1 + vault.go | 8 +- 15 files changed, 635 insertions(+), 165 deletions(-) delete mode 100644 date.go create mode 100644 datetime.go create mode 100644 e2ebilling_test.go create mode 100644 e2einvoice_test.go create mode 100644 e2enotification_test.go diff --git a/billing.go b/billing.go index 677ac85..83339a8 100644 --- a/billing.go +++ b/billing.go @@ -34,15 +34,21 @@ func (c *Client) CreateBillingPlan(p *Plan) (*Plan, error, *http.Response) { // UpdateBillingPlan updates data of an existing billing plan. The state of a plan // must be PlanStateActive before a billing agreement is created -func (c *Client) UpdateBillingPlan(p *Plan) (error, *http.Response) { - req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, p.ID), struct { +func (c *Client) UpdateBillingPlan(planID string, p *PatchPlan) (error, *http.Response) { + req, err := NewRequest("PATCH", fmt.Sprintf("%s/payments/billing-plans/%s", c.APIBase, planID), []struct { Path string `json:"path"` - Value *Plan `json:"value"` + Value *PatchPlan `json:"value"` OP PatchOperation `json:"op"` }{ - Path: "/", - Value: p, - OP: PatchOperationReplace, + struct { + Path string `json:"path"` + Value *PatchPlan `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: p, + OP: PatchOperationReplace, + }, }) if err != nil { return err, nil diff --git a/billingagreementtype.go b/billingagreementtype.go index 6b77b96..9658c46 100644 --- a/billingagreementtype.go +++ b/billingagreementtype.go @@ -38,37 +38,37 @@ type ( // Agreement maps to agreement object Agreement struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Name string `json:"name"` - Description string `json:"desription"` - StartDate *Date `json:"start_date"` + Description string `json:"description"` + StartDate *DatetimeRFC3339 `json:"start_date"` Payer *AgreementPayer `json:"payer"` ShippingAddress *Address `json:"shipping_address,omitempty"` OverrideMerchantPreferences *MerchantPreferences `json:"override_merchant_preferences,omitempty"` OverrideChargeModels []ChargeModels `json:"override_charge_models,omitempty"` - Plan *Plan `json:"plan"` - CreateTime *time.Time `json:"create_time"` - UpdateTime *time.Time `json:"update_time"` - Links []Links `json:"links"` + Plan *PatchPlan `json:"plan"` + CreateTime *time.Time `json:"create_time,omitempty"` + UpdateTime *time.Time `json:"update_time,omitempty"` + Links []Links `json:"links,omitempty"` } // AgreementPayer maps to the payer object in Billing Agreements AgreementPayer struct { PaymentMethod PaymentMethod `json:"payment_method"` - FundingInstruments []AgreementFundingInstrument `json:"funding_instruments"` - FundingOptionID string `json:"funding_option_id"` - PayerInfo *AgreementPayerInfo `json:"payer_info"` + FundingInstruments []AgreementFundingInstrument `json:"funding_instruments,omitempty"` + FundingOptionID string `json:"funding_option_id,omitempty"` + PayerInfo *AgreementPayerInfo `json:"payer_info,omitempty"` } // AgreementFundingInstrument maps to the funding_instrument object in Billing Agreements AgreementFundingInstrument struct { - CreditCard *AgreementCreditCard `json:"credit_card"` - CreditCardToken *CreditCardToken `json:"credit_card_token"` - PaymentCard *PaymentCard `json:"payment_card"` - PaymentCardToken *PaymentCardToken `json:"payment_card_token"` - BankAccount string `json:"bank_account"` - BankAccountToken *BankToken `json:"bank_token"` - Credit *Credit `json:"credit"` + CreditCard *AgreementCreditCard `json:"credit_card,omitempty"` + CreditCardToken *CreditCardToken `json:"credit_card_token,omitempty"` + PaymentCard *PaymentCard `json:"payment_card,omitempty"` + PaymentCardToken *PaymentCardToken `json:"payment_card_token,omitempty"` + BankAccount string `json:"bank_account,omitempty"` + BankAccountToken *BankToken `json:"bank_token,omitempty"` + Credit *Credit `json:"credit,omitempty"` } // AgreementCreditCard maps to the credit_card object in Billing Agreements @@ -89,7 +89,7 @@ type ( // PaymentCard maps to payment_card object PaymentCard struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Number string `json:"number"` Type PaymentCardType `json:"type"` ExpireMonth string `json:"expire_month"` @@ -111,10 +111,10 @@ type ( PaymentCardToken struct { PaymentCardID string `json:"payment_card_id"` ExternalCustomerID string `json:"external_customer_id"` - Last4 string `json:"last4"` + Last4 string `json:"last4,omitempty"` Type PaymentCardType `json:"type"` - ExpireMonth string `json:"expire_month"` - ExpireYear string `json:"expire_year"` + ExpireMonth string `json:"expire_month,omitempty"` + ExpireYear string `json:"expire_year,omitempty"` } // BankToken maps to bank_token object @@ -128,9 +128,9 @@ type ( // Credit maps to credit object // A resource representing a credit instrument. Credit struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Type CreditType `json:"type"` - Terms string `json:"terms"` + Terms string `json:"terms,omitempty"` } // AgreementPayerInfo maps to payer_info object in billing agreement @@ -148,9 +148,9 @@ type ( // AgreementShippingAddress maps to shipping_address object in billing agreement // Extended Address object used as shipping address in a payment. AgreementShippingAddress struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` RecipientName string `json:"recipient_name"` - DefaultAddress bool `json:"default_address"` + DefaultAddress bool `json:"default_address,omitempty"` Line1 string `json:"line1"` Line2 string `json:"line2,omitempty"` City string `json:"city"` @@ -186,15 +186,15 @@ type ( // A resource representing an agreement_transaction that is returned during // a transaction search. AgreementTransaction struct { - TransactionID string `json:"transaction_id"` - Status string `json:"status"` - TransactionType string `json:"transaction_type"` + TransactionID string `json:"transaction_id,omitempty"` + Status string `json:"status,omitempty"` + TransactionType string `json:"transaction_type,omitempty"` Amount *Currency `json:"amount"` FeeAmount *Currency `json:"fee_amount"` NetAmount *Currency `json:"net_amount"` - PayerEmail string `json:"payer_email"` - PayerName string `json:"payer_name"` - TimeUpdated string `json:"time_updated"` - TimeZone string `json:"time_zone"` + PayerEmail string `json:"payer_email,omitempty"` + PayerName string `json:"payer_name,omitempty"` + TimeUpdated string `json:"time_updated,omitempty"` + TimeZone string `json:"time_zone,omitempty"` } ) diff --git a/billingplantype.go b/billingplantype.go index 0a837f1..44fd7c1 100644 --- a/billingplantype.go +++ b/billingplantype.go @@ -5,8 +5,8 @@ import "time" // https://developer.paypal.com/webapps/developer/docs/api/#plan-object var ( - PlanTypeFixed PlanType = "FIXED" - PlanTypeInfinite PlanType = "INFINITE" + PlanTypeFixed PlanType = "fixed" + PlanTypeInfinite PlanType = "infinite" PlanStateCreated PlanState = "CREATED" PlanStateActive PlanState = "ACTIVE" @@ -15,8 +15,8 @@ var ( PaymentDefinitionTypeTrial PaymentDefinitionType = "TRIAL" PaymentDefinitionTypeRegular PaymentDefinitionType = "REGULAR" - ChargeModelsTypeShipping ChargeModelsType = "shipping" - ChargeModelsTypeTax ChargeModelsType = "tax" + ChargeModelsTypeShipping ChargeModelsType = "SHIPPING" + ChargeModelsTypeTax ChargeModelsType = "TAX" PatchOperationAdd PatchOperation = "add" PatchOperationRemove PatchOperation = "remove" @@ -36,18 +36,34 @@ type ( // Plan maps to plan object Plan struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Name string `json:"name"` Description string `json:"description"` Type PlanType `json:"type"` - State PlanState `json:"state"` - Payee *Payee `json:"payee"` - CreateTime *time.Time `json:"create_time"` - UpdateTime *time.Time `json:"update_time"` - PaymentDefinitions []PaymentDefinition `json:"payment_definitions"` - Terms []Terms `json:"terms"` + State PlanState `json:"state,omitempty"` + Payee *Payee `json:"payee,omitempty"` + CreateTime *time.Time `json:"create_time,omitempty"` + UpdateTime *time.Time `json:"update_time,omitempty"` + PaymentDefinitions []PaymentDefinition `json:"payment_definitions,omitempty"` + Terms []Terms `json:"terms,omitempty"` MerchantPreferences *MerchantPreferences `json:"merchant_preferences,omitempty"` - Links []Links `json:"links"` + Links []Links `json:"links,omitempty"` + } + + // PatchPlan is used in PATCH reqeusts + PatchPlan struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Type PlanType `json:"type,omitempty"` + State PlanState `json:"state,omitempty"` + Payee *Payee `json:"payee,omitempty"` + CreateTime *time.Time `json:"create_time,omitempty"` + UpdateTime *time.Time `json:"update_time,omitempty"` + PaymentDefinitions []PaymentDefinition `json:"payment_definitions,omitempty"` + Terms []Terms `json:"terms,omitempty"` + MerchantPreferences *MerchantPreferences `json:"merchant_preferences,omitempty"` + Links []Links `json:"links,omitempty"` } // Payee maps to payee object @@ -59,15 +75,11 @@ type ( } // Phone maps to phone object - Phone struct { - CountryCode string `json:"country_code"` - NationalNumber string `json:"national_number"` - Extension string `json:"extension,omitempty"` - } + // See commontype.go // PaymentDefinition maps to payment_definition object PaymentDefinition struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Name string `json:"name"` Type PaymentDefinitionType `json:"type"` FrequencyInterval string `json:"frequency_interval"` @@ -79,14 +91,14 @@ type ( // ChargeModels maps to charge_models object ChargeModels struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Type ChargeModelsType `json:"type"` Amount *Currency `json:"amount"` } // Terms maps to terms object Terms struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Type TermType `json:"type"` MaxBillingAmount *Currency `json:"max_billing_amount"` Occurrences string `json:"occurrences"` @@ -96,24 +108,16 @@ type ( // MerchantPreferences maps to merchant_preferences boject MerchantPreferences struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` SetupFee *Currency `json:"setup_fee,omitempty"` CancelURL string `json:"cancel_url"` ReturnURL string `json:"return_url"` - NotifyURL string `json:"notify_url"` - MaxFailAttemps string `json:"max_fail_attemps,omitempty"` // Default is 0, which is unlimited + NotifyURL string `json:"notify_url,omitempty"` + MaxFailAttempts string `json:"max_fail_attempts,omitempty"` // Default is 0, which is unlimited AutoBillAmount string `json:"auto_bill_amount,omitempty,omitempty"` InitialFailAmountAction string `json:"initial_fail_amount_action,omitempty"` - AcceptedPaymentType string `json:"accepted_payment_type"` - CharSet string `json:"char_set"` - } - - // PatchRequest maps to patch_request object - PatchRequest struct { - OP PatchOperation `json:"op"` - Path string `json:"path"` - Value string `json:"value"` - From string `json:"from"` + AcceptedPaymentType string `json:"accepted_payment_type,omitempty"` + CharSet string `json:"char_set,omitempty"` } // PlanList maps to plan_list object diff --git a/commontype.go b/commontype.go index 1d352f6..4490309 100644 --- a/commontype.go +++ b/commontype.go @@ -64,6 +64,21 @@ type ( ValidUntil string `json:"valid_until,omitempty"` } + // PatchCreditCard is used in PATCH requests for updating credit cards (e.g. in vault endpoint) + PatchCreditCard struct { + PayerID string `json:"payer_id,omitempty"` + Number string `json:"number,omitempty"` + Type CreditCardType `json:"type,omitempty"` + ExpireMonth string `json:"expire_month,omitempty"` + ExpireYear string `json:"expire_year,omitempty"` + CVV2 string `json:"cvv2,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + State CreditCardState `json:"state,omitempty"` + ValidUntil string `json:"valid_until,omitempty"` + } + // CreditCardToken maps to credit_card_token object CreditCardToken struct { CreditCardID string `json:"credit_card_id"` @@ -72,4 +87,11 @@ type ( ExpireYear string `json:"expire_year,omitempty"` ExpireMonth string `json:"expire_month,omitempty"` } + + // Phone maps to phone object + Phone struct { + CountryCode string `json:"country_code"` + NationalNumber string `json:"national_number"` + Extension string `json:"extension,omitempty"` + } ) diff --git a/date.go b/date.go deleted file mode 100644 index 4cc7fa7..0000000 --- a/date.go +++ /dev/null @@ -1,36 +0,0 @@ -package paypal - -import ( - "errors" - "fmt" - "time" -) - -const ( - ISO8601 = "2006-01-02 MST" -) - -type Date struct { - time.Time -} - -// String returns the formatted date -func (d Date) String() string { - return d.Time.Format(ISO8601) -} - -// MarshalJSON implements the json.Marshaler interface. -func (d Date) MarshalJSON() ([]byte, error) { - if y := d.Year(); y < 0 || y >= 10000 { - // ISO8601 is clear that years are 4 digits exactly. - return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") - } - return []byte(d.Format(`"` + ISO8601 + `"`)), nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (d *Date) UnmarshalJSON(data []byte) (err error) { - fmt.Println(string(data)) - d.Time, err = time.Parse(`"`+ISO8601+`"`, string(data)) - return -} diff --git a/datetime.go b/datetime.go new file mode 100644 index 0000000..e28c69a --- /dev/null +++ b/datetime.go @@ -0,0 +1,83 @@ +package paypal + +import ( + "errors" + "time" +) + +const ( + ISO8601Date = "2006-01-02 MST" + ISO8601Datetime = "2006-01-02 15:04:05 MST" +) + +type Date struct { + time.Time +} + +type Datetime struct { + time.Time +} + +type DatetimeRFC3339 struct { + time.Time +} + +// String returns the formatted date +func (d Date) String() string { + return d.Time.Format(ISO8601Date) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Date) MarshalJSON() ([]byte, error) { + if y := d.Year(); y < 0 || y >= 10000 { + // ISO8601 is clear that years are 4 digits exactly. + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + return []byte(d.Format(`"` + ISO8601Date + `"`)), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Date) UnmarshalJSON(data []byte) (err error) { + d.Time, err = time.Parse(`"`+ISO8601Date+`"`, string(data)) + return +} + +// String returns the formatted date +func (d Datetime) String() string { + return d.Time.Format(ISO8601Datetime) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Datetime) MarshalJSON() ([]byte, error) { + if y := d.Year(); y < 0 || y >= 10000 { + // ISO8601 is clear that years are 4 digits exactly. + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + return []byte(d.Format(`"` + ISO8601Datetime + `"`)), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Datetime) UnmarshalJSON(data []byte) (err error) { + d.Time, err = time.Parse(`"`+ISO8601Datetime+`"`, string(data)) + return +} + +// String returns the formatted date +func (d DatetimeRFC3339) String() string { + return d.Time.Format(time.RFC3339) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d DatetimeRFC3339) MarshalJSON() ([]byte, error) { + if y := d.Year(); y < 0 || y >= 10000 { + // ISO8601 is clear that years are 4 digits exactly. + return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") + } + return []byte(d.Format(`"` + time.RFC3339 + `"`)), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *DatetimeRFC3339) UnmarshalJSON(data []byte) (err error) { + d.Time, err = time.Parse(`"`+time.RFC3339+`"`, string(data)) + return +} diff --git a/e2ebilling_test.go b/e2ebilling_test.go new file mode 100644 index 0000000..de06508 --- /dev/null +++ b/e2ebilling_test.go @@ -0,0 +1,136 @@ +package paypal + +import ( + "net/http" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestBilling(t *testing.T) { + withContext(func(client *Client) { + Convey("With the billing plan endpoint", t, func() { + Convey("Creating a billing plan with valid data should be successful", func() { + plan := &Plan{ + Name: "T-Shirt of the Month Club Plan", + Description: "Template creation.", + Type: PlanTypeFixed, + PaymentDefinitions: []PaymentDefinition{ + PaymentDefinition{ + Name: "Regular Payments", + Type: PaymentDefinitionTypeRegular, + Frequency: "MONTH", + FrequencyInterval: "2", + Amount: &Currency{ + Value: "100", + Currency: "USD", + }, + Cycles: "12", + ChargeModels: []ChargeModels{ + ChargeModels{ + Type: ChargeModelsTypeShipping, + Amount: &Currency{ + Value: "10", + Currency: "USD", + }, + }, + ChargeModels{ + Type: ChargeModelsTypeTax, + Amount: &Currency{ + Value: "12", + Currency: "USD", + }, + }, + }, + }, + }, + MerchantPreferences: &MerchantPreferences{ + SetupFee: &Currency{ + Value: "1", + Currency: "USD", + }, + ReturnURL: "http://www.return.com", + CancelURL: "http://www.cancel.com", + AutoBillAmount: "YES", + InitialFailAmountAction: "CONTINUE", + MaxFailAttempts: "0", + }, + } + + newPlan, err, resp := client.CreateBillingPlan(plan) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newPlan.ID, ShouldNotEqual, "") + So(newPlan.Name, ShouldEqual, plan.Name) + So(newPlan.Description, ShouldEqual, plan.Description) + + Convey("Updating the billing plan should be successful", func() { + err, resp := client.UpdateBillingPlan(newPlan.ID, &PatchPlan{ + State: PlanStateActive, + }) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + + Convey("Retrieving the updated plan should be successful", func() { + plan, err, resp := client.GetBillingPlan(newPlan.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(plan.State, ShouldEqual, PlanStateActive) + }) + + Convey("Creating a billing agreement with valid data should be successful", func() { + startDate := DatetimeRFC3339{time.Now().Add(24 * time.Hour)} + agreement := &Agreement{ + Name: "T-Shirt of the Month Club Agreement", + Description: "Agreement for T-Shirt of the Month Club Plan", + StartDate: &startDate, + Plan: &PatchPlan{ + ID: newPlan.ID, + }, + Payer: &AgreementPayer{ + PaymentMethod: PaymentMethodPaypal, + }, + ShippingAddress: &Address{ + Line1: "111 First Street", + City: "Saratoga", + State: "CA", + PostalCode: "95070", + CountryCode: "US", + }, + } + + newAgreement, err, resp := client.CreateAgreement(agreement) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newAgreement.Name, ShouldEqual, agreement.Name) + So(newAgreement.Description, ShouldEqual, agreement.Description) + + SkipConvey("Executing the new agreement should be successful", func() { + _, err, resp := client.ExecuteAgreement("123") + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + }) + }) + + }) + + Convey("Listing billing plans shold return valid data", func() { + plans, err, resp := client.ListBillingPlans(nil) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(len(plans), ShouldBeGreaterThan, 0) + }) + + }) + + }) + }) + +} diff --git a/e2einvoice_test.go b/e2einvoice_test.go new file mode 100644 index 0000000..378050f --- /dev/null +++ b/e2einvoice_test.go @@ -0,0 +1,191 @@ +package paypal + +import ( + "net/http" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestInvoice(t *testing.T) { + withContext(func(client *Client) { + Convey("With the invoice endpoint", t, func() { + Convey("Creating a invoice with valid data should be successful", func() { + invoice := &Invoice{ + + MerchantInfo: &MerchantInfo{ + Email: "lee-facilitator@fundary.com", + FirstName: "Dennis", + LastName: "Doctor", + BusinessName: "Medical Professionals, LLC", + Phone: &Phone{ + CountryCode: "001", + NationalNumber: "5032141716", + }, + Address: &Address{ + Line1: "1234 Main St.", + City: "Portland", + State: "OR", + PostalCode: "97217", + CountryCode: "US", + }, + }, + + BillingInfo: []BillingInfo{ + BillingInfo{ + Email: "example@example.com", + }, + }, + + Items: []InvoiceItem{ + InvoiceItem{ + Name: "Sutures", + Quantity: 100, + UnitPrice: &Currency{ + Currency: "USD", + Value: "5.00", + }, + }, + }, + + Note: "Medical Invoice 16 Jul, 2013 PST", + + PaymentTerm: &PaymentTerm{ + TermType: PaymentTermTypeNet45, + }, + + ShippingInfo: &ShippingInfo{ + FirstName: "Sally", + LastName: "Patient", + BusinessName: "Not applicable", + Address: &Address{ + Line1: "1234 Broad St.", + City: "Portland", + State: "OR", + PostalCode: "97216", + CountryCode: "US", + }, + }, + } + + newInvoice, err, resp := client.CreateInvoice(invoice) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newInvoice.ID, ShouldNotEqual, "") + So(newInvoice.Status, ShouldEqual, InvoiceStatusDraft) + So(newInvoice.Number, ShouldNotEqual, "") + So(newInvoice.Items[0].UnitPrice.Value, ShouldEqual, "5.00") + So(newInvoice.PaymentTerm.TermType, ShouldEqual, PaymentTermTypeNet45) + + Convey("Sending the new invoice to the payer should be successful", func() { + err, resp := client.SendInvoice(newInvoice.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusAccepted) + }) + + // Currently paypal returns 500 when updating invoice + SkipConvey("Updating the new invoice should be successful", func() { + newInvoice.Items[0].UnitPrice.Value = "250.00" + newInvoice.PaymentTerm.TermType = PaymentTermTypeNoDueDate + + updatedInvoice, err, resp := client.UpdateInvoice(newInvoice.ID, newInvoice) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(updatedInvoice.Items[0].UnitPrice.Value, ShouldEqual, "250.00") + So(updatedInvoice.PaymentTerm.TermType, ShouldEqual, PaymentTermTypeNoDueDate) + + Convey("Retrieving the updated invoice should be successful", func() { + newInvoice, err, resp := client.GetInvoice(newInvoice.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(newInvoice.Items[0].UnitPrice.Value, ShouldEqual, "250.00") + So(newInvoice.ID, ShouldEqual, updatedInvoice.ID) + }) + }) + + Convey("Listing invoices should be successful", func() { + invoices, err, resp := client.ListInvoices(nil) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(len(invoices), ShouldBeGreaterThan, 0) + }) + + Convey("Searching for invoices should be successful", func() { + invoices, err, resp := client.SearchInvoices(&InvoiceSearch{ + Email: "example@example.com", + }) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(len(invoices), ShouldBeGreaterThan, 0) + }) + + // Require invoices with status "SENT" + SkipConvey("Sending an invoice reminder should be successful", func() { + notification := &Notification{ + Subject: "Past due", + Note: "Please pay soon", + SendToMerchant: true, + } + + err, resp := client.SendInvoiceReminder(newInvoice.ID, notification) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusAccepted) + }) + + Convey("Retrieving QR code for the new invoice should be successful", func() { + img, err, resp := client.GetInvoiceQRCode(newInvoice.ID, 150, 150) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(img, ShouldNotEqual, "") + }) + + // Require status SENT + SkipConvey("Recording a payment for the new invoice should be successful", func() { + err, resp := client.RecordInvoicePayment(newInvoice.ID, PaymentDetailMethodCash, &Datetime{time.Now()}, "Cash received.") + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + }) + + // Require status PAID + SkipConvey("Recording a refund for the new invoice should be successful", func() { + err, resp := client.RecordInvoiceRefund(newInvoice.ID, &Datetime{time.Now()}, "Refund provided by cash.") + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + }) + + // Require status SENT + SkipConvey("Cancelling the new invoice should be successful", func() { + err, resp := client.CancelInvoice(newInvoice.ID, &Notification{ + Subject: "Past due", + Note: "Canceling invoice", + SendToMerchant: true, + SendToPayer: true, + }) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusAccepted) + }) + + Convey("Deleting the new invoice should be successful", func() { + err, resp := client.DeleteInvoice(newInvoice.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + }) + }) + + }) + }) + +} diff --git a/e2enotification_test.go b/e2enotification_test.go new file mode 100644 index 0000000..51ff1e2 --- /dev/null +++ b/e2enotification_test.go @@ -0,0 +1,60 @@ +package paypal + +import ( + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestNotifications(t *testing.T) { + withContext(func(client *Client) { + Convey("With the notifications endpoint", t, func() { + Convey("Listing event types should return valid data", func() { + eventTypes, err, resp := client.ListWebhookEventTypes() + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(len(eventTypes), ShouldEqual, 6) + So(eventTypes[0].Name, ShouldEqual, "PAYMENT.AUTHORIZATION.CREATED") + So(eventTypes[0].Description, ShouldEqual, "A payment authorization was created") + }) + + // Skipping as endpoint requires a valid webhook URL + SkipConvey("Creating a webhook with valid data should be successful", func() { + w := &Webhook{ + URL: "http://www.yeowza.com/paypal_webhook", + EventTypes: []EventType{ + EventType{Name: "PAYMENT.AUTHORIZATION.CREATED"}, + EventType{Name: "PAYMENT.AUTHORIZATION.VOIDED"}, + }, + } + + newWebhook, err, resp := client.CreateWebhook(w) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newWebhook.URL, ShouldEqual, w.URL) + So(len(newWebhook.EventTypes), ShouldEqual, len(w.EventTypes)) + So(newWebhook.EventTypes[0].Name, ShouldEqual, w.EventTypes[0].Name) + So(newWebhook.EventTypes[0].Description, ShouldEqual, "A payment authorization was created") + So(newWebhook.ID, ShouldNotBeNil) + + Convey("Retrieving the newly created webhook should be successful", func() { + w, err, resp := client.GetWebhook(newWebhook.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newWebhook.URL, ShouldEqual, w.URL) + }) + }) + + Convey("Listing all webhooks should return valid data", func() { + _, err, resp := client.ListWebhooks() + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + }) + }) +} diff --git a/e2evault_test.go b/e2evault_test.go index 5a02a3f..2ef388d 100644 --- a/e2evault_test.go +++ b/e2evault_test.go @@ -38,8 +38,10 @@ func TestVault(t *testing.T) { // Skip this test for now as it seems the endpoint does not behave as specified in documentation SkipConvey("Updating the stored credit card should updates the data", func() { - newCreditCard.FirstName = "Carol" - creditCard, err, resp := client.UpdateStoredCreditCard(newCreditCard.ID, newCreditCard) + patchCreditCard := &PatchCreditCard{ + FirstName: "Carol", + } + creditCard, err, resp := client.UpdateStoredCreditCard(newCreditCard.ID, patchCreditCard) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) diff --git a/invoicing.go b/invoicing.go index fb88400..2b61464 100644 --- a/invoicing.go +++ b/invoicing.go @@ -3,7 +3,6 @@ package paypal import ( "fmt" "net/http" - "time" ) // https://developer.paypal.com/webapps/developer/docs/api/#invoicing @@ -43,8 +42,9 @@ func (c *Client) SendInvoice(invoiceID string) (error, *http.Response) { } // UpdateInvoice updates an invoic -func (c *Client) UpdateInvoice(i *Invoice) (*Invoice, error, *http.Response) { - req, err := NewRequest("PUT", fmt.Sprintf("%s/invoicing/invoices/%s", c.APIBase, i.ID), i) +func (c *Client) UpdateInvoice(invoiceID string, i *Invoice) (*Invoice, error, *http.Response) { + i.ID = "" + req, err := NewRequest("PUT", fmt.Sprintf("%s/invoicing/invoices/%s", c.APIBase, invoiceID), i) if err != nil { return nil, err, nil } @@ -106,7 +106,7 @@ func (c *Client) ListInvoices(filter map[string]string) ([]Invoice, error, *http } // SearchInvoices returns invoices that match the specificed criteria -func (c *Client) SearchInvoices(s *Search) ([]Invoice, error, *http.Response) { +func (c *Client) SearchInvoices(s *InvoiceSearch) ([]Invoice, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/search", c.APIBase), s) if err != nil { return nil, err, nil @@ -182,7 +182,7 @@ func (c *Client) DeleteInvoice(invoiceID string) (error, *http.Response) { // the invoice from a draft state to a payable state. As stated above, if you specify // qrinvoice@paypal.com as the recipient email address, the invoice will not be emailed. func (c *Client) GetInvoiceQRCode(invoiceID string, width, height int) (string, error, *http.Response) { - req, err := NewRequest("GET", fmt.Sprintf("%s/invoicing/invoices/%s/qr-code?width=%s&height%s", c.APIBase, invoiceID, width, height), nil) + req, err := NewRequest("GET", fmt.Sprintf("%s/invoicing/invoices/%s/qr-code?width=%d&height=%d", c.APIBase, invoiceID, width, height), nil) if err != nil { return "", err, nil } @@ -200,10 +200,10 @@ func (c *Client) GetInvoiceQRCode(invoiceID string, width, height int) (string, } // RecordInvoicePayment marks an invoice as paid -func (c *Client) RecordInvoicePayment(invoiceID string, method PaymentDetailMethod, date *time.Time, note string) (error, *http.Response) { +func (c *Client) RecordInvoicePayment(invoiceID string, method PaymentDetailMethod, date *Datetime, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/record-payment", c.APIBase, invoiceID), &struct { Method PaymentDetailMethod `json:"method"` - Date *time.Time `json:"date"` + Date *Datetime `json:"date"` Note string `json:"note"` }{ Method: method, @@ -223,10 +223,10 @@ func (c *Client) RecordInvoicePayment(invoiceID string, method PaymentDetailMeth } // RecordInvoiceRefund marks an invoice as refunded -func (c *Client) RecordInvoiceRefund(invoiceID string, date *time.Time, note string) (error, *http.Response) { +func (c *Client) RecordInvoiceRefund(invoiceID string, date *Datetime, note string) (error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/invoicing/invoices/%s/record-refund", c.APIBase, invoiceID), &struct { - Date *time.Time `json:"date"` - Note string `json:"note"` + Date *Datetime `json:"date"` + Note string `json:"note"` }{ Date: date, Note: note, diff --git a/invoicingtype.go b/invoicingtype.go index d910fa3..1c992ad 100644 --- a/invoicingtype.go +++ b/invoicingtype.go @@ -41,6 +41,7 @@ var ( BillingInfoLanguageZHXC BillingInfoLanguage = "zh_XC" PaymentTermTypeDueOnReceipt PaymentTermType = "DUE_ON_RECEIPT" + PaymentTermTypeNoDueDate PaymentTermType = "NO_DUE_DATE" PaymentTermTypeNet10 PaymentTermType = "NET_10" PaymentTermTypeNet15 PaymentTermType = "NET_15" PaymentTermTypeNet30 PaymentTermType = "NET_30" @@ -77,15 +78,15 @@ type ( // Invoice maps to invoice object Invoice struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Number string `json:"number,omitempty"` - URI string `json:"uri"` - Status InvoiceStatus `json:"status"` + URI string `json:"uri,omitempty"` + Status InvoiceStatus `json:"status,omitempty"` MerchantInfo *MerchantInfo `json:"merchant_info"` BillingInfo []BillingInfo `json:"billing_info"` - ShippingInfo *ShippingInfo `json:"shipping_info"` + ShippingInfo *ShippingInfo `json:"shipping_info,omitempty"` Items []InvoiceItem `json:"items"` - InvoiceDate *Date `json:"invoice_date"` + InvoiceDate *Date `json:"invoice_date,omitempty"` PaymentTerm *PaymentTerm `json:"payment_term,omitempty"` Discount *Cost `json:"discount,omitempty"` ShippingCost *ShippingCost `json:"shipping_cost,omitempty"` @@ -96,10 +97,10 @@ type ( Note string `json:"note,omitempty"` MerchantMemo string `json:"merchant_memo,omitempty"` LogoURL string `json:"logo_url,omitempty"` - TotalAmount *Currency `json:"total_amount"` - PaymentDetails []PaymentDetail `json:"payment_details"` - RefundDetails []RefundDetail `json:"refund_details"` - Metadata *Metadata `json:"metadata"` + TotalAmount *Currency `json:"total_amount,omitempty"` + PaymentDetails []PaymentDetail `json:"payment_details,omitempty"` + RefundDetails []RefundDetail `json:"refund_details,omitempty"` + Metadata *Metadata `json:"metadata,omitempty"` } // InvoiceItem maps to invoice_item object @@ -149,41 +150,41 @@ type ( // PaymentTerm maps to payment_term object PaymentTerm struct { TermType PaymentTermType `json:"term_type"` - DueDate *Date `json:"due_date"` + DueDate *Date `json:"due_date,omitempty"` } // Cost maps to cost object Cost struct { - Percent int `json:"percent"` - Amount *Currency `json:"amount"` + Percent int `json:"percent,omitempty"` + Amount *Currency `json:"amount,omitempty"` } // ShippingCost maps to shipping_cost object ShippingCost struct { - Amount *Currency `json:"amount"` - Tax *Tax `json:"tax"` + Amount *Currency `json:"amount,omitempty"` + Tax *Tax `json:"tax,omitempty"` } // Tax maps to tax object Tax struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Name string `json:"name"` Percent int `json:"percent"` - Amount *Currency `json:"amount"` + Amount *Currency `json:"amount,omitempty"` } // CustomAmount maps to custom_amount object CustomAmount struct { - Label string `json:"label"` - Amount *Currency `json:"amount"` + Label string `json:"label,omitempty"` + Amount *Currency `json:"amount,omitempty"` } // PaymentDetail maps to payment_detail object PaymentDetail struct { - Type PaymentDetailType `json:"type"` - TransactionID string `json:"transaction_id"` - TransactionType PaymentDetailTransactionType `json:"transaction_type"` - Date *Date `json:"date"` + Type PaymentDetailType `json:"type,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + TransactionType PaymentDetailTransactionType `json:"transaction_type,omitempty"` + Date *Date `json:"date,omitempty"` Method PaymentDetailMethod `json:"method"` Note string `json:"note,omitempty"` } @@ -191,25 +192,25 @@ type ( // RefundDetail maps to refund_detail object RefundDetail struct { Type RefundDetailType `json:"type"` - Date *Date `json:"date"` + Date *Date `json:"date,omitempty"` Note string `json:"note,omitempty"` } // Metadata maps to metadata object Metadata struct { - CreatedDate *Date `json:"created_date"` - CreatedBy string `json:"created_by"` - CancelledDate *Date `json:"cancelled_date"` - CancelledBy string `json:"cancelled_by"` - LastUpdatedDate *Date `json:"last_updated_date"` - LastUpdatedBy string `json:"last_updated_by"` - FirstSentDate *Date `json:"first_sent_date"` - LastSentDate *Date `json:"last_sent_date"` - LastSentBy *Date `json:"last_sent_by"` + CreatedDate *Datetime `json:"created_date,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + CancelledDate *Datetime `json:"cancelled_date,omitempty"` + CancelledBy string `json:"cancelled_by,omitempty"` + LastUpdatedDate *Datetime `json:"last_updated_date,omitempty"` + LastUpdatedBy string `json:"last_updated_by,omitempty"` + FirstSentDate *Datetime `json:"first_sent_date,omitempty"` + LastSentDate *Datetime `json:"last_sent_date,omitempty"` + LastSentBy *Datetime `json:"last_sent_by,omitempty"` } // Search maps to search object. Invoice search parameters - Search struct { + InvoiceSearch struct { Email string `json:"email,omitempty"` RecipientFirstName string `json:"recipient_first_name,omitempty"` RecipientLastName string `json:"recipient_last_name,omitempty"` @@ -218,14 +219,14 @@ type ( Status InvoiceStatus `json:"status,omitempty"` LowerTotalAmount *Currency `json:"lower_total_amount,omitempty"` UpperTotalAmount *Currency `json:"upper_total_amount,omitempty"` - StartInvoiceDate *Date `json:"start_invoice_date,omitempty"` - EndInvoiceDate *Date `json:"end_invoice_date,omitempty"` - StartDueDate *Date `json:"start_due_date,omitempty"` - EndDueDate *Date `json:"end_due_date,omitempty"` - StartPaymentDate *Date `json:"start_payment_date,omitempty"` - EndPaymentDate *Date `json:"end_payment_date,omitempty"` - StartCreationDate *Date `json:"start_creation_date,omitempty"` - EndCreationDate *Date `json:"end_creation_date,omitempty"` + StartInvoiceDate *Datetime `json:"start_invoice_date,omitempty"` + EndInvoiceDate *Datetime `json:"end_invoice_date,omitempty"` + StartDueDate *Datetime `json:"start_due_date,omitempty"` + EndDueDate *Datetime `json:"end_due_date,omitempty"` + StartPaymentDate *Datetime `json:"start_payment_date,omitempty"` + EndPaymentDate *Datetime `json:"end_payment_date,omitempty"` + StartCreationDate *Datetime `json:"start_creation_date,omitempty"` + EndCreationDate *Datetime `json:"end_creation_date,omitempty"` Page int `json:"page,omitempty"` PageSize int `json:"page_size,omitempty"` TotalCountRequired bool `json:"total_count_required,omitempty"` diff --git a/notification.go b/notification.go index 1c5f267..237cc9c 100644 --- a/notification.go +++ b/notification.go @@ -92,7 +92,7 @@ func (c *Client) ListWebhooks() ([]Webhook, error, *http.Response) { Webhooks []Webhook `json:"webhooks"` } - resp, err := c.SendWithAuth(req, v) + resp, err := c.SendWithAuth(req, &v) if err != nil { return nil, err, resp } diff --git a/paypal.go b/paypal.go index 03bfadd..9d830bf 100644 --- a/paypal.go +++ b/paypal.go @@ -124,6 +124,7 @@ func (c *Client) Send(req *http.Request, v interface{}) (*http.Response, error) } log.Println(req.Method, ": ", req.URL) + log.Println("body:", req.Body) resp, err := c.client.Do(req) if err != nil { diff --git a/vault.go b/vault.go index 300954e..f4c5a21 100644 --- a/vault.go +++ b/vault.go @@ -58,11 +58,11 @@ func (c *Client) GetStoredCreditCard(creditCardID string) (*CreditCard, error, * } // UpdateStoredCreditCard modifies a stored credit card -func (c *Client) UpdateStoredCreditCard(creditCardID string, creditCard *CreditCard) (*CreditCard, error, *http.Response) { +func (c *Client) UpdateStoredCreditCard(creditCardID string, creditCard *PatchCreditCard) (*CreditCard, error, *http.Response) { req, err := NewRequest("PATCH", fmt.Sprintf("%s/vault/credit-card/%s", c.APIBase, creditCardID), struct { - Path string `json:"path"` - Value *CreditCard `json:"value"` - OP PatchOperation `json:"op"` + Path string `json:"path"` + Value *PatchCreditCard `json:"value"` + OP PatchOperation `json:"op"` }{ Path: "/", Value: creditCard, From d1607e9114e4d7fdd842e3246b5c2a532e250dbf Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 00:34:08 +0200 Subject: [PATCH 15/20] Added tests for sales, captures, refunds and authorizations --- e2epayment_test.go | 150 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 37 deletions(-) diff --git a/e2epayment_test.go b/e2epayment_test.go index 9b782e0..73fcf2d 100644 --- a/e2epayment_test.go +++ b/e2epayment_test.go @@ -1,6 +1,7 @@ package paypal import ( + "net/http" "testing" . "github.com/smartystreets/goconvey/convey" @@ -54,25 +55,26 @@ func TestPayment(t *testing.T) { Payer: &payer, Transactions: []Transaction{transaction}, } - newPaymentResp, err, _ := client.CreatePayment(payment) + newPaymentResp, err, resp := client.CreatePayment(payment) So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) So(newPaymentResp.Intent, ShouldEqual, PaymentIntentSale) So(newPaymentResp.ID, ShouldNotBeNil) // This requires manual test as the payer needs to approve inside Paypal - // Convey("Execute the newly created payment should be successful", func() { - // resp, err := client.ExecutePayment(newPaymentResp.ID, payer.ID, nil) - // - // So(err, ShouldBeNil) - // So(resp.ID, ShouldEqual, newPaymentResp.ID) - // So(resp.State, ShouldEqual, PaymentStateApproved) - // }) + SkipConvey("Execute the newly created payment should be successful", func() { + _, err, resp := client.ExecutePayment(newPaymentResp.ID, "123", nil) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) Convey("Fetching the newly created payment should return valid results", func() { - payment, err, _ := client.GetPayment(newPaymentResp.ID) + payment, err, resp := client.GetPayment(newPaymentResp.ID) So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) So(payment.ID, ShouldEqual, newPaymentResp.ID) So(payment.Intent, ShouldEqual, PaymentIntentSale) So(payment.Payer.PaymentMethod, ShouldEqual, PaymentMethodCreditCard) @@ -80,53 +82,127 @@ func TestPayment(t *testing.T) { Convey("With the sale endpoints", func() { Convey("Fetching an existing sale should return valid data", func() { - sale, err, _ := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) + sale, err, resp := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) So(sale.ID, ShouldEqual, newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) // Cannot test refund as it require that a payment is approved and completed - // Convey("A partial refund for an existing sale should be successful", func() { - // amount := Amount{ - // Total: "2.34", - // Currency: "USD", - // } - // - // refund, err := client.RefundSale(sale.ID, &amount) - // So(err, ShouldBeNil) - // - // refundedSale, err := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) - // - // So(err, ShouldBeNil) - // So(refund.Amount.Total, ShouldEqual, amount.Total) - // So(refund.ParentPayment, ShouldEqual, payment.ID) - // So(refundedSale.State, ShouldEqual, SaleStatePartiallyRefunded) - // So(refundedSale.Amount.Total, ShouldEqual, "5.13") - // - // Convey("Retrieving the new refund should return valid results", func() { - // newRefund, err := client.GetRefund(refund.ID) - // - // So(err, ShouldBeNil) - // So(newRefund.ID, ShouldEqual, refund.ID) - // So(newRefund.Amount, ShouldResemble, refund.Amount) - // - // }) - // }) + SkipConvey("A partial refund for an existing sale should be successful", func() { + amount := Amount{ + Total: "2.34", + Currency: "USD", + } + + refund, err, resp := client.RefundSale(sale.ID, &amount) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + + refundedSale, err, resp := client.GetSale(newPaymentResp.Transactions[0].RelatedResources[0].Sale.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(refund.Amount.Total, ShouldEqual, amount.Total) + So(refund.ParentPayment, ShouldEqual, payment.ID) + So(refundedSale.State, ShouldEqual, SaleStatePartiallyRefunded) + So(refundedSale.Amount.Total, ShouldEqual, "5.13") + + Convey("Retrieving the new refund should return valid results", func() { + newRefund, err, resp := client.GetRefund(refund.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(newRefund.ID, ShouldEqual, refund.ID) + So(newRefund.Amount, ShouldResemble, refund.Amount) + + }) + }) }) }) }) Convey("List payments should include the newly created payment", func() { - payments, err, _ := client.ListPayments(map[string]string{ + payments, err, resp := client.ListPayments(map[string]string{ "count": "10", "sort_by": "create_time", }) So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) So(len(payments), ShouldBeGreaterThan, 0) So(payments[0].ID, ShouldEqual, newPaymentResp.ID) }) + + Convey("Authorize a new payment should be successful", func() { + payment.Intent = PaymentIntentAuthorize + + authorizedPayment, err, resp := client.CreatePayment(payment) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(authorizedPayment.Intent, ShouldEqual, PaymentIntentAuthorize) + So(authorizedPayment.Transactions[0].RelatedResources[0].Authorization.ID, ShouldNotEqual, "") + + authID := authorizedPayment.Transactions[0].RelatedResources[0].Authorization.ID + + Convey("Looking up the payment's authorization should return valid data", func() { + authorization, err, resp := client.GetAuthorization(authID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(authorization.Amount.Total, ShouldEqual, "7.47") + }) + + Convey("Capturing the authorization should be successful", func() { + capture, err, resp := client.CaptureAuthorization(authID, &Amount{ + Currency: "USD", + Total: "4.54", + }, true) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(capture.Amount.Total, ShouldEqual, "4.54") + So(capture.IsFinalCapture, ShouldEqual, true) + So(capture.State, ShouldEqual, CaptureStatePending) + + Convey("Retrieving the new capture should returns valid data", func() { + newCapture, err, resp := client.GetCapture(capture.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(newCapture.Amount.Total, ShouldEqual, capture.Amount.Total) + So(newCapture.IsFinalCapture, ShouldEqual, capture.IsFinalCapture) + So(newCapture.State, ShouldEqual, capture.State) + }) + + Convey("Refunding the new capture should returns a valid refund object", func() { + refund, err, resp := client.RefundCapture(capture.ID, &Amount{ + Currency: "USD", + Total: "4.54", + }) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(refund.ID, ShouldNotEqual, "") + So(refund.State, ShouldEqual, RefundStatePending) + So(refund.ParentPayment, ShouldEqual, authorizedPayment.ID) + }) + }) + + Convey("Voiding an authorization should be successful", func() { + voidedAuthorization, err, resp := client.VoidAuthorization(authID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(voidedAuthorization.ID, ShouldNotEqual, authID) + So(voidedAuthorization.State, ShouldEqual, AuthorizationStateVoided) + }) + + // TODO: Add test for reauthorize payments + }) }) }) From 95721645a794c3346613362fbe76e41f6bc5483e Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 01:27:05 +0200 Subject: [PATCH 16/20] Fixed invoice tests --- e2einvoice_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/e2einvoice_test.go b/e2einvoice_test.go index 378050f..0d359a3 100644 --- a/e2einvoice_test.go +++ b/e2einvoice_test.go @@ -2,6 +2,7 @@ package paypal import ( "net/http" + "os" "testing" "time" @@ -9,13 +10,18 @@ import ( ) func TestInvoice(t *testing.T) { + accountEmail := os.Getenv("PAYPAL_TEST_ACCOUNT_EMAIL") + if accountEmail == "" { + panic("Test Paypal account email is missing") + } + withContext(func(client *Client) { Convey("With the invoice endpoint", t, func() { Convey("Creating a invoice with valid data should be successful", func() { invoice := &Invoice{ MerchantInfo: &MerchantInfo{ - Email: "lee-facilitator@fundary.com", + Email: accountEmail, FirstName: "Dennis", LastName: "Doctor", BusinessName: "Medical Professionals, LLC", From a446ce93dcf5d3caf0c17230e39c3284ec502e11 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 01:27:41 +0200 Subject: [PATCH 17/20] Added debug mode and support for Paypal-Request-Id header using UUIDs --- paypal.go | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/paypal.go b/paypal.go index 9d830bf..3fb117a 100644 --- a/paypal.go +++ b/paypal.go @@ -8,7 +8,11 @@ import ( "io/ioutil" "log" "net/http" + "os" + "strconv" "time" + + "code.google.com/p/go-uuid/uuid" ) const ( @@ -60,6 +64,21 @@ type ( } ) +var ( + Debug bool +) + +func init() { + var err error + debug := os.Getenv("PAYPAL_DEBUG") + if debug != "" { + Debug, err = strconv.ParseBool(debug) + if err != nil { + panic("Invalid value for PAYPAL_DEBUG") + } + } +} + func (r *ErrorResponse) Error() string { return fmt.Sprintf("%v %v: %d %v\nDetails: %v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message, r.Details) @@ -123,8 +142,14 @@ func (c *Client) Send(req *http.Request, v interface{}) (*http.Response, error) req.Header.Set("Content-type", "application/json") } - log.Println(req.Method, ": ", req.URL) - log.Println("body:", req.Body) + // Safe concurrent requests + req.Header.Set("Paypal-Request-Id", uuid.New()) + + if Debug { + log.Println(req.Method, ": ", req.URL) + log.Println(req.Header) + log.Println("body:", req.Body) + } resp, err := c.client.Do(req) if err != nil { @@ -135,7 +160,9 @@ func (c *Client) Send(req *http.Request, v interface{}) (*http.Response, error) if c := resp.StatusCode; c < 200 || c > 299 { errResp := &ErrorResponse{Response: resp} data, err := ioutil.ReadAll(resp.Body) - log.Println(string(data)) + if Debug { + log.Println(string(data)) + } if err == nil && len(data) > 0 { json.Unmarshal(data, errResp) } From 4d6a562a16a819b5aa5d722e4f6d807aceebfe5e Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 01:27:58 +0200 Subject: [PATCH 18/20] Updated README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d47f160..611f482 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ A Go client for the Paypal REST API ([https://developer.paypal.com/webapps/devel ## Goals - [x] Automated tests that don't require manual approval in Paypal account +- [x] Concurrency safety by utilizing `PayPal-Request-Id` - [ ] Automated tests that require manual approval in a Paypal account (with a different build tag, eg. `PAYPAL_APPROVED_PAYMENT_ID` -- [ ] Concurrency safety by utilizing `PayPal-Request-Id` ## Usage @@ -59,13 +59,13 @@ func main() { This library use [Goconvey](http://goconvey.co/) for tests, so to run them, start Goconvey: ``` -PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] goconvey +PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] PAYPAL_TEST_ACCOUNT_EMAIL=[Paypal test account email] goconvey ``` Or you can just use `go test` ``` -PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] go test +PAYPAL_TEST_CLIENTID=[Paypal Client ID] PAYPAL_TEST_SECRET=[Paypal Secret] PAYPAL_TEST_ACCOUNT_EMAIL=[Paypal test account email] go test ``` ## Roadmap From ced02fe81b05b7c595c7f97a6c7e0f72e3b84bbf Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 02:10:20 +0200 Subject: [PATCH 19/20] Added tests for order endpoint --- e2eorder_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ e2epayment_test.go | 8 ++-- order.go | 4 +- paymenttype.go | 19 +++++----- 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 e2eorder_test.go diff --git a/e2eorder_test.go b/e2eorder_test.go new file mode 100644 index 0000000..e9fed10 --- /dev/null +++ b/e2eorder_test.go @@ -0,0 +1,94 @@ +package paypal + +import ( + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestOrder(t *testing.T) { + withContext(func(client *Client) { + Convey("With the orders endpoint", t, func() { + Convey("Creating a order with valid data should be successful", func() { + payer := Payer{ + PaymentMethod: PaymentMethodPaypal, + } + amountDetail := Details{ + Subtotal: "7.41", + Tax: "0.03", + Shipping: "0.03", + } + amount := Amount{ + Total: "7.47", + Currency: "USD", + Details: &amountDetail, + } + transaction := Transaction{ + Amount: &amount, + Description: "This is the payment transaction description.", + } + order := Payment{ + Intent: PaymentIntentOrder, + Payer: &payer, + Transactions: []Transaction{transaction}, + RedirectURLs: RedirectURLs{ + ReturnURL: "http://www.return.com", + CancelURL: "http://www.cancel.com", + }, + } + newOrder, err, resp := client.CreatePayment(order) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusCreated) + So(newOrder.Intent, ShouldEqual, PaymentIntentOrder) + So(newOrder.ID, ShouldNotBeNil) + + Convey("Retrieving an order should returns valid data", func() { + order, err, resp := client.GetOrder(newOrder.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(order.ID, ShouldNotEqual, "") + So(order.State, ShouldEqual, OrderStatePending) + So(order.PendingReason, ShouldEqual, PendingReasonOrder) + + Convey("Authorizing the order should return a valid authorization object", func() { + authorization, err, resp := client.AuthorizeOrder(newOrder.ID, &Amount{ + Currency: "USD", + Total: "4.54", + }) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(authorization.ID, ShouldNotEqual, "") + So(authorization.State, ShouldEqual, AuthorizationStateAuthorized) + }) + + Convey("Capturing the order should return a valid capture object", func() { + capture, err, resp := client.CaptureOrder(newOrder.ID, &Amount{ + Currency: "USD", + Total: "4.54", + }, true) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(capture.Amount.Total, ShouldEqual, "4.54") + So(capture.IsFinalCapture, ShouldEqual, true) + So(capture.State, ShouldEqual, CaptureStatePending) + }) + + Convey("Voiding the order should be successful", func() { + voidedOrder, err, resp := client.VoidOrder(newOrder.ID) + + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + So(voidedOrder.State, ShouldEqual, "voided") + }) + }) + }) + + }) + }) + +} diff --git a/e2epayment_test.go b/e2epayment_test.go index 73fcf2d..f0dc8c7 100644 --- a/e2epayment_test.go +++ b/e2epayment_test.go @@ -174,11 +174,11 @@ func TestPayment(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) So(newCapture.Amount.Total, ShouldEqual, capture.Amount.Total) - So(newCapture.IsFinalCapture, ShouldEqual, capture.IsFinalCapture) - So(newCapture.State, ShouldEqual, capture.State) + So(newCapture.State, ShouldEqual, CaptureStateAuthorized) }) - Convey("Refunding the new capture should returns a valid refund object", func() { + // Cannot refund + SkipConvey("Refunding the new capture should returns a valid refund object", func() { refund, err, resp := client.RefundCapture(capture.ID, &Amount{ Currency: "USD", Total: "4.54", @@ -197,7 +197,7 @@ func TestPayment(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) - So(voidedAuthorization.ID, ShouldNotEqual, authID) + So(voidedAuthorization.ID, ShouldEqual, authID) So(voidedAuthorization.State, ShouldEqual, AuthorizationStateVoided) }) diff --git a/order.go b/order.go index b978fb7..f2e1572 100644 --- a/order.go +++ b/order.go @@ -25,7 +25,7 @@ func (c *Client) GetOrder(orderID string) (*Order, error, *http.Response) { } // AuthorizeOrder authorizes an order -func (c *Client) AuthorizeOrder(orderID, string, amount *Amount) (*Authorization, error, *http.Response) { +func (c *Client) AuthorizeOrder(orderID string, amount *Amount) (*Authorization, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/authorize", c.APIBase, orderID), struct { Amount *Amount `json:"amount"` }{ @@ -47,7 +47,7 @@ func (c *Client) AuthorizeOrder(orderID, string, amount *Amount) (*Authorization // CaptureOrder captures a payment on an order. To use this call, an original payment // must specify an "intent" of "order" -func (c *Client) CaptureOrder(orderID, string, amount *Amount, isFinal bool) (*Capture, error, *http.Response) { +func (c *Client) CaptureOrder(orderID string, amount *Amount, isFinal bool) (*Capture, error, *http.Response) { req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/capture", c.APIBase, orderID), struct { Amount *Amount `json:"amount"` IsFinalCapture bool `json:"is_final_capture"` diff --git a/paymenttype.go b/paymenttype.go index 5c2bd5a..0c57aa9 100644 --- a/paymenttype.go +++ b/paymenttype.go @@ -18,6 +18,7 @@ var ( CaptureStateCompleted CaptureState = "completed" CaptureStateRefunded CaptureState = "refunded" CaptureStatePartiallyRefunded CaptureState = "partially_refunded" + CaptureStateAuthorized CaptureState = "authorized" OrderStatePending OrderState = "PENDING" OrderStateCompleted OrderState = "COMPLETED" @@ -232,15 +233,15 @@ type ( // Payment maps to payment object Payment struct { - Intent PaymentIntent `json:"intent"` - Payer *Payer `json:"payer"` - Transactions []Transaction `json:"transactions"` - RedirectURLs []RedirectURLs `json:"redirect_urls,omitempty"` - ID string `json:"id,omitempty"` - CreateTime *time.Time `json:"create_time,omitempty"` - State PaymentState `json:"state,omitempty"` - UpdateTime *time.Time `json:"update_time,omitempty"` - ExperienceProfileID string `json:"experience_profile_id,omitempty"` + Intent PaymentIntent `json:"intent"` + Payer *Payer `json:"payer"` + Transactions []Transaction `json:"transactions"` + RedirectURLs RedirectURLs `json:"redirect_urls,omitempty"` + ID string `json:"id,omitempty"` + CreateTime *time.Time `json:"create_time,omitempty"` + State PaymentState `json:"state,omitempty"` + UpdateTime *time.Time `json:"update_time,omitempty"` + ExperienceProfileID string `json:"experience_profile_id,omitempty"` } // PaymentExecution maps to payment_execution object From 3d9f4479e0ab427613096c26af3a3b7365843d50 Mon Sep 17 00:00:00 2001 From: Peter Nguyen Date: Mon, 13 Oct 2014 02:25:12 +0200 Subject: [PATCH 20/20] Skipping tests for order which requires user's approval --- e2eorder_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2eorder_test.go b/e2eorder_test.go index e9fed10..2c04686 100644 --- a/e2eorder_test.go +++ b/e2eorder_test.go @@ -44,7 +44,8 @@ func TestOrder(t *testing.T) { So(newOrder.Intent, ShouldEqual, PaymentIntentOrder) So(newOrder.ID, ShouldNotBeNil) - Convey("Retrieving an order should returns valid data", func() { + // Require user's approval + SkipConvey("Retrieving an order should returns valid data", func() { order, err, resp := client.GetOrder(newOrder.ID) So(err, ShouldBeNil)