diff --git a/README.md b/README.md index 0e7aa327..3b4a7047 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/billing.go b/billing.go index 90aacd9b..4c5397da 100644 --- a/billing.go +++ b/billing.go @@ -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 } diff --git a/const.go b/const.go new file mode 100644 index 00000000..60b0dcb7 --- /dev/null +++ b/const.go @@ -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" +) \ No newline at end of file diff --git a/go.mod b/go.mod index 1ae798d9..13768d3b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/plutov/paypal/v3 go 1.12 + +require github.com/stretchr/testify v1.6.0 diff --git a/go.sum b/go.sum index e69de29b..e357f9bc 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/integration_test.go b/integration_test.go index aa6902ba..b0c2c389 100644 --- a/integration_test.go +++ b/integration_test.go @@ -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) + +} diff --git a/patch.go b/patch.go new file mode 100644 index 00000000..6739bd91 --- /dev/null +++ b/patch.go @@ -0,0 +1,7 @@ +package paypal + +type Patch struct { + Operation string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value"` +} diff --git a/products.go b/products.go new file mode 100644 index 00000000..61e03673 --- /dev/null +++ b/products.go @@ -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 +} diff --git a/subscription.go b/subscription.go index c6760b65..b420dce4 100644 --- a/subscription.go +++ b/subscription.go @@ -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 +} diff --git a/subscription_plan.go b/subscription_plan.go new file mode 100644 index 00000000..30f1a453 --- /dev/null +++ b/subscription_plan.go @@ -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 +} diff --git a/transaction_search.go b/transaction_search.go index d74552e4..9fdf7738 100644 --- a/transaction_search.go +++ b/transaction_search.go @@ -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. diff --git a/types.go b/types.go index 24fbc5be..576f241c 100644 --- a/types.go +++ b/types.go @@ -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