Subscriptions, Subscription plans and Products API (#151)

This commit is contained in:
Rami 2020-05-31 07:14:19 +02:00 committed by GitHub
parent b3eb2c69dd
commit d355a65df0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 883 additions and 66 deletions

View File

@ -20,11 +20,6 @@ Currently supports **v2** only, if you want to use **v1**, use **v1.1.4** git ta
* GET /v1/payment-experience/web-profiles/**ID**
* PUT /v1/payment-experience/web-profiles/**ID**
* DELETE /v1/payment-experience/web-profiles/**ID**
* POST /v1/vault/credit-cards
* DELETE /v1/vault/credit-cards/**ID**
* PATCH /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards
* GET /v2/payments/authorizations/**ID**
* POST /v2/payments/authorizations/**ID**/capture
* POST /v2/payments/authorizations/**ID**/void
@ -32,24 +27,55 @@ Currently supports **v2** only, if you want to use **v1**, use **v1.1.4** git ta
* GET /v1/payments/sale/**ID**
* POST /v1/payments/sale/**ID**/refund
* GET /v2/payments/refund/**ID**
* POST /v1/reporting/transactions
#Vault
* POST /v1/vault/credit-cards
* DELETE /v1/vault/credit-cards/**ID**
* PATCH /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards
#Checkout
* POST /v2/checkout/orders
* GET /v2/checkout/orders/**ID**
* PATCH /v2/checkout/orders/**ID**
* POST /v2/checkout/orders/**ID**/authorize
* POST /v2/checkout/orders/**ID**/capture
#Billing plans (payments)
* GET /v1/payments/billing-plans
* POST /v1/payments/billing-plans
* PATCH /v1/payments/billing-plans/***ID***
* POST /v1/payments/billing-agreements
* POST /v1/payments/billing-agreements/***TOKEN***/agreement-execute
#Notifications
* POST /v1/notifications/webhooks
* GET /v1/notifications/webhooks
* GET /v1/notifications/webhooks/**ID**
* PATCH /v1/notifications/webhooks/**ID**
* DELETE /v1/notifications/webhooks/**ID**
* POST /v1/notifications/verify-webhook-signature
* POST /v1/reporting/transactions
#Products (Catalog)
* POST /v1/catalogs/products
* PATCH /v1/catalogs/products/**ID**
* GET /v1/catalogs/products/**ID**
* GET /v1/catalogs/products
#Billing Plans (Subscriptions)
* POST /v1/billing/plans
* PATCH /v1/billing/plans/**ID**
* GET /v1/billing/plans/**ID**
* GET /v1/billing/plans
* POST /v1/billing/plans/**ID**/activate
* POST /v1/billing/plans/**ID**/deactivate
* POST /v1/billing/plans/**ID**/update-pricing-schemes
#Subscriptions
* POST /v1/billing/subscriptions
* PATCH /v1/billing/subscriptions/**ID**
* GET /v1/billing/subscriptions/**ID**
* POST /v1/billing/subscriptions/**ID**/activate
* POST /v1/billing/subscriptions/**ID**/cancel
* POST /v1/billing/subscriptions/**ID**/capture
* POST /v1/billing/subscriptions/**ID**/suspend
* GET /v1/billing/subscriptions/**ID**/transactions
### Missing endpoints
It is possible that some endpoints are missing in this SDK Client, but you can use built-in **paypal** functions to perform a request: **NewClient -> NewRequest -> SendWithAuth**

View File

@ -2,6 +2,7 @@ package paypal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -31,26 +32,21 @@ type (
// BillingPlanListParams struct
BillingPlanListParams struct {
Page string `json:"page,omitempty"` //Default: 0.
Status string `json:"status,omitempty"` //Allowed values: CREATED, ACTIVE, INACTIVE, ALL.
PageSize string `json:"page_size,omitempty"` //Default: 10.
TotalRequired string `json:"total_required,omitempty"` //Default: no.
ListParams
Status string `json:"status,omitempty"` //Allowed values: CREATED, ACTIVE, INACTIVE, ALL.
}
//BillingPlanListResp struct
BillingPlanListResp struct {
Plans []BillingPlan `json:"plans,omitempty"`
TotalItems string `json:"total_items,omitempty"`
TotalPages string `json:"total_pages,omitempty"`
Links []Link `json:"links,omitempty"`
SharedListResponse
Plans []BillingPlan `json:"plans,omitempty"`
}
)
// CreateBillingPlan creates a billing plan in Paypal
// Endpoint: POST /v1/payments/billing-plans
func (c *Client) CreateBillingPlan(plan BillingPlan) (*CreateBillingResp, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans"), plan)
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans"), plan)
response := &CreateBillingResp{}
if err != nil {
return response, err
@ -59,18 +55,35 @@ func (c *Client) CreateBillingPlan(plan BillingPlan) (*CreateBillingResp, error)
return response, err
}
// UpdateBillingPlan updates values inside a billing plan
// Endpoint: PATCH /v1/payments/billing-plans
func (c *Client) UpdateBillingPlan(planId string, pathValues map[string]map[string]interface{}) error {
patchData := []Patch{}
for path, data := range pathValues {
patchData = append(patchData, Patch{
Operation: "replace",
Path: path,
Value: data,
})
}
jsonData, err := json.Marshal(patchData)
buf := bytes.NewBuffer(jsonData)
req, err := c.NewRequest(http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payments/billing-plans/", planId), buf)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// ActivatePlan activates a billing plan
// By default, a new plan is not activated
// Endpoint: PATCH /v1/payments/billing-plans/
func (c *Client) ActivatePlan(planID string) error {
buf := bytes.NewBuffer([]byte(`[{"op":"replace","path":"/","value":{"state":"ACTIVE"}}]`))
req, err := http.NewRequest("PATCH", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans/"+planID), buf)
if err != nil {
return err
}
req.SetBasicAuth(c.ClientID, c.Secret)
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
return c.SendWithAuth(req, nil)
return c.UpdateBillingPlan(planID, map[string]map[string]interface{}{
"/": {"state": BillingPlanStatusActive},
})
}
// CreateBillingAgreement creates an agreement for specified plan
@ -81,7 +94,7 @@ func (c *Client) CreateBillingAgreement(a BillingAgreement) (*CreateAgreementRes
ID: a.Plan.ID,
}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-agreements"), a)
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-agreements"), a)
response := &CreateAgreementResp{}
if err != nil {
return response, err
@ -93,7 +106,7 @@ func (c *Client) CreateBillingAgreement(a BillingAgreement) (*CreateAgreementRes
// ExecuteApprovedAgreement - Use this call to execute (complete) a PayPal agreement that has been approved by the payer.
// Endpoint: POST /v1/payments/billing-agreements/token/agreement-execute
func (c *Client) ExecuteApprovedAgreement(token string) (*ExecuteAgreementResponse, error) {
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-agreements/"+token+"/agreement-execute"), nil)
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/payments/billing-agreements/%s/agreement-execute", c.APIBase, token), nil)
response := &ExecuteAgreementResponse{}
if err != nil {
@ -118,16 +131,18 @@ func (c *Client) ExecuteApprovedAgreement(token string) (*ExecuteAgreementRespon
// Endpoint: GET /v1/payments/billing-plans
func (c *Client) ListBillingPlans(bplp BillingPlanListParams) (*BillingPlanListResp, error) {
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans"), nil)
response := &BillingPlanListResp{}
if err != nil {
return response, err
}
q := req.URL.Query()
q.Add("page", bplp.Page)
q.Add("page_size", bplp.PageSize)
q.Add("status", bplp.Status)
q.Add("total_required", bplp.TotalRequired)
req.URL.RawQuery = q.Encode()
response := &BillingPlanListResp{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}

79
const.go Normal file
View File

@ -0,0 +1,79 @@
package paypal
type SubscriptionPlanStatus string
const (
SubscriptionPlanStatusCreated SubscriptionPlanStatus = "CREATED"
SubscriptionPlanStatusInactive SubscriptionPlanStatus = "INACTIVE"
SubscriptionPlanStatusActive SubscriptionPlanStatus = "ACTIVE"
)
type BillingPlanStatus string
const (
BillingPlanStatusActive BillingPlanStatus = "ACTIVE"
)
type IntervalUnit string
const (
IntervalUnitDay IntervalUnit = "DAY"
IntervalUnitWeek IntervalUnit = "WEEK"
IntervalUnitMonth IntervalUnit = "MONTH"
IntervalUnitYear IntervalUnit = "YEAR"
)
type TenureType string
const (
TenureTypeRegular TenureType = "REGULAR"
TenureTypeTrial TenureType = "TRIAL"
)
type SetupFeeFailureAction string
const (
SetupFeeFailureActionContinue SetupFeeFailureAction = "CONTINUE"
SetupFeeFailureActionCancel SetupFeeFailureAction = "CANCEL"
)
type ShippingPreference string
const (
ShippingPreferenceGetFromFile ShippingPreference = "GET_FROM_FILE"
ShippingPreferenceNoShipping ShippingPreference = "NO_SHIPPING"
ShippingPreferenceSetProvidedAddress ShippingPreference = "SET_PROVIDED_ADDRESS"
)
type UserAction string
const (
UserActionContinue UserAction = "CONTINUE"
UserActionSubscribeNow UserAction = "SUBSCRIBE_NOW"
)
type SubscriptionStatus string
const (
SubscriptionStatusApprovalPending SubscriptionStatus = "APPROVAL_PENDING"
SubscriptionStatusApproved SubscriptionStatus = "APPROVED"
SubscriptionStatusActive SubscriptionStatus = "ACTIVE"
SubscriptionStatusSuspended SubscriptionStatus = "SUSPENDED"
SubscriptionStatusCancelled SubscriptionStatus = "CANCELLED"
SubscriptionStatusExpired SubscriptionStatus = "EXPIRED"
)
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-transaction
type SubscriptionTransactionStatus string
const (
SubscriptionCaptureStatusCompleted SubscriptionTransactionStatus = "COMPLETED"
SubscriptionCaptureStatusDeclined SubscriptionTransactionStatus = "DECLINED"
SubscriptionCaptureStatusPartiallyRefunded SubscriptionTransactionStatus = "PARTIALLY_REFUNDED"
SubscriptionCaptureStatusPending SubscriptionTransactionStatus = "PENDING"
SubscriptionCaptureStatusRefunded SubscriptionTransactionStatus = "REFUNDED"
)
type CaptureType string
const (
CaptureTypeOutstandingBalance CaptureType = "OUTSTANDING_BALANCE"
)

2
go.mod
View File

@ -1,3 +1,5 @@
module github.com/plutov/paypal/v3
go 1.12
require github.com/stretchr/testify v1.6.0

11
go.sum
View File

@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -3,7 +3,9 @@
package paypal
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// All test values are defined here
@ -12,6 +14,9 @@ var testSecret = "EBoIiUSkCKeSk49hHSgTem1qnjzzJgRQHDEHvGpzlLEf_nIoJd91xu8rPOBDCd
var testUserID = "https://www.paypal.com/webapps/auth/identity/user/VBqgHcgZwb1PBs69ybjjXfIW86_Hr93aBvF_Rgbh2II"
var testCardID = "CARD-54E6956910402550WKGRL6EA"
var testProductId = "" // will be fetched in func TestProduct(t *testing.T)
var testBillingPlan = "" // will be fetched in func TestSubscriptionPlans(t *testing.T)
func TestGetAccessToken(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
token, err := c.GetAccessToken()
@ -212,3 +217,155 @@ func TestListWebhooks(t *testing.T) {
t.Errorf("Cannot registered list webhooks, error %v", err)
}
}
func TestProduct(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
//create a product
productData := Product{
Name: "Test Product",
Description: "A Test Product",
Category: PRODUCT_CATEGORY_SOFTWARE,
Type: PRODUCT_TYPE_SERVICE,
ImageUrl: "https://example.com/image.png",
HomeUrl: "https://example.com",
}
productCreateResponse, err := c.CreateProduct(productData)
assert.Equal(t, nil, err)
testProductId = productCreateResponse.ID
//update the product
productData.ID = productCreateResponse.ID
productData.Description = "Updated product"
err = c.UpdateProduct(productData)
assert.Equal(t, nil, err)
//get product data
productFetched, err := c.GetProduct(productData.ID)
assert.Equal(t, nil, err)
assert.Equal(t, productFetched.Description, "Updated product")
//test that lising products have more than one product
productList, err := c.ListProducts(nil)
assert.Equal(t, nil, err)
assert.NotEqual(t, len(productList.Products), 0)
}
func TestSubscriptionPlans(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
//create a product
newSubscriptionPlan := SubscriptionPlan{
ProductId: testProductId,
Name: "Test subscription plan",
Status: SubscriptionPlanStatusCreated,
Description: "Integration test subscription plan",
BillingCycles: []BillingCycle{
{
PricingScheme: PricingScheme{
Version: 1,
FixedPrice: Money{
Currency: "EUR",
Value: "5",
},
CreateTime: time.Now(),
UpdateTime: time.Now(),
},
Frequency: Frequency{
IntervalUnit: IntervalUnitYear,
IntervalCount: 1,
},
TenureType: TenureTypeRegular,
Sequence: 1,
TotalCycles: 0,
},
},
PaymentPreferences: PaymentPreferences{
AutoBillOutstanding: false,
SetupFee: nil,
SetupFeeFailureAction: SetupFeeFailureActionCancel,
PaymentFailureThreshold: 0,
},
Taxes: Taxes{
Percentage: "19",
Inclusive: false,
},
QuantitySupported: false,
}
//test create new plan
planCreateResponse, err := c.CreateSubscriptionPlan(newSubscriptionPlan)
assert.Equal(t, nil, err)
testBillingPlan = planCreateResponse.ID // for next test
//test update the newly created plan
newSubscriptionPlan.ID = planCreateResponse.ID
newSubscriptionPlan.Description = "updated description"
err = c.UpdateSubscriptionPlan(newSubscriptionPlan)
assert.Equal(t, nil, err)
//test get plan information
existingPlan, err := c.GetSubscriptionPlan(newSubscriptionPlan.ID)
assert.Equal(t, nil, err)
assert.Equal(t, newSubscriptionPlan.Description, existingPlan.Description)
//test activate plan
err = c.ActivateSubscriptionPlan(newSubscriptionPlan.ID)
assert.Equal(t, nil, err)
//test deactivate plan
err = c.DeactivateSubscriptionPlans(newSubscriptionPlan.ID)
assert.Equal(t, nil, err)
//reactivate this plan for next next (subscription)
err = c.ActivateSubscriptionPlan(newSubscriptionPlan.ID)
assert.Equal(t, nil, err)
//test upadte plan pricing
err = c.UpdateSubscriptionPlanPricing(newSubscriptionPlan.ID, []PricingSchemeUpdate{
{
BillingCycleSequence: 1,
PricingScheme: PricingScheme{
Version: 1,
FixedPrice: Money{
Currency: "EUR",
Value: "6",
},
CreateTime: time.Now(),
UpdateTime: time.Now(),
},
},
})
assert.Equal(t, nil, err)
//test update pricing scheme
updatedPricingPlan, err := c.GetSubscriptionPlan(newSubscriptionPlan.ID)
assert.Equal(t, nil, err)
assert.Equal(t, "6.0", updatedPricingPlan.BillingCycles[0].PricingScheme.FixedPrice.Value)
}
func TestSubscription(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
newSubscription := SubscriptionBase{
PlanID: testBillingPlan,
}
//create new subscription
newSubResponse, err := c.CreateSubscription(newSubscription)
assert.Equal(t, nil, err)
assert.NotEqual(t, "", newSubResponse.ID)
//get subscription details
subDetails, err := c.GetSubscriptionDetails(newSubResponse.ID)
assert.Equal(t, nil, err)
assert.NotEqual(t, "", subDetails.ID)
}

7
patch.go Normal file
View File

@ -0,0 +1,7 @@
package paypal
type Patch struct {
Operation string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}

129
products.go Normal file
View File

@ -0,0 +1,129 @@
package paypal
import (
"fmt"
"net/http"
)
type (
ProductType string
ProductCategory string
// Product struct
Product struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description"`
Category ProductCategory `json:"category,omitempty"`
Type ProductType `json:"type"`
ImageUrl string `json:"image_url"`
HomeUrl string `json:"home_url"`
}
CreateProductResponse struct {
Product
SharedResponse
}
ListProductsResponse struct {
Products []Product `json:"products"`
SharedListResponse
}
ProductListParameters struct {
ListParams
}
)
func (self *Product) GetUpdatePatch() []Patch {
return []Patch{
{
Operation: "replace",
Path: "/description",
Value: self.Description,
},
{
Operation: "replace",
Path: "/category",
Value: self.Category,
},
{
Operation: "replace",
Path: "/image_url",
Value: self.ImageUrl,
},
{
Operation: "replace",
Path: "/home_url",
Value: self.HomeUrl,
},
}
}
const (
PRODUCT_TYPE_PHYSICAL ProductType = "PHYSICAL"
PRODUCT_TYPE_DIGITAL ProductType = "DIGITAL"
PRODUCT_TYPE_SERVICE ProductType = "SERVICE"
PRODUCT_CATEGORY_SOFTWARE ProductCategory = "software"
)
// CreateProduct creates a product
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_create
// Endpoint: POST /v1/catalogs/products
func (c *Client) CreateProduct(product Product) (*CreateProductResponse, error) {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), product)
response := &CreateProductResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateProduct. updates a product information
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_patch
// Endpoint: PATCH /v1/catalogs/products/:product_id
func (c *Client) UpdateProduct(product Product) error {
req, err := c.NewRequest(http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/catalogs/products/", product.ID), product.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Get product details
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_get
// Endpoint: GET /v1/catalogs/products/:product_id
func (c *Client) GetProduct(productId string) (*Product, error) {
req, err := c.NewRequest(http.MethodGet, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/catalogs/products/", productId), nil)
response := &Product{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// List all products
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_list
// Endpoint: GET /v1/catalogs/products
func (c *Client) ListProducts(params *ProductListParameters) (*ListProductsResponse, error) {
req, err := c.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), nil)
response := &ListProductsResponse{}
if err != nil {
return response, err
}
if params != nil {
q := req.URL.Query()
q.Add("page", params.Page)
q.Add("page_size", params.PageSize)
q.Add("total_required", params.TotalRequired)
req.URL.RawQuery = q.Encode()
}
err = c.SendWithAuth(req, response)
return response, err
}

View File

@ -7,27 +7,110 @@ import (
)
type (
SubscriptionBase struct {
PlanID string `json:"plan_id"`
StartTime *JSONTime `json:"start_time,omitempty"`
Quantity string `json:"quantity,omitempty"`
ShippingAmount *Money `json:"shipping_amount,omitempty"`
Subscriber *Subscriber `json:"subscriber,omitempty"`
AutoRenewal bool `json:"auto_renewal,omitempty"`
ApplicationContext *ApplicationContext `json:"application_context,omitempty"`
}
SubscriptionDetails struct {
ID string `json:"id,omitempty"`
SubscriptionStatus SubscriptionStatus `json:"status,omitempty"`
SubscriptionStatusChangeNote string `json:"status_change_note,omitempty"`
StatusUpdateTime time.Time `json:"status_update_time,omitempty"`
}
Subscription struct {
SubscriptionDetailResp
}
// SubscriptionDetailResp struct
SubscriptionDetailResp struct {
ID string `json:"id,omitempty"`
PlanID string `json:"plan_id,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
Quantity string `json:"quantity,omitempty"`
ShippingAmount ShippingAmount `json:"shipping_amount,omitempty"`
Subscriber Subscriber `json:"subscriber,omitempty"`
BillingInfo BillingInfo `json:"billing_info,omitempty"`
CreateTime time.Time `json:"create_time,omitempty"`
UpdateTime time.Time `json:"update_time,omitempty"`
Links []Link `json:"links,omitempty"`
Status string `json:"status,omitempty"`
StatusUpdateTime time.Time `json:"status_update_time,omitempty"`
SubscriptionBase
SubscriptionDetails
BillingInfo BillingInfo `json:"billing_info,omitempty"` // not found in documentation
}
SubscriptionCaptureResponse struct {
Status SubscriptionTransactionStatus `json:"status"`
Id string `json:"id"`
AmountWithBreakdown AmountWithBreakdown `json:"amount_with_breakdown"`
PayerName Name `json:"payer_name"`
PayerEmail string `json:"payer_email"`
Time time.Time `json:"time"`
}
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-amount_with_breakdown
AmountWithBreakdown struct {
GrossAmount Money `json:"gross_amount"`
FeeAmount Money `json:"fee_amount"`
ShippingAmount Money `json:"shipping_amount"`
TaxAmount Money `json:"tax_amount"`
NetAmount Money `json:"net_amount"`
}
SubscriptionTransactionsParams struct {
SubscriptionId string
StartTime time.Time
EndTime time.Time
}
SubscriptionTransactionsResponse struct {
Transactions []SubscriptionCaptureResponse `json:"transactions"`
SharedListResponse
}
CaptureReqeust struct {
Note string `json:"note"`
CaptureType CaptureType `json:"capture_type"`
Amount Money `json:"amount"`
}
)
func (self *Subscription) GetUpdatePatch() []Patch {
result := []Patch{
{
Operation: "replace",
Path: "/billing_info/outstanding_balance",
Value: self.BillingInfo.OutstandingBalance,
},
}
return result
}
// CreateSubscriptionPlan creates a subscriptionPlan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
// Endpoint: POST /v1/billing/subscriptions
func (c *Client) CreateSubscription(newSubscription SubscriptionBase) (*Subscription, error) {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/subscriptions"), newSubscription)
response := &Subscription{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_patch
// Endpoint: PATCH /v1/billing/subscriptions/:subscription_id
func (c *Client) UpdateSubscription(updatedSubscription Subscription) error {
req, err := c.NewRequest(http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/subscriptions/", updatedSubscription.ID), updatedSubscription.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// GetSubscriptionDetails shows details for a subscription, by ID.
// Endpoint: GET /v1/billing/subscriptions/
func (c *Client) GetSubscriptionDetails(subscriptionID string) (*SubscriptionDetailResp, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/billing/subscriptions/%s", c.APIBase, subscriptionID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v1/billing/subscriptions/%s", c.APIBase, subscriptionID), nil)
response := &SubscriptionDetailResp{}
if err != nil {
return response, err
@ -35,3 +118,70 @@ func (c *Client) GetSubscriptionDetails(subscriptionID string) (*SubscriptionDet
err = c.SendWithAuth(req, response)
return response, err
}
// Activates the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_activate
// Endpoint: POST /v1/billing/subscriptions/{id}/activate
func (c *Client) ActivateSubscription(subscriptionId, activateReason string) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/activate", c.APIBase, subscriptionId), map[string]string{"reason":activateReason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Cancels the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_cancel
// Endpoint: POST /v1/billing/subscriptions/{id}/cancel
func (c *Client) CancelSubscription(subscriptionId, cancelReason string) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/cancel", c.APIBase, subscriptionId), map[string]string{"reason": cancelReason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Captures an authorized payment from the subscriber on the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_capture
// Endpoint: POST /v1/billing/subscriptions/{id}/capture
func (c *Client) CaptureSubscription(subscriptionId string, request CaptureReqeust) (*SubscriptionCaptureResponse, error) {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/capture", c.APIBase, subscriptionId), request)
response := &SubscriptionCaptureResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// Suspends the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_suspend
// Endpoint: POST /v1/billing/subscriptions/{id}/suspend
func (c *Client) SuspendSubscription(subscriptionId, reason string) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/suspend", c.APIBase, subscriptionId), map[string]string{"reason": reason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Lists transactions for a subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_transactions
// Endpoint: GET /v1/billing/subscriptions/{id}/transactions
func (c *Client) GetSubscriptionTransactions(requestParams SubscriptionTransactionsParams) (*SubscriptionTransactionsResponse, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v1/billing/subscriptions/%s/transactions", c.APIBase, requestParams.SubscriptionId), nil)
response := &SubscriptionTransactionsResponse{}
if err != nil {
return response, err
}
q := req.URL.Query()
q.Add("start_time", requestParams.StartTime.Format(time.RFC3339Nano))
q.Add("end_time", requestParams.EndTime.Format(time.RFC3339Nano))
err = c.SendWithAuth(req, response)
return response, err
}

226
subscription_plan.go Normal file
View File

@ -0,0 +1,226 @@
package paypal
import (
"fmt"
"net/http"
"time"
)
type (
// SubscriptionDetailResp struct
SubscriptionPlan struct {
ID string `json:"id,omitempty"`
ProductId string `json:"product_id"`
Name string `json:"name"`
Status SubscriptionPlanStatus `json:"status"`
Description SubscriptionPlanStatus `json:"description,omitempty"`
BillingCycles []BillingCycle `json:"billing_cycles"`
PaymentPreferences PaymentPreferences `json:"payment_preferences"`
Taxes Taxes `json:"taxes"`
QuantitySupported bool `json:"quantity_supported"` //Indicates whether you can subscribe to this plan by providing a quantity for the goods or service.
}
// Doc https://developer.paypal.com/docs/api/subscriptions/v1/#definition-billing_cycle
BillingCycle struct {
PricingScheme PricingScheme `json:"pricing_scheme"` // The active pricing scheme for this billing cycle. A free trial billing cycle does not require a pricing scheme.
Frequency Frequency `json:"frequency"` // The frequency details for this billing cycle.
TenureType TenureType `json:"tenure_type"` // The tenure type of the billing cycle. In case of a plan having trial cycle, only 2 trial cycles are allowed per plan. The possible values are:
Sequence int `json:"sequence"` // The order in which this cycle is to run among other billing cycles. For example, a trial billing cycle has a sequence of 1 while a regular billing cycle has a sequence of 2, so that trial cycle runs before the regular cycle.
TotalCycles int `json:"total_cycles"` // The number of times this billing cycle gets executed. Trial billing cycles can only be executed a finite number of times (value between 1 and 999 for total_cycles). Regular billing cycles can be executed infinite times (value of 0 for total_cycles) or a finite number of times (value between 1 and 999 for total_cycles).
}
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-payment_preferences
PaymentPreferences struct {
AutoBillOutstanding bool `json:"auto_bill_outstanding"`
SetupFee *Money `json:"setup_fee"`
SetupFeeFailureAction SetupFeeFailureAction `json:"setup_fee_failure_action"`
PaymentFailureThreshold int `json:"payment_failure_threshold"`
}
PricingScheme struct {
Version int `json:"version"`
FixedPrice Money `json:"fixed_price"`
CreateTime time.Time `json:"create_time"`
UpdateTime time.Time `json:"update_time"`
}
PricingSchemeUpdateRequest struct {
Schemes []PricingSchemeUpdate `json:"pricing_schemes"`
}
PricingSchemeUpdate struct {
BillingCycleSequence int `json:"billing_cycle_sequence"`
PricingScheme PricingScheme `json:"pricing_scheme"`
}
//doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-frequency
Frequency struct {
IntervalUnit IntervalUnit `json:"interval_unit"`
IntervalCount int `json:"interval_count"` //different per unit. check documentation
}
Taxes struct {
Percentage string `json:"percentage"`
Inclusive bool `json:"inclusive"`
}
CreateSubscriptionPlanResponse struct {
SubscriptionPlan
SharedResponse
}
SubscriptionPlanListParameters struct {
ProductId string `json:"product_id"`
PlanIds string `json:"plan_ids"` // Filters the response by list of plan IDs. Filter supports upto 10 plan IDs.
ListParams
}
ListSubscriptionPlansResponse struct {
Plans []SubscriptionPlan `json:"plans"`
SharedListResponse
}
)
func (self *SubscriptionPlan) GetUpdatePatch() []Patch {
result := []Patch{
{
Operation: "replace",
Path: "/description",
Value: self.Description,
},
{
Operation: "replace",
Path: "/payment_preferences/auto_bill_outstanding",
Value: self.PaymentPreferences.AutoBillOutstanding,
},
{
Operation: "replace",
Path: "/payment_preferences/payment_failure_threshold",
Value: self.PaymentPreferences.PaymentFailureThreshold,
},
{
Operation: "replace",
Path: "/payment_preferences/setup_fee_failure_action",
Value: self.PaymentPreferences.SetupFeeFailureAction,
},
{
Operation: "replace",
Path: "/taxes/percentage",
Value: self.Taxes.Percentage,
},
}
if self.PaymentPreferences.SetupFee != nil {
result = append(result, Patch{
Operation: "replace",
Path: "/payment_preferences/setup_fee",
Value: self.PaymentPreferences.SetupFee,
},
)
}
return result
}
// CreateSubscriptionPlan creates a subscriptionPlan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_create
// Endpoint: POST /v1/billing/plans
func (c *Client) CreateSubscriptionPlan(newPlan SubscriptionPlan) (*CreateSubscriptionPlanResponse, error) {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/plans"), newPlan)
response := &CreateSubscriptionPlanResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_patch
// Endpoint: PATCH /v1/billing/plans/:plan_id
func (c *Client) UpdateSubscriptionPlan(updatedPlan SubscriptionPlan) error {
req, err := c.NewRequest(http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/plans/", updatedPlan.ID), updatedPlan.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_get
// Endpoint: GET /v1/billing/plans/:plan_id
func (c *Client) GetSubscriptionPlan(planId string) (*SubscriptionPlan, error) {
req, err := c.NewRequest(http.MethodGet, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/plans/", planId), nil)
response := &SubscriptionPlan{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// List all plans
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_list
// Endpoint: GET /v1/billing/plans
func (c *Client) ListSubscriptionPlans(params *SubscriptionPlanListParameters) (*ListProductsResponse, error) {
req, err := c.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/plans"), nil)
response := &ListProductsResponse{}
if err != nil {
return response, err
}
if params != nil {
q := req.URL.Query()
q.Add("page", params.Page)
q.Add("page_size", params.PageSize)
q.Add("total_required", params.TotalRequired)
q.Add("product_id", params.ProductId)
q.Add("plan_ids", params.PlanIds)
req.URL.RawQuery = q.Encode()
}
err = c.SendWithAuth(req, response)
return response, err
}
// Activates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_activate
// Endpoint: POST /v1/billing/plans/{id}/activate
func (c *Client) ActivateSubscriptionPlan(planId string) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/activate", c.APIBase, planId), nil)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Deactivates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_deactivate
// Endpoint: POST /v1/billing/plans/{id}/deactivate
func (c *Client) DeactivateSubscriptionPlans(planId string) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/deactivate", c.APIBase, planId), nil)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Updates pricing for a plan. For example, you can update a regular billing cycle from $5 per month to $7 per month.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_update-pricing-schemes
// Endpoint: POST /v1/billing/plans/{id}/update-pricing-schemes
func (c *Client) UpdateSubscriptionPlanPricing(planId string, pricingSchemes []PricingSchemeUpdate) error {
req, err := c.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/update-pricing-schemes", c.APIBase, planId), PricingSchemeUpdateRequest{
Schemes: pricingSchemes,
})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}

View File

@ -30,9 +30,7 @@ type TransactionSearchResponse struct {
EndDate JSONTime `json:"end_date"`
LastRefreshDatetime JSONTime `json:"last_refreshed_datetime"`
Page int `json:"page"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
Links []Link `json:"links"`
SharedListResponse
}
// ListTransactions - Use this to search PayPal transactions from the last 31 days.

View File

@ -85,11 +85,6 @@ const (
// Possible values for `shipping_preference` in ApplicationContext
//
// https://developer.paypal.com/docs/api/orders/v2/#definition-application_context
const (
ShippingPreferenceGetFromFile string = "GET_FROM_FILE"
ShippingPreferenceNoShipping string = "NO_SHIPPING"
ShippingPreferenceSetProvidedAddress string = "SET_PROVIDED_ADDRESS"
)
const (
EventPaymentCaptureCompleted string = "PAYMENT.CAPTURE.COMPLETED"
@ -171,14 +166,15 @@ type (
}
// ApplicationContext struct
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-application_context
ApplicationContext struct {
BrandName string `json:"brand_name,omitempty"`
Locale string `json:"locale,omitempty"`
LandingPage string `json:"landing_page,omitempty"`
ShippingPreference string `json:"shipping_preference,omitempty"`
UserAction string `json:"user_action,omitempty"`
ReturnURL string `json:"return_url,omitempty"`
CancelURL string `json:"cancel_url,omitempty"`
BrandName string `json:"brand_name,omitempty"`
Locale string `json:"locale,omitempty"`
ShippingPreference ShippingPreference `json:"shipping_preference,omitempty"`
UserAction UserAction `json:"user_action,omitempty"`
//LandingPage string `json:"landing_page,omitempty"` // not found in documentation
ReturnURL string `json:"return_url,omitempty"`
CancelURL string `json:"cancel_url,omitempty"`
}
// Authorization struct
@ -358,10 +354,8 @@ type (
// CreditCards GET /v1/vault/credit-cards
CreditCards struct {
Items []CreditCard `json:"items"`
Links []Link `json:"links"`
TotalItems int `json:"total_items"`
TotalPages int `json:"total_pages"`
Items []CreditCard `json:"items"`
SharedListResponse
}
// CreditCardToken struct
@ -829,8 +823,7 @@ type (
//ShippingAmount struct
ShippingAmount struct {
CurrencyCode string `json:"currency_code,omitempty"`
Value string `json:"value,omitempty"`
Money
}
// ShippingAddress struct
@ -857,8 +850,14 @@ type (
}
// Name struct
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-name
Name struct {
FullName string `json:"full_name,omitempty"`
FullName string `json:"full_name,omitempty"`
Suffix string `json:"suffix,omitempty"`
Prefix string `json:"prefix,omitempty"`
GivenName string `json:"given_name,omitempty"`
Surname string `json:"surname,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
}
// ShippingDetail struct
@ -1184,6 +1183,24 @@ type (
ShippingInfo *SearchShippingInfo `json:"shipping_info"`
CartInfo *SearchCartInfo `json:"cart_info"`
}
SharedResponse struct {
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
Links []Link `json:"links"`
}
ListParams struct {
Page string `json:"page,omitempty"` //Default: 0.
PageSize string `json:"page_size,omitempty"` //Default: 10.
TotalRequired string `json:"total_required,omitempty"` //Default: no.
}
SharedListResponse struct {
TotalItems string `json:"total_items,omitempty"`
TotalPages string `json:"total_pages,omitempty"`
Links []Link `json:"links,omitempty"`
}
)
// Error method implementation for ErrorResponse struct