diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..120cae5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - 1.1 + - 1.2 + - 1.3 + - release + - tip + +before_install: + - go get github.com/smartystreets/goconvey + +script: + - go test -v diff --git a/README.md b/README.md index 4e67c69..611f482 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # 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) +[![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) -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 - [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 @@ -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", }) @@ -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 @@ -75,9 +75,10 @@ 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) -- [ ] [Payments - Order](https://developer.paypal.com/webapps/developer/docs/api/#orders) -- [ ] [Vault](https://developer.paypal.com/webapps/developer/docs/api/#vault) +- [x] [Payments - Billing Plans and Agreements](https://developer.paypal.com/webapps/developer/docs/api/#billing-plans-and-agreements) +- [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) 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 new file mode 100644 index 0000000..83339a8 --- /dev/null +++ b/billing.go @@ -0,0 +1,290 @@ +package paypal + +import ( + "fmt" + "net/http" +) + +// 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, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-plans", c.APIBase), p) + if err != nil { + return nil, err, nil + } + + v := &Plan{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + 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(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 *PatchPlan `json:"value"` + OP PatchOperation `json:"op"` + }{ + struct { + Path string `json:"path"` + Value *PatchPlan `json:"value"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: p, + OP: PatchOperationReplace, + }, + }) + if err != nil { + return err, nil + } + + resp, err := c.SendWithAuth(req, nil) + if err != nil { + return err, resp + } + + return nil, resp +} + +// GetBillingPlan returns details about a specific billing plan +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, nil + } + + v := &Plan{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + 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, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/billing-plans", 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 ListBillingPlansResp + + resp, err := c.SendWithAuth(req, &v) + if err != nil { + return nil, err, resp + } + + 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, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/billing-agreements", c.APIBase), a) + if err != nil { + return nil, err, nil + } + + v := &Agreement{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// ExecuteAgreement executes an agreement after the buyer approves it. +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, nil + } + + v := &Agreement{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// UpdateAgreement updates an agreement +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"` + OP PatchOperation `json:"op"` + }{ + Path: "/", + Value: a, + OP: PatchOperationReplace, + }) + if err != nil { + return err, nil + } + + resp, err := c.SendWithAuth(req, nil) + if err != nil { + return err, resp + } + + return nil, resp +} + +// GetAgreement returns an agreement +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, nil + } + + v := &Agreement{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// SuspendAgreement suspends an agreement +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, nil + } + + resp, err := c.SendWithAuth(req, nil) + + return err, resp +} + +// ReactivateAgreement reactivate an agreement +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, nil + } + + resp, err := c.SendWithAuth(req, nil) + + return err, resp +} + +// SearchAgreementTransactions searches for transactions within a billing agreement +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, nil + } + + if filter != nil { + q := req.URL.Query() + + for k, v := range filter { + q.Set(k, v) + } + + req.URL.RawQuery = q.Encode() + } + + v := &AgreementTransactions{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// CancelAgreement cancels an agreement +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, nil + } + + resp, err := c.SendWithAuth(req, nil) + + return err, resp +} + +// SetAgreementBalance sets the outstanding amount of an agreement +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, nil + } + + resp, err := c.SendWithAuth(req, nil) + + return err, resp +} + +// BillAgreementBalance bills the outstanding amount of an agreement +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"` + }{ + Note: note, + Amount: currency, + }) + if err != nil { + return err, nil + } + + resp, err := c.SendWithAuth(req, nil) + + return err, resp +} diff --git a/billingagreementtype.go b/billingagreementtype.go new file mode 100644 index 0000000..9658c46 --- /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,omitempty"` + Name string `json:"name"` + 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 *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,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,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 + 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,omitempty"` + 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,omitempty"` + Type PaymentCardType `json:"type"` + ExpireMonth string `json:"expire_month,omitempty"` + ExpireYear string `json:"expire_year,omitempty"` + } + + // 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,omitempty"` + Type CreditType `json:"type"` + Terms string `json:"terms,omitempty"` + } + + // 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,omitempty"` + RecipientName string `json:"recipient_name"` + DefaultAddress bool `json:"default_address,omitempty"` + 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,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,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 new file mode 100644 index 0000000..44fd7c1 --- /dev/null +++ b/billingplantype.go @@ -0,0 +1,128 @@ +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,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Type PlanType `json:"type"` + 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"` + } + + // 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 + 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 + // See commontype.go + + // PaymentDefinition maps to payment_definition object + PaymentDefinition struct { + ID string `json:"id,omitempty"` + 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"` + } + + // ChargeModels maps to charge_models object + ChargeModels struct { + ID string `json:"id,omitempty"` + Type ChargeModelsType `json:"type"` + Amount *Currency `json:"amount"` + } + + // Terms maps to terms object + Terms struct { + ID string `json:"id,omitempty"` + 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,omitempty"` + SetupFee *Currency `json:"setup_fee,omitempty"` + CancelURL string `json:"cancel_url"` + ReturnURL string `json:"return_url"` + 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,omitempty"` + CharSet string `json:"char_set,omitempty"` + } + + // PlanList maps to plan_list object + PlanList struct { + Plans []Plan `json:"plans"` + Links []Links `json:"links"` + } +) 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/commontype.go b/commontype.go index a937a70..4490309 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 { @@ -12,4 +29,69 @@ 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"` + } + + // 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"` + } + + // 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"` + PayerID string `json:"payer_id,omitempty"` + Last4 string `json:"last4,omitempty"` + 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/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/e2e_test.go b/e2e_test.go deleted file mode 100644 index 5bda15f..0000000 --- a/e2e_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package paypal - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestPayment(t *testing.T) { - withContext(func(client *Client) { - Convey("With the payments endpoint", t, func() { - Convey("Creating a payment with valid data should be successful", func() { - billingAddress := Address{ - Line1: "111 First Street", - City: "Saratoga", - State: "CA", - PostalCode: "95070", - CountryCode: "US", - } - creditCard := CreditCard{ - Number: "4417119669820331", - Type: "visa", - ExpireMonth: "11", - ExpireYear: "2018", - CVV2: "874", - FirstName: "Betsy", - LastName: "Buyer", - BillingAddress: &billingAddress, - } - payer := Payer{ - PaymentMethod: PaymentMethodCreditCard, - FundingInstruments: []FundingInstrument{ - FundingInstrument{ - CreditCard: &creditCard, - }, - }, - } - 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.", - } - payment := Payment{ - Intent: PaymentIntentSale, - Payer: &payer, - Transactions: []Transaction{transaction}, - } - newPaymentResp, err := client.CreatePayment(payment) - - So(err, ShouldBeNil) - 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) - // }) - - Convey("Fetching the newly created payment should return valid results", func() { - payment, err := client.GetPayment(newPaymentResp.ID) - - So(err, ShouldBeNil) - So(payment.ID, ShouldEqual, newPaymentResp.ID) - So(payment.Intent, ShouldEqual, PaymentIntentSale) - So(payment.Payer.PaymentMethod, ShouldEqual, PaymentMethodCreditCard) - So(payment.Transactions[0].RelatedResources[0], ShouldNotBeNil) - - 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) - - So(err, ShouldBeNil) - 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) - // - // }) - // }) - }) - - }) - }) - - Convey("List payments should include the newly created payment", func() { - payments, err := client.ListPayments(map[string]string{ - "count": "10", - "sort_by": "create_time", - }) - - So(err, ShouldBeNil) - So(len(payments), ShouldBeGreaterThan, 0) - So(payments[0].ID, ShouldEqual, newPaymentResp.ID) - }) - }) - - }) - }) - -} 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..0d359a3 --- /dev/null +++ b/e2einvoice_test.go @@ -0,0 +1,197 @@ +package paypal + +import ( + "net/http" + "os" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +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: accountEmail, + 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/e2eorder_test.go b/e2eorder_test.go new file mode 100644 index 0000000..2c04686 --- /dev/null +++ b/e2eorder_test.go @@ -0,0 +1,95 @@ +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) + + // Require user's approval + SkipConvey("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 new file mode 100644 index 0000000..f0dc8c7 --- /dev/null +++ b/e2epayment_test.go @@ -0,0 +1,211 @@ +package paypal + +import ( + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestPayment(t *testing.T) { + withContext(func(client *Client) { + Convey("With the payments endpoint", t, func() { + Convey("Creating a payment with valid data should be successful", func() { + billingAddress := Address{ + Line1: "111 First Street", + City: "Saratoga", + State: "CA", + PostalCode: "95070", + CountryCode: "US", + } + creditCard := CreditCard{ + Number: "4417119669820331", + Type: "visa", + ExpireMonth: "11", + ExpireYear: "2018", + CVV2: "874", + FirstName: "Betsy", + LastName: "Buyer", + BillingAddress: &billingAddress, + } + payer := Payer{ + PaymentMethod: PaymentMethodCreditCard, + FundingInstruments: []FundingInstrument{ + FundingInstrument{ + CreditCard: &creditCard, + }, + }, + } + 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.", + } + payment := Payment{ + Intent: PaymentIntentSale, + Payer: &payer, + Transactions: []Transaction{transaction}, + } + 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 + 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, 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) + So(payment.Transactions[0].RelatedResources[0], ShouldNotBeNil) + + Convey("With the sale endpoints", func() { + Convey("Fetching an existing sale should return valid data", func() { + 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 + 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, 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.State, ShouldEqual, CaptureStateAuthorized) + }) + + // 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", + }) + + 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, ShouldEqual, authID) + So(voidedAuthorization.State, ShouldEqual, AuthorizationStateVoided) + }) + + // TODO: Add test for reauthorize payments + }) + }) + + }) + }) + +} diff --git a/e2evault_test.go b/e2evault_test.go new file mode 100644 index 0000000..2ef388d --- /dev/null +++ b/e2evault_test.go @@ -0,0 +1,70 @@ +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() { + patchCreditCard := &PatchCreditCard{ + FirstName: "Carol", + } + creditCard, err, resp := client.UpdateStoredCreditCard(newCreditCard.ID, patchCreditCard) + + 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 new file mode 100644 index 0000000..2b61464 --- /dev/null +++ b/invoicing.go @@ -0,0 +1,244 @@ +package paypal + +import ( + "fmt" + "net/http" +) + +// 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 + } + + resp, err := c.SendWithAuth(req, nil) + if err != nil { + return err, resp + } + + return nil, resp +} + +// UpdateInvoice updates an invoic +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 + } + + 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 *InvoiceSearch) ([]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 + } + + resp, err := c.SendWithAuth(req, nil) + 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 + } + + resp, err := c.SendWithAuth(req, nil) + 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 + } + + resp, err := c.SendWithAuth(req, nil) + 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=%d&height=%d", 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 *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 *Datetime `json:"date"` + Note string `json:"note"` + }{ + Method: method, + Date: date, + Note: note, + }) + if err != nil { + return err, nil + } + + resp, err := c.SendWithAuth(req, nil) + if err != nil { + return err, resp + } + + return nil, resp +} + +// RecordInvoiceRefund marks an invoice as refunded +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 *Datetime `json:"date"` + Note string `json:"note"` + }{ + Date: date, + Note: note, + }) + if err != nil { + return err, nil + } + + resp, err := c.SendWithAuth(req, nil) + if err != nil { + return err, resp + } + + return nil, resp +} diff --git a/invoicingtype.go b/invoicingtype.go new file mode 100644 index 0000000..1c992ad --- /dev/null +++ b/invoicingtype.go @@ -0,0 +1,242 @@ +package paypal + +// 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" + PaymentTermTypeNoDueDate PaymentTermType = "NO_DUE_DATE" + 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,omitempty"` + Number string `json:"number,omitempty"` + 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,omitempty"` + Items []InvoiceItem `json:"items"` + InvoiceDate *Date `json:"invoice_date,omitempty"` + 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,omitempty"` + PaymentDetails []PaymentDetail `json:"payment_details,omitempty"` + RefundDetails []RefundDetail `json:"refund_details,omitempty"` + Metadata *Metadata `json:"metadata,omitempty"` + } + + // 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 *Date `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 *Date `json:"due_date,omitempty"` + } + + // Cost maps to cost object + Cost struct { + Percent int `json:"percent,omitempty"` + Amount *Currency `json:"amount,omitempty"` + } + + // ShippingCost maps to shipping_cost object + ShippingCost struct { + Amount *Currency `json:"amount,omitempty"` + Tax *Tax `json:"tax,omitempty"` + } + + // Tax maps to tax object + Tax struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Percent int `json:"percent"` + Amount *Currency `json:"amount,omitempty"` + } + + // CustomAmount maps to custom_amount object + CustomAmount struct { + Label string `json:"label,omitempty"` + Amount *Currency `json:"amount,omitempty"` + } + + // PaymentDetail maps to payment_detail object + PaymentDetail struct { + 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"` + } + + // RefundDetail maps to refund_detail object + RefundDetail struct { + Type RefundDetailType `json:"type"` + Date *Date `json:"date,omitempty"` + Note string `json:"note,omitempty"` + } + + // Metadata maps to metadata object + Metadata struct { + 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 + InvoiceSearch 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 *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"` + } + + // 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/notification.go b/notification.go new file mode 100644 index 0000000..237cc9c --- /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..ba3d1af --- /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 *Date `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"` + } +) diff --git a/order.go b/order.go new file mode 100644 index 0000000..f2e1572 --- /dev/null +++ b/order.go @@ -0,0 +1,95 @@ +package paypal + +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, *http.Response) { + req, err := NewRequest("GET", fmt.Sprintf("%s/payments/orders/%s", c.APIBase, orderID), nil) + if err != nil { + return nil, err, nil + } + + v := &Order{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + return v, nil, resp +} + +// AuthorizeOrder authorizes an order +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, nil + } + + v := &Authorization{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + 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, *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"` + }{ + Amount: amount, + IsFinalCapture: isFinal, + }) + if err != nil { + return nil, err, nil + } + + v := &Capture{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + 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, *http.Response) { + req, err := NewRequest("POST", fmt.Sprintf("%s/payments/orders/%s/do-void", c.APIBase, orderID), nil) + if err != nil { + return nil, err, nil + } + + v := &Order{} + + resp, err := c.SendWithAuth(req, v) + if err != nil { + return nil, err, resp + } + + 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, *http.Response) { + return c.RefundCapture(captureID, a) +} diff --git a/payment.go b/payment.go index d52b13f..b368988 100644 --- a/payment.go +++ b/payment.go @@ -1,6 +1,11 @@ package paypal -import "fmt" +import ( + "fmt" + "net/http" +) + +// https://developer.paypal.com/webapps/developer/docs/api/#payments type ( CreatePaymentResp struct { @@ -21,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"` @@ -47,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 { @@ -96,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/paymenttype.go b/paymenttype.go index 5f792c0..0c57aa9 100644 --- a/paymenttype.go +++ b/paymenttype.go @@ -18,9 +18,7 @@ var ( CaptureStateCompleted CaptureState = "completed" CaptureStateRefunded CaptureState = "refunded" CaptureStatePartiallyRefunded CaptureState = "partially_refunded" - - CreditCardStateExpired CreditCardState = "expired" - CreditCardStateOK CreditCardState = "ok" + CaptureStateAuthorized CaptureState = "authorized" OrderStatePending OrderState = "PENDING" OrderStateCompleted OrderState = "COMPLETED" @@ -70,10 +68,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" @@ -96,7 +90,6 @@ var ( type ( AuthorizationState string CaptureState string - CreditCardState string OrderState string PendingReason string ReasonCode string @@ -104,7 +97,6 @@ type ( ProtectionEligibilityType string TaxIDType string PaymentState string - AddressType string PaymentMethod string PayerStatus string PaymentIntent string @@ -113,15 +105,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 { @@ -184,29 +168,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 string `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 { @@ -268,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 diff --git a/paypal.go b/paypal.go index 871b879..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 ( @@ -36,11 +40,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 @@ -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) @@ -102,7 +121,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 +132,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") @@ -123,22 +142,32 @@ func (c *Client) Send(req *http.Request, v interface{}) error { req.Header.Set("Content-type", "application/json") } - log.Println(req.Method, ": ", req.URL) + // 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 { - return err + return resp, err } defer resp.Body.Close() if c := resp.StatusCode; c < 200 || c > 299 { errResp := &ErrorResponse{Response: resp} data, err := ioutil.ReadAll(resp.Body) + if Debug { + log.Println(string(data)) + } if err == nil && len(data) > 0 { json.Unmarshal(data, errResp) } - return errResp + return resp, errResp } if v != nil { @@ -147,22 +176,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..f4c5a21 --- /dev/null +++ b/vault.go @@ -0,0 +1,83 @@ +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 + } + + resp, err := c.SendWithAuth(req, nil) + + 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(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 *PatchCreditCard `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 +}