├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── example └── main.go ├── go.mod ├── go.sum └── paddle ├── alert_types.go ├── coupons.go ├── coupons_test.go ├── messages.go ├── modifiers.go ├── modifiers_test.go ├── one_off_charges.go ├── one_off_charges_test.go ├── order_details.go ├── order_details_test.go ├── paddle.go ├── paddle_test.go ├── pay_link.go ├── pay_link_test.go ├── payments.go ├── payments_test.go ├── plans.go ├── plans_test.go ├── prices.go ├── prices_test.go ├── products.go ├── products_test.go ├── refund_payment.go ├── refund_payment_test.go ├── user_history.go ├── user_history_test.go ├── users.go ├── users_test.go ├── webhooks.go └── webhooks_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.16 22 | 23 | - name: Run go fmt 24 | run: diff -u <(echo -n) <(gofmt -d -s .) 25 | 26 | - name: Run go vet 27 | run: go vet ./... 28 | 29 | - name: Test 30 | run: go test -v ./paddle 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Walid Berrahal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-paddle # 2 | 3 | [![go-paddle release (latest SemVer)](https://img.shields.io/github/v/release/Fakerr/go-paddle?sort=semver)](https://github.com/Fakerr/go-paddle/releases) 4 | [![Test Status](https://github.com/Fakerr/go-paddle/actions/workflows/test.yml/badge.svg)](https://github.com/Fakerr/go-paddle/actions/workflows/test.yml) 5 | 6 | go-paddle is a Go client library for accessing the [Paddle API](https://developer.paddle.com/api-reference/intro). 7 | 8 | ## Installation ## 9 | 10 | 11 | ```bash 12 | go get github.com/Fakerr/go-paddle 13 | ``` 14 | 15 | 16 | Alternatively the same can be achieved if you use import in a package: 17 | 18 | ```go 19 | import "github.com/Fakerr/go-paddle" 20 | ``` 21 | 22 | and run `go get` without paramters. 23 | 24 | ## Usage ## 25 | The package paddle comes with two different clients. A client for the Product, Subscription, Alert APIs that will require 26 | a vendor_id and a vendor_auth_code and a client for the Checkout API. 27 | 28 | 29 | The services of a client divide the API into logical chunks and correspond to 30 | the structure of the Paddle API documentation at 31 | https://developer.paddle.com/api-reference/. 32 | 33 | ### Product API, Subscription API, Alert API ### 34 | 35 | ```go 36 | import "github.com/Fakerr/go-paddle/paddle" 37 | ``` 38 | 39 | Construct a new Paddle client, then use the various services on the client to access different parts of the Paddle API. 40 | The client always requires a vendor_id and a vendor_auth_code arguments that you can get from the Paddle dashboard. For example: 41 | 42 | ```go 43 | client := paddle.NewClient(vendorId, vendorAuthCode, nil) 44 | 45 | // List all users subscribed to any of your subscription plans 46 | users, _, err := client.Users.List(context.Background(), nil) 47 | ``` 48 | 49 | Some API methods have optional parameters that can be passed. For example: 50 | 51 | ```go 52 | client := paddle.NewClient(vendorId, vendorAuthCode, nil) 53 | 54 | // List all users subscribed to any of your subscription plans 55 | opt := &UsersOptions{SubscriptionID: "1"} 56 | users, _, err := client.Users.List(context.Background(), opt) 57 | ``` 58 | 59 | NOTE: Using the [context](https://godoc.org/context) package, one can easily 60 | pass cancelation signals and deadlines to various services of the client for 61 | handling a request. In case there is no context available, then `context.Background()` 62 | can be used as a starting point. 63 | 64 | For more sample code snippets, head over to the 65 | [example](https://github.com/fakerr/go-paddle/tree/master/example) directory. 66 | 67 | ### Checkout API ### 68 | 69 | ```go 70 | import "github.com/Fakerr/go-paddle/paddle" 71 | ``` 72 | 73 | Construct a new Paddle checkout client, then use the various services on the client to access different parts of the Paddle API. 74 | 75 | ```go 76 | client := paddle.NewCheckoutClient(nil) 77 | 78 | // Retrieve prices for one or multiple products or plans 79 | options := &PricesOptions{CustomerCountry: "tn"} 80 | prices, _, err := client.Prices.Get(context.Background(), "1", options) 81 | 82 | ``` 83 | ### Sandbox environment ### 84 | If you want to send requests against a sandbox environment, the package paddle provides two specific clients for that purpose: 85 | 86 | ``` 87 | client := paddle.NewSandboxClient(sandboxVendorId, sandboxVendorAuthCode, nil) 88 | ``` 89 | or to access the checkout API 90 | 91 | ``` 92 | client := paddle.NewSandboxCheckoutClient(nil) 93 | ``` 94 | 95 | ### Pagination ### 96 | 97 | Some requests for resource collections (users, webhooks, etc.) 98 | support pagination. Pagination options are described in the 99 | `paddle.ListOptions` struct and passed to the list methods directly or as an 100 | embedded type of a more specific list options struct (for example 101 | `paddle.UsersOptions`). 102 | 103 | ```go 104 | client := paddle.NewClient(vendorId, vendorAuthCode, nil) 105 | 106 | // List all users subscribed to any of your subscription plans 107 | opt := &UsersOptions{ 108 | SubscriptionID: "1", 109 | ListOptions: ListOptions{Page: 2}, 110 | } 111 | users, _, err := client.Users.List(context.Background(), opt) 112 | ``` 113 | ### Webhooks ### 114 | go-paddle comes with helper functions in order to facilitate the validatation and parsing of webhook events. 115 | For recognized event types, a value of the corresponding struct type will be returned. 116 | 117 | Example usage: 118 | 119 | ```go 120 | func PaddleWebhookHandler(w http.ResponseWriter, r *http.Request) { 121 | payload, err := paddle.ValidatePayload(r, webhookSecretKey) 122 | if err != nil { ... } 123 | 124 | alert, err := paddle.ParsePayload(payload) 125 | if err != nil { ... } 126 | 127 | switch alert := alert.(type) { 128 | case *paddle.SubscriptionCreatedAlert: 129 | processSubscriptionCreatedAlert(alert) 130 | case *paddle.SubscriptionCanceledAlert: 131 | processSubscriptionCanceledAlert(alert) 132 | ... 133 | } 134 | } 135 | 136 | ``` 137 | 138 | ## Todos ## 139 | List of Paddle APIs that are not covered yet or are work in progress: 140 | - [ ] Licenses 141 | - [ ] Transactions 142 | 143 | ## Contributing ## 144 | Pull requests are welcome, along with any feedback or ideas. The calling pattern is pretty well established, so adding new methods is relatively 145 | straightforward. 146 | 147 | ## License ## 148 | 149 | This library is distributed under the MIT-style license found in the [LICENSE](./LICENSE) 150 | file. 151 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | // The simple command demonstrates a simple functionality which 2 | // list all users subscribed to any of your subscription plans 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/Fakerr/go-paddle/paddle" 11 | ) 12 | 13 | func main() { 14 | 15 | var vendorID = os.Getenv("VENDOR_ID") 16 | var vendorAuthCode = os.Getenv("VENDOR_AUTH_CODE") 17 | var planID = os.Getenv("PLAN_ID") 18 | 19 | client := paddle.NewClient(vendorID, vendorAuthCode, nil) 20 | 21 | options := &paddle.UsersOptions{ 22 | PlanID: planID, 23 | ListOptions: paddle.ListOptions{ResultsPerPage: 10}, 24 | } 25 | 26 | users, _, err := client.Users.List(context.Background(), options) 27 | if err != nil { 28 | fmt.Printf("Error: %v\n", err) 29 | return 30 | } 31 | 32 | for i, user := range users { 33 | fmt.Printf("%v. %v\n", i+1, *user.UserID) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Fakerr/go-paddle 2 | 3 | go 1.16 4 | 5 | require github.com/google/go-querystring v1.1.0 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 3 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /paddle/alert_types.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | // Sent when an order is processed for a product or plan with webhook fulfillment enabled 4 | // Paddle reference: https://developer.paddle.com/webhook-reference/product-fulfillment/fulfillment-webhook 5 | type FulfillmentWebhook struct { 6 | EventTime *string `json:"event_time"` 7 | PCountry *string `json:"p_country"` 8 | PCoupon *string `json:"p_coupon"` 9 | PCouponSavings *string `json:"p_coupon_savings"` 10 | PCurrency *string `json:"p_currency"` 11 | PEarnings *string `json:"p_earnings"` 12 | POrderID *string `json:"p_order_id"` 13 | PPaddleFee *string `json:"p_paddle_fee"` 14 | PPrice *string `json:"p_price"` 15 | PProductID *string `json:"p_product_id"` 16 | PQuantity *string `json:"p_quantity"` 17 | PSaleGross *string `json:"p_sale_gross"` 18 | PTaxAmount *string `json:"p_tax_amount"` 19 | PUsedPriceOverride *string `json:"p_used_price_override"` 20 | Passthrough *string `json:"passthrough"` 21 | Quantity *string `json:"quantity"` 22 | } 23 | 24 | // Fired when a new subscription is created, and a customer has successfully subscribed. 25 | // Paddle Reference: https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-created 26 | type SubscriptionCreatedAlert struct { 27 | AlertName *string `json:"alert_name"` 28 | AlertID *string `json:"alert_id"` 29 | CancelURL *string `json:"cancel_url"` 30 | CheckoutID *string `json:"checkout_id"` 31 | Currency *string `json:"currency"` 32 | Email *string `json:"email"` 33 | EventTime *string `json:"event_time"` 34 | MarketingConsent *string `json:"marketing_consent"` 35 | NextBillDate *string `json:"next_bill_date"` 36 | Passthrough *string `json:"passthrough"` 37 | Quantity *string `json:"quantity"` 38 | Source *string `json:"source"` 39 | Status *string `json:"status"` 40 | SubscriptionID *string `json:"subscription_id"` 41 | SubscriptionPlanID *string `json:"subscription_plan_id"` 42 | UnitPrice *string `json:"unit_price"` 43 | UserID *string `json:"user_id"` 44 | UpdateURL *string `json:"update_url"` 45 | } 46 | 47 | // Fired when the plan, price, quantity, status of an existing subscription changes, or if the payment date is rescheduled manually. 48 | // Paddle reference: https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-updated 49 | type SubscriptionUpdatedAlert struct { 50 | AlertName *string `json:"alert_name"` 51 | AlertID *string `json:"alert_id"` 52 | CancelURL *string `json:"cancel_url"` 53 | CheckoutID *string `json:"checkout_id"` 54 | Email *string `json:"email"` 55 | EventTime *string `json:"event_time"` 56 | MarketingConsent *string `json:"marketing_consent"` 57 | NewPrice *string `json:"new_price"` 58 | NewQuantity *string `json:"new_quantity"` 59 | NewUnitPrice *string `json:"new_unit_price"` 60 | NextBillDate *string `json:"next_bill_date"` 61 | OldPrice *string `json:"old_price"` 62 | OldQuantity *string `json:"old_quantity"` 63 | OldUnitPrice *string `json:"old_unit_price"` 64 | Currency *string `json:"currency"` 65 | Passthrough *string `json:"passthrough"` 66 | Status *string `json:"status"` 67 | SubscriptionID *string `json:"subscription_id"` 68 | SubscriptionPlanID *string `json:"subscription_plan_id"` 69 | UserID *string `json:"user_id"` 70 | UpdateURL *string `json:"update_url"` 71 | OldNextBillDate *string `json:"old_next_bill_date"` 72 | OldStatus *string `json:"old_status"` 73 | OldSubscriptionPlanID *string `json:"old_subscription_plan_id"` 74 | PausedAt *string `json:"paused_at"` 75 | PausedFrom *string `json:"paused_from"` 76 | PausedReason *string `json:"paused_reason"` 77 | } 78 | 79 | // The subscription canceled alert is triggered whenever a user cancel a subscription 80 | // Paddle Reference: https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-cancelled 81 | type SubscriptionCancelledAlert struct { 82 | AlertName *string `json:"alert_name"` 83 | AlertID *string `json:"alert_id"` 84 | CancellationEffectiveDate *string `json:"cancellation_effective_date"` 85 | CheckoutID *string `json:"checkout_id"` 86 | Currency *string `json:"currency"` 87 | Email *string `json:"email"` 88 | EventTime *string `json:"event_time"` 89 | MarketingConsent *string `json:"marketing_consent"` 90 | Passthrough *string `json:"passthrough"` 91 | Quantity *string `json:"quantity"` 92 | Status *string `json:"status"` 93 | SubscriptionID *string `json:"subscription_id"` 94 | SubscriptionPlanID *string `json:"subscription_plan_id"` 95 | UnitPrice *string `json:"unit_price"` 96 | UserID *string `json:"user_id"` 97 | } 98 | 99 | // Fired when a subscription payment is received successfully. 100 | // Paddle reference: https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-succeeded 101 | type SubscriptionPaymentSucceededAlert struct { 102 | AlertName *string `json:"alert_name"` 103 | AlertID *string `json:"alert_id"` 104 | BalanceCurrency *string `json:"balance_currency"` 105 | BalanceEarnings *string `json:"balance_earnings"` 106 | BalanceFee *string `json:"balance_fee"` 107 | BalanceGross *string `json:"balance_gross"` 108 | BalanceTax *string `json:"balance_tax"` 109 | CheckoutID *string `json:"checkout_id"` 110 | Country *string `json:"country"` 111 | Coupon *string `json:"coupon"` 112 | Currency *string `json:"currency"` 113 | CustomerName *string `json:"customer_name"` 114 | Earnings *string `json:"earnings"` 115 | Email *string `json:"email"` 116 | EventTime *string `json:"event_time"` 117 | Fee *string `json:"fee"` 118 | InitialPayment *string `json:"initial_payment"` 119 | Instalments *string `json:"instalments"` 120 | MarketingConsent *string `json:"marketing_consent"` 121 | NextBillDate *string `json:"next_bill_date"` 122 | NextPaymentAmount *string `json:"next_payment_amount"` 123 | OrderID *string `json:"order_id"` 124 | Passthrough *string `json:"passthrough"` 125 | PaymentMethod *string `json:"payment_method"` 126 | PaymentTax *string `json:"payment_tax"` 127 | PlanName *string `json:"plan_name"` 128 | Quantity *string `json:"quantity"` 129 | ReceiptURL *string `json:"receipt_url"` 130 | SaleGross *string `json:"sale_gross"` 131 | Status *string `json:"status"` 132 | SubscriptionID *string `json:"subscription_id"` 133 | SubscriptionPaymentID *string `json:"subscription_payment_id"` 134 | SubscriptionPlanID *string `json:"subscription_plan_id"` 135 | UnitPrice *string `json:"unit_price"` 136 | UserID *string `json:"user_id"` 137 | } 138 | 139 | // Fired when a payment for an existing subscription fails. 140 | // Paddle reference! https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-failed 141 | type SubscriptionPaymentFailedAlert struct { 142 | AlertName *string `json:"alert_name"` 143 | AlertID *string `json:"alert_id"` 144 | Amount *string `json:"amount"` 145 | CancelURL *string `json:"cancel_url"` 146 | CheckoutID *string `json:"checkout_id"` 147 | Currency *string `json:"currency"` 148 | Email *string `json:"email"` 149 | EventTime *string `json:"event_time"` 150 | MarketingConsent *string `json:"marketing_consent"` 151 | NextRetryDate *string `json:"next_retry_date"` 152 | Passthrough *string `json:"passthrough"` 153 | Quantity *string `json:"quantity"` 154 | Status *string `json:"status"` 155 | SubscriptionID *string `json:"subscription_id"` 156 | SubscriptionPlanID *string `json:"subscription_plan_id"` 157 | UnitPrice *string `json:"unit_price"` 158 | UpdateURL *string `json:"update_url"` 159 | SubscriptionPaymentID *string `json:"subscription_payment_id"` 160 | Instalments *string `json:"instalments"` 161 | OrderID *string `json:"order_id"` 162 | UserID *string `json:"user_id"` 163 | AttemptNumber *string `json:"attempt_number"` 164 | } 165 | 166 | // Fired when a refund for an existing subscription payment is issued. 167 | // Paddle reference: https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-refunded 168 | type SubscriptionPaymentRefundedAlert struct { 169 | AlertName *string `json:"alert_name"` 170 | AlertID *string `json:"alert_id"` 171 | Amount *string `json:"amount"` 172 | BalanceCurrency *string `json:"balance_currency"` 173 | BalanceEarningsDecrease *string `json:"balance_earnings_decrease"` 174 | BalanceFeeRefund *string `json:"balance_fee_refund"` 175 | BalanceGrossRefund *string `json:"balance_gross_refund"` 176 | BalanceTaxRefund *string `json:"balance_tax_refund"` 177 | CheckoutID *string `json:"checkout_id"` 178 | Currency *string `json:"currency"` 179 | EarningsDecrease *string `json:"earnings_decrease"` 180 | Email *string `json:"email"` 181 | EventTime *string `json:"event_time"` 182 | FeeRefund *string `json:"fee_refund"` 183 | GrossRefund *string `json:"gross_refund"` 184 | InitialPayment *string `json:"initial_payment"` 185 | Instalments *string `json:"instalments"` 186 | MarketingConsent *string `json:"marketing_consent"` 187 | OrderID *string `json:"order_id"` 188 | Passthrough *string `json:"passthrough"` 189 | Quantity *string `json:"quantity"` 190 | RefundReason *string `json:"refund_reason"` 191 | RefundType *string `json:"refund_type"` 192 | Status *string `json:"status"` 193 | SubscriptionID *string `json:"subscription_id"` 194 | SubscriptionPaymentID *string `json:"subscription_payment_id"` 195 | SubscriptionPlanID *string `json:"subscription_plan_id"` 196 | TaxRefund *string `json:"tax_refund"` 197 | UnitPrice *string `json:"unit_price"` 198 | UserID *string `json:"user_id"` 199 | } 200 | 201 | // Fired when a payment is made into your Paddle account. 202 | // Paddle reference: https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-succeeded 203 | type PaymentSucceededAlert struct { 204 | AlertName *string `json:"alert_name"` 205 | AlertID *string `json:"alert_id"` 206 | BalanceCurrency *string `json:"balance_currency"` 207 | BalanceEarnings *string `json:"balance_earnings"` 208 | BalanceFee *string `json:"balance_fee"` 209 | BalanceGross *string `json:"balance_gross"` 210 | BalanceTax *string `json:"balance_tax"` 211 | CheckoutID *string `json:"checkout_id"` 212 | Country *string `json:"country"` 213 | Coupon *string `json:"coupon"` 214 | Currency *string `json:"currency"` 215 | CustomerName *string `json:"customer_name"` 216 | Earnings *string `json:"earnings"` 217 | Email *string `json:"email"` 218 | EventTime *string `json:"event_time"` 219 | Fee *string `json:"fee"` 220 | IP *string `json:"ip"` 221 | MarketingConsent *string `json:"marketing_consent"` 222 | OrderID *string `json:"order_id"` 223 | Passthrough *string `json:"passthrough"` 224 | PaymentMethod *string `json:"payment_method"` 225 | PaymentTax *string `json:"payment_tax"` 226 | ProductID *string `json:"product_id"` 227 | ProductName *string `json:"product_name"` 228 | Quantity *string `json:"quantity"` 229 | ReceiptURL *string `json:"receipt_url"` 230 | SaleGross *string `json:"sale_gross"` 231 | UsedPriceOverride *string `json:"used_price_override"` 232 | } 233 | 234 | // Fired when a payment is refunded. 235 | // Paddle reference: https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-refunded 236 | type PaymentRefundedAlert struct { 237 | AlertName *string `json:"alert_name"` 238 | AlertID *string `json:"alert_id"` 239 | Amount *string `json:"amount"` 240 | BalanceCurrency *string `json:"balance_currency"` 241 | BalanceEarningsDecrease *string `json:"balance_earnings_decrease"` 242 | BalanceFeeRefund *string `json:"balance_fee_refund"` 243 | BalanceGrossRefund *string `json:"balance_gross_refund"` 244 | BalanceTaxRefund *string `json:"balance_tax_refund"` 245 | CheckoutID *string `json:"checkout_id"` 246 | Currency *string `json:"currency"` 247 | EarningsDecrease *string `json:"earnings_decrease"` 248 | Email *string `json:"email"` 249 | EventTime *string `json:"event_time"` 250 | FeeRefund *string `json:"fee_refund"` 251 | GrossRefund *string `json:"gross_refund"` 252 | MarketingConsent *string `json:"marketing_consent"` 253 | OrderID *string `json:"order_id"` 254 | Passthrough *string `json:"passthrough"` 255 | Quantity *string `json:"quantity"` 256 | RefundReason *string `json:"refund_reason"` 257 | RefundType *string `json:"refund_type"` 258 | TaxRefund *string `json:"tax_refund"` 259 | } 260 | 261 | // Fired when an order is created after a successful payment event. 262 | // Paddle reference: https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/order-processing-completed 263 | type LockerProcessedAlert struct { 264 | AlertName *string `json:"alert_name"` 265 | AlertID *string `json:"alert_id"` 266 | CheckoutID *string `json:"checkout_id"` 267 | CheckoutRecovery *string `json:"checkout_recovery"` 268 | Coupon *string `json:"coupon"` 269 | Download *string `json:"download"` 270 | Email *string `json:"email"` 271 | EventTime *string `json:"event_time"` 272 | Instructions *string `json:"instructions"` 273 | Licence *string `json:"licence"` 274 | MarketingConsent *string `json:"marketing_consent"` 275 | OrderID *string `json:"order_id"` 276 | ProductID *string `json:"product_id"` 277 | Quantity *string `json:"quantity"` 278 | Source *string `json:"source"` 279 | } 280 | 281 | // Fired when a dispute/chargeback is raised for a card transaction. 282 | // Paddle reference: https://developer.paddle.com/webhook-reference/risk-dispute-alerts/payment-dispute-created 283 | type PaymentDisputeCreatedAlert struct { 284 | AlertName *string `json:"alert_name"` 285 | AlertID *string `json:"alert_id"` 286 | Amount *string `json:"amount"` 287 | CheckoutID *string `json:"checkout_id"` 288 | Currency *string `json:"currency"` 289 | Email *string `json:"email"` 290 | EventTime *string `json:"event_time"` 291 | FeeUsd *string `json:"fee_usd"` 292 | MarketingConsent *string `json:"marketing_consent"` 293 | OrderID *string `json:"order_id"` 294 | Passthrough *string `json:"passthrough"` 295 | Status *string `json:"status"` 296 | } 297 | 298 | // Fired when a dispute/chargeback is closed for a card transaction. This indicates that the dispute/chargeback was contested and won by Paddle. 299 | // Paddle reference: https://developer.paddle.com/webhook-reference/risk-dispute-alerts/payment-dispute-closed 300 | type PaymentDisputeClosedAlert struct { 301 | AlertName *string `json:"alert_name"` 302 | AlertID *string `json:"alert_id"` 303 | Amount *string `json:"amount"` 304 | CheckoutID *string `json:"checkout_id"` 305 | Currency *string `json:"currency"` 306 | Email *string `json:"email"` 307 | EventTime *string `json:"event_time"` 308 | FeeUsd *string `json:"fee_usd"` 309 | MarketingConsent *string `json:"marketing_consent"` 310 | OrderID *string `json:"order_id"` 311 | Passthrough *string `json:"passthrough"` 312 | Status *string `json:"status"` 313 | } 314 | 315 | // Fired when a transaction is flagged as high risk. 316 | // Paddle reference: https://developer.paddle.com/webhook-reference/risk-dispute-alerts/high-risk-transaction-created 317 | type HighRiskTransactionCreatedAlert struct { 318 | AlertName *string `json:"alert_name"` 319 | AlertID *string `json:"alert_id"` 320 | CaseID *string `json:"case_id"` 321 | CheckoutID *string `json:"checkout_id"` 322 | CreatedAt *string `json:"created_at"` 323 | CustomerEmailAddress *string `json:"customer_email_address"` 324 | CustomerUserID *string `json:"customer_user_id"` 325 | EventTime *string `json:"event_time"` 326 | MarketingConsent *string `json:"marketing_consent"` 327 | Passthrough *string `json:"passthrough"` 328 | ProductID *string `json:"product_id"` 329 | RiskScore *string `json:"risk_score"` 330 | Status *string `json:"status"` 331 | } 332 | 333 | // Fired when a flagged transaction is approved or rejected. 334 | // Paddle reference: https://developer.paddle.com/webhook-reference/risk-dispute-alerts/high-risk-transaction-updated 335 | type HighRiskTransactionUpdatedAlert struct { 336 | AlertName *string `json:"alert_name"` 337 | AlertID *string `json:"alert_id"` 338 | CaseID *string `json:"case_id"` 339 | CheckoutID *string `json:"checkout_id"` 340 | CreatedAt *string `json:"created_at"` 341 | CustomerEmailAddress *string `json:"customer_email_address"` 342 | CustomerUserID *string `json:"customer_user_id"` 343 | EventTime *string `json:"event_time"` 344 | MarketingConsent *string `json:"marketing_consent"` 345 | OrderID *string `json:"order_id"` 346 | Passthrough *string `json:"passthrough"` 347 | ProductID *string `json:"product_id"` 348 | RiskScore *string `json:"risk_score"` 349 | } 350 | 351 | // Fired when a new transfer/payout is created for your account. 352 | // Paddle reference: https://developer.paddle.com/webhook-reference/payout-alerts/transfer-created 353 | type TransferCreatedAlert struct { 354 | AlertName *string `json:"alert_name"` 355 | AlertID *string `json:"alert_id"` 356 | Amount *string `json:"amount"` 357 | Currency *string `json:"currency"` 358 | EventTime *string `json:"event_time"` 359 | PayoutID *string `json:"payout_id"` 360 | Status *string `json:"status"` 361 | } 362 | 363 | // Fired when a new transfer/payout is marked as paid for your account. 364 | // Paddle reference: https://developer.paddle.com/webhook-reference/payout-alerts/transfer-paid 365 | type TransferPaidAlert struct { 366 | AlertName *string `json:"alert_name"` 367 | AlertID *string `json:"alert_id"` 368 | Amount *string `json:"amount"` 369 | Currency *string `json:"currency"` 370 | EventTime *string `json:"event_time"` 371 | PayoutID *string `json:"payout_id"` 372 | Status *string `json:"status"` 373 | } 374 | 375 | // Fired when a customer opts in to receive marketing communication from you. 376 | // Paddle reference: https://developer.paddle.com/webhook-reference/audience-alerts/new-audience-member 377 | type NewAudienceMemberAlert struct { 378 | AlertName *string `json:"alert_name"` 379 | AlertID *string `json:"alert_id"` 380 | CreatedAt *string `json:"created_at"` 381 | Email *string `json:"email"` 382 | EventTime *string `json:"event_time"` 383 | MarketingConsent *string `json:"marketing_consent"` 384 | Products *string `json:"products"` 385 | Source *string `json:"source"` 386 | Subscribed *string `json:"subscribed"` 387 | UserID *string `json:"user_id"` 388 | } 389 | 390 | // Fired when the information of an audience member is updated. 391 | // Paddle reference: https://developer.paddle.com/webhook-reference/audience-alerts/update-audience-member 392 | type UpdateAudienceMemberAlert struct { 393 | AlertName *string `json:"alert_name"` 394 | AlertID *string `json:"alert_id"` 395 | EventTime *string `json:"event_time"` 396 | NewCustomerEmail *string `json:"new_customer_email"` 397 | NewMarketingConsent *string `json:"new_marketing_consent"` 398 | OldCustomerEmail *string `json:"old_customer_email"` 399 | OldMarketingConsent *string `json:"old_marketing_consent"` 400 | Products *string `json:"products"` 401 | Source *string `json:"source"` 402 | UpdatedAt *string `json:"updated_at"` 403 | UserID *string `json:"user_id"` 404 | } 405 | 406 | // Fired when a manual invoice has been successfully paid by a customer. 407 | // Paddle reference: https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-paid 408 | type InvoicePaidAlert struct { 409 | AlertName *string `json:"alert_name"` 410 | AlertID *string `json:"alert_id"` 411 | PaymentID *string `json:"payment_id"` 412 | Amount *string `json:"amount"` 413 | SaleGross *string `json:"sale_gross"` 414 | TermDays *string `json:"term_days"` 415 | Status *string `json:"status"` 416 | PurchaseOrderNumber *string `json:"purchase_order_number"` 417 | InvoicedAt *string `json:"invoiced_at"` 418 | Currency *string `json:"currency"` 419 | ProductID *string `json:"product_id"` 420 | ProductName *string `json:"product_name"` 421 | ProductAdditionalInformation *string `json:"product_additional_information"` 422 | CustomerID *string `json:"customer_id"` 423 | CustomerName *string `json:"customer_name"` 424 | Email *string `json:"email"` 425 | CustomerVatNumber *string `json:"customer_vat_number"` 426 | CustomerCompanyNumber *string `json:"customer_company_number"` 427 | CustomerAddress *string `json:"customer_address"` 428 | CustomerCity *string `json:"customer_city"` 429 | CustomerState *string `json:"customer_state"` 430 | CustomerZipcode *string `json:"customer_zipcode"` 431 | Country *string `json:"country"` 432 | ContractID *string `json:"contract_id"` 433 | ContractStartDate *string `json:"contract_start_date"` 434 | ContractEndDate *string `json:"contract_end_date"` 435 | Passthrough *string `json:"passthrough"` 436 | DateCreated *string `json:"date_created"` 437 | BalanceCurrency *string `json:"balance_currency"` 438 | PaymentTax *string `json:"payment_tax"` 439 | PaymentMethod *string `json:"payment_method"` 440 | Fee *string `json:"fee"` 441 | Earnings *string `json:"earnings"` 442 | BalanceEarnings *string `json:"balance_earnings"` 443 | BalanceFee *string `json:"balance_fee"` 444 | BalanceTax *string `json:"balance_tax"` 445 | BalanceGross *string `json:"balance_gross"` 446 | DateReconciled *string `json:"date_reconciled"` 447 | EventTime *string `json:"event_time"` 448 | } 449 | 450 | // Fired when a manual invoice has been successfully sent to a customer. 451 | // Paddle reference: https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-sent 452 | type InvoiceSentAlert struct { 453 | AlertName *string `json:"alert_name"` 454 | AlertID *string `json:"alert_id"` 455 | PaymentID *string `json:"payment_id"` 456 | Amount *string `json:"amount"` 457 | SaleGross *string `json:"sale_gross"` 458 | TermDays *string `json:"term_days"` 459 | Status *string `json:"status"` 460 | PurchaseOrderNumber *string `json:"purchase_order_number"` 461 | InvoicedAt *string `json:"invoiced_at"` 462 | Currency *string `json:"currency"` 463 | ProductID *string `json:"product_id"` 464 | ProductName *string `json:"product_name"` 465 | ProductAdditionalInformation *string `json:"product_additional_information"` 466 | CustomerID *string `json:"customer_id"` 467 | CustomerName *string `json:"customer_name"` 468 | Email *string `json:"email"` 469 | CustomerVatNumber *string `json:"customer_vat_number"` 470 | CustomerCompanyNumber *string `json:"customer_company_number"` 471 | CustomerAddress *string `json:"customer_address"` 472 | CustomerCity *string `json:"customer_city"` 473 | CustomerState *string `json:"customer_state"` 474 | CustomerZipcode *string `json:"customer_zipcode"` 475 | Country *string `json:"country"` 476 | ContractID *string `json:"contract_id"` 477 | ContractStartDate *string `json:"contract_start_date"` 478 | ContractEndDate *string `json:"contract_end_date"` 479 | Passthrough *string `json:"passthrough"` 480 | DateCreated *string `json:"date_created"` 481 | BalanceCurrency *string `json:"balance_currency"` 482 | PaymentTax *string `json:"payment_tax"` 483 | PaymentMethod *string `json:"payment_method"` 484 | Fee *string `json:"fee"` 485 | Earnings *string `json:"earnings"` 486 | EventTime *string `json:"event_time"` 487 | } 488 | 489 | // Fired when a manual invoice has exceeded the payment term and is now overdue. 490 | // Paddle reference: https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-overdue 491 | type InvoiceOverdueAlert struct { 492 | AlertName *string `json:"alert_name"` 493 | AlertID *string `json:"alert_id"` 494 | PaymentID *string `json:"payment_id"` 495 | Amount *string `json:"amount"` 496 | SaleGross *string `json:"sale_gross"` 497 | TermDays *string `json:"term_days"` 498 | Status *string `json:"status"` 499 | PurchaseOrderNumber *string `json:"purchase_order_number"` 500 | InvoicedAt *string `json:"invoiced_at"` 501 | Currency *string `json:"currency"` 502 | ProductID *string `json:"product_id"` 503 | ProductName *string `json:"product_name"` 504 | ProductAdditionalInformation *string `json:"product_additional_information"` 505 | CustomerID *string `json:"customer_id"` 506 | CustomerName *string `json:"customer_name"` 507 | Email *string `json:"email"` 508 | CustomerVatNumber *string `json:"customer_vat_number"` 509 | CustomerCompanyNumber *string `json:"customer_company_number"` 510 | CustomerAddress *string `json:"customer_address"` 511 | CustomerCity *string `json:"customer_city"` 512 | CustomerState *string `json:"customer_state"` 513 | CustomerZipcode *string `json:"customer_zipcode"` 514 | Country *string `json:"country"` 515 | ContractID *string `json:"contract_id"` 516 | ContractStartDate *string `json:"contract_start_date"` 517 | ContractEndDate *string `json:"contract_end_date"` 518 | Passthrough *string `json:"passthrough"` 519 | DateCreated *string `json:"date_created"` 520 | BalanceCurrency *string `json:"balance_currency"` 521 | PaymentTax *string `json:"payment_tax"` 522 | PaymentMethod *string `json:"payment_method"` 523 | Fee *string `json:"fee"` 524 | Earnings *string `json:"earnings"` 525 | EventTime *string `json:"event_time"` 526 | } 527 | -------------------------------------------------------------------------------- /paddle/coupons.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // CouponsService handles communication with the coupons related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/coupons/listcoupons 12 | type CouponsService service 13 | 14 | // Coupon represents a Paddle plan. 15 | 16 | type Coupon struct { 17 | Coupon *string `json:"coupon,omitempty"` 18 | Description *string `json:"description,omitempty"` 19 | DiscountType *string `json:"discount_type,omitempty"` 20 | DiscountAmount *float64 `json:"discount_amount,omitempty"` 21 | DiscountCurrency *string `json:"discount_currency,omitempty"` 22 | AllowedUses *int `json:"allowed_uses,omitempty"` 23 | TimesUsed *int `json:"times_used,omitempty"` 24 | IsRecurring *bool `json:"is_recurring,omitempty"` 25 | Expires *string `json:"expires,omitempty"` 26 | } 27 | 28 | type CouponsResponse struct { 29 | Success bool `json:"success"` 30 | Response []*Coupon `json:"response"` 31 | } 32 | 33 | // CouponsOptions specifies the optional parameters to the 34 | // CouponsService.List method. 35 | type CouponsOptions struct { 36 | ProductID int `url:"product_id,omitempty"` 37 | } 38 | 39 | // List all coupons valid for a specified one-time product or subscription plan 40 | // 41 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/coupons/listcoupons 42 | func (s *CouponsService) List(ctx context.Context, productID int) ([]*Coupon, *http.Response, error) { 43 | u := "2.0/product/list_coupons" 44 | 45 | options := &CouponsOptions{ 46 | ProductID: productID, 47 | } 48 | 49 | req, err := s.client.NewRequest("POST", u, options) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | couponsResponse := new(CouponsResponse) 55 | response, err := s.client.Do(ctx, req, couponsResponse) 56 | if err != nil { 57 | return nil, response, err 58 | } 59 | 60 | return couponsResponse.Response, response, nil 61 | } 62 | 63 | type CouponCreate struct { 64 | CouponCode string `url:"coupon_code,omitempty"` 65 | CouponPrefix string `url:"coupon_prefix,omitempty"` 66 | NumCoupons int `url:"num_coupons,omitempty"` 67 | Description string `url:"description,omitempty"` 68 | CouponType string `url:"coupon_type,omitempty"` 69 | ProductIds string `url:"product_ids,omitempty"` 70 | DiscountType string `url:"discount_type,omitempty"` 71 | DiscountAmount float64 `url:"discount_amount,omitempty"` 72 | Currency string `url:"currency,omitempty"` 73 | AllowedUses int `url:"allowed_uses,omitempty"` 74 | Expires string `url:"expires,omitempty"` 75 | Recurring int `url:"recurring,omitempty"` 76 | Group string `url:"group,omitempty"` 77 | } 78 | 79 | type CouponCreateOptions struct { 80 | CouponCode string 81 | CouponPrefix string 82 | NumCoupons int 83 | Description string 84 | ProductIds string 85 | Currency string 86 | AllowedUses int 87 | Expires string 88 | Recurring int 89 | Group string 90 | } 91 | 92 | type CouponCreateResponse struct { 93 | Success bool `json:"success"` 94 | Response *CouponCodes `json:"response"` 95 | } 96 | 97 | type CouponCodes struct { 98 | CouponCode []string `json:"coupon_code,omitempty"` 99 | } 100 | 101 | // Create a new coupon for the given product or a checkout 102 | // 103 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/coupons/createcoupon 104 | func (s *CouponsService) Create(ctx context.Context, couponType, discountType string, discountAmount float64, options *CouponCreateOptions) (*CouponCodes, *http.Response, error) { 105 | u := "2.1/product/create_coupon" 106 | 107 | create := &CouponCreate{ 108 | CouponType: couponType, 109 | DiscountType: discountType, 110 | DiscountAmount: discountAmount, 111 | } 112 | if options != nil { 113 | create.CouponCode = options.CouponCode 114 | create.CouponPrefix = options.CouponPrefix 115 | create.NumCoupons = options.NumCoupons 116 | create.Description = options.Description 117 | create.ProductIds = options.ProductIds 118 | create.Currency = options.Currency 119 | create.AllowedUses = options.AllowedUses 120 | create.Expires = options.Expires 121 | create.Recurring = options.Recurring 122 | create.Group = options.Group 123 | } 124 | req, err := s.client.NewRequest("POST", u, create) 125 | if err != nil { 126 | return nil, nil, err 127 | } 128 | 129 | couponCreateResponse := new(CouponCreateResponse) 130 | response, err := s.client.Do(ctx, req, couponCreateResponse) 131 | if err != nil { 132 | return nil, response, err 133 | } 134 | 135 | return couponCreateResponse.Response, response, nil 136 | } 137 | 138 | type CouponDelete struct { 139 | CouponCode *string `url:"coupon_code,omitempty"` 140 | ProductID *int `url:"product_id,omitempty"` 141 | } 142 | 143 | type CouponDeleteOptions struct { 144 | ProductID int 145 | } 146 | 147 | type CouponDeleteResponse struct { 148 | Success bool `json:"success"` 149 | } 150 | 151 | // Delete a given coupon and prevent it from being further used 152 | // 153 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/coupons/deletecoupon 154 | func (s *CouponsService) Delete(ctx context.Context, couponCode string, options *CouponDeleteOptions) (bool, *http.Response, error) { 155 | u := "2.0/product/delete_coupon" 156 | 157 | delete := &CouponDelete{ 158 | CouponCode: String(couponCode), 159 | } 160 | if options != nil { 161 | delete.ProductID = Int(options.ProductID) 162 | } 163 | req, err := s.client.NewRequest("POST", u, delete) 164 | if err != nil { 165 | return false, nil, err 166 | } 167 | 168 | couponDeleteResponse := new(CouponDeleteResponse) 169 | response, err := s.client.Do(ctx, req, couponDeleteResponse) 170 | if err != nil { 171 | return false, response, err 172 | } 173 | 174 | return couponDeleteResponse.Success, response, nil 175 | } 176 | 177 | type CouponUpdateOptions struct { 178 | CouponCode string `url:"coupon_code,omitempty"` 179 | Group string `url:"group,omitempty"` 180 | NewCouponCode string `url:"new_coupon_code,omitempty"` 181 | NewGroup string `url:"new_group,omitempty"` 182 | ProductIds string `url:"product_ids,omitempty"` 183 | Expires string `url:"expires,omitempty"` 184 | AllowedUses int `url:"allowed_uses,omitempty"` 185 | Currency string `url:"currency,omitempty"` 186 | DiscountAmount float64 `url:"discount_amount,omitempty"` 187 | Recurring int `url:"recurring,omitempty"` 188 | } 189 | 190 | type CouponUpdateResponse struct { 191 | Success bool `json:"success"` 192 | Response *struct { 193 | Updated *int `json:"updated"` 194 | } `json:"response"` 195 | } 196 | 197 | // Update an existing coupon in your account 198 | // 199 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/coupons/updatecoupon 200 | func (s *CouponsService) Update(ctx context.Context, options *CouponUpdateOptions) (*int, *http.Response, error) { 201 | u := "2.1/product/update_coupon" 202 | req, err := s.client.NewRequest("POST", u, options) 203 | if err != nil { 204 | return nil, nil, err 205 | } 206 | 207 | couponUpdateResponse := new(CouponUpdateResponse) 208 | response, err := s.client.Do(ctx, req, couponUpdateResponse) 209 | if err != nil { 210 | return nil, response, err 211 | } 212 | 213 | return couponUpdateResponse.Response.Updated, response, nil 214 | } 215 | -------------------------------------------------------------------------------- /paddle/coupons_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestCouponsService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/product/list_coupons", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"product_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": [{"coupon":"1"}]}`) 19 | }) 20 | 21 | coupons, _, err := client.Coupons.List(context.Background(), 1) 22 | if err != nil { 23 | t.Errorf("Coupons.List returned error: %v", err) 24 | } 25 | 26 | want := []*Coupon{{Coupon: String("1")}} 27 | if !reflect.DeepEqual(coupons, want) { 28 | t.Errorf("Coupons.List returned %+v, want %+v", coupons, want) 29 | } 30 | } 31 | 32 | func TestCouponsService_Create(t *testing.T) { 33 | client, mux, _, teardown := setup() 34 | defer teardown() 35 | 36 | mux.HandleFunc("/2.1/product/create_coupon", func(w http.ResponseWriter, r *http.Request) { 37 | testMethod(t, r, "POST") 38 | testFormValues(t, r, values{"coupon_type": "a", "discount_type": "a", "discount_amount": "10", "coupon_code": "1"}) 39 | fmt.Fprint(w, `{"success":true, "response": {"coupon_code": ["1"]}}`) 40 | }) 41 | 42 | options := &CouponCreateOptions{CouponCode: "1"} 43 | codes, _, err := client.Coupons.Create(context.Background(), "a", "a", 10, options) 44 | if err != nil { 45 | t.Errorf("Coupons.Create returned error: %v", err) 46 | } 47 | 48 | want := &CouponCodes{CouponCode: []string{"1"}} 49 | if !reflect.DeepEqual(codes, want) { 50 | t.Errorf("Coupons.Create returned %+v, want %+v", codes, want) 51 | } 52 | } 53 | 54 | func TestCouponsService_Delete(t *testing.T) { 55 | client, mux, _, teardown := setup() 56 | defer teardown() 57 | 58 | mux.HandleFunc("/2.0/product/delete_coupon", func(w http.ResponseWriter, r *http.Request) { 59 | testMethod(t, r, "POST") 60 | testFormValues(t, r, values{"coupon_code": "a", "product_id": "1"}) 61 | fmt.Fprint(w, `{"success":true}`) 62 | }) 63 | 64 | options := &CouponDeleteOptions{ProductID: 1} 65 | resp, _, err := client.Coupons.Delete(context.Background(), "a", options) 66 | if err != nil { 67 | t.Errorf("Coupons.Delete returned error: %v", err) 68 | } 69 | 70 | want := true 71 | if !reflect.DeepEqual(resp, want) { 72 | t.Errorf("Coupons.Delete returned %+v, want %+v", resp, want) 73 | } 74 | } 75 | 76 | func TestCouponsService_Update(t *testing.T) { 77 | client, mux, _, teardown := setup() 78 | defer teardown() 79 | 80 | mux.HandleFunc("/2.1/product/update_coupon", func(w http.ResponseWriter, r *http.Request) { 81 | testMethod(t, r, "POST") 82 | testFormValues(t, r, values{"coupon_code": "a"}) 83 | fmt.Fprint(w, `{"success":true, "response": {"updated": 1}}`) 84 | }) 85 | 86 | options := &CouponUpdateOptions{CouponCode: "a"} 87 | update, _, err := client.Coupons.Update(context.Background(), options) 88 | if err != nil { 89 | t.Errorf("Coupons.Update returned error: %v", err) 90 | } 91 | 92 | want := 1 93 | if !reflect.DeepEqual(*update, want) { 94 | t.Errorf("Coupons.Update returned %+v, want %+v", *update, want) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /paddle/messages.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "crypto/sha1" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/json" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "net/url" 15 | "sort" 16 | ) 17 | 18 | // ValidatePayload validates an incoming Paddle Webhook event request 19 | // and returns the (map[string]string) payload. 20 | // The Content-Type header of the payload needs to be "application/x-www-form-urlencoded". 21 | // If the Content-Type is different then an error is returned. 22 | // publicKey is the Paddle public key. 23 | // 24 | // Example usage: 25 | // 26 | // func PaddleWebhookHandler(w http.ResponseWriter, r *http.Request) { 27 | // payload, err := paddle.ValidatePayload(r, []byte(config.PaddleWebHookPublicKey)) 28 | // if err != nil { ... } 29 | // // Process payload... 30 | // } 31 | func ValidatePayload(r *http.Request, publicKey []byte) (map[string]string, error) { 32 | payload := map[string]string{} 33 | 34 | ct := r.Header.Get("Content-Type") 35 | if ct != "application/x-www-form-urlencoded" { 36 | return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct) 37 | } 38 | 39 | if err := r.ParseForm(); err != nil { 40 | return nil, err 41 | } 42 | 43 | // Get the p_signature parameter. 44 | p_signature := r.Form.Get("p_signature") 45 | 46 | // Remove the p_signature parameter from Form 47 | r.Form.Del("p_signature") 48 | 49 | // Verify signature to make sure the request was sent by Paddle 50 | if err := validateSignature(r.Form, p_signature, publicKey); err != nil { 51 | return nil, err 52 | } 53 | 54 | // Construct payload from fields sent in the request 55 | for k := range r.Form { 56 | payload[k] = r.Form.Get(k) // r.Form is a map[string][]string 57 | } 58 | 59 | return payload, nil 60 | } 61 | 62 | // validateSignature validates the signature for the given payload. 63 | // The signature is included on each webhook with the attribute p_signature. 64 | // payload is the Form payload sent by Paddle Webhooks. 65 | // publicKey is the Paddle public key. 66 | // 67 | // Paddle Reference: https://paddle.com/docs/reference-verifying-webhooks/ 68 | func validateSignature(form url.Values, p_signature string, publicKey []byte) error { 69 | // Find PEM public key block. 70 | der, _ := pem.Decode(publicKey) 71 | if der == nil { 72 | return errors.New("Could not parse public key pem") 73 | } 74 | 75 | // Parse public key in PKIX, ASN.1 DER form. 76 | pub, err := x509.ParsePKIXPublicKey(der.Bytes) 77 | if err != nil { 78 | return errors.New("Could not parse public key pem der") 79 | } 80 | 81 | signingKey, ok := pub.(*rsa.PublicKey) 82 | if !ok { 83 | return errors.New("Not the correct key format") 84 | } 85 | 86 | // base64 decode p_signature 87 | sig, err := base64.StdEncoding.DecodeString(p_signature) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // ksort() and serialize the Form 93 | sha1Sum := sha1.Sum(phpserialize(form)) 94 | 95 | err = rsa.VerifyPKCS1v15(signingKey, crypto.SHA1, sha1Sum[:], sig) 96 | if err != nil { 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | // php serialize Form in sorted order 103 | func phpserialize(form url.Values) []byte { 104 | var keys []string 105 | for k := range form { 106 | keys = append(keys, k) 107 | } 108 | sort.Strings(keys) 109 | 110 | serialized := fmt.Sprintf("a:%d:{", len(keys)) 111 | for _, k := range keys { 112 | serialized += fmt.Sprintf("s:%d:\"%s\";s:%d:\"%s\";", len(k), k, len(form.Get(k)), form.Get(k)) 113 | } 114 | serialized += "}" 115 | 116 | return []byte(serialized) 117 | } 118 | 119 | // ParsePayload parses the alert payload. For recognized alert types, a 120 | // value of the corresponding struct type will be returned. 121 | // An error will be returned for unrecognized alert types. 122 | // 123 | // Example usage: 124 | // 125 | // func PaddleWebhookHandler(w http.ResponseWriter, r *http.Request) { 126 | // payload, err := paddle.ValidatePayload(r, s.webhookSecretKey) 127 | // if err != nil { ... } 128 | // alert, err := paddle.ParsePayload(payload) 129 | // if err != nil { ... } 130 | // switch alert := alert.(type) { 131 | // case *paddle.SubscriptionCreatedAlert: 132 | // processSubscriptionCreatedAlert(alert) 133 | // case *paddle.SubscriptionCanceledAlert: 134 | // processSubscriptionCanceledAlert(alert) 135 | // ... 136 | // } 137 | // } 138 | // 139 | func ParsePayload(payload map[string]string) (interface{}, error) { 140 | var parsedPayload interface{} 141 | alert_type := payload["alert_name"] 142 | 143 | switch alert_type { 144 | case "subscription_created": 145 | parsedPayload = &SubscriptionCreatedAlert{} 146 | case "subscription_updated": 147 | parsedPayload = &SubscriptionUpdatedAlert{} 148 | case "subscription_cancelled": 149 | parsedPayload = &SubscriptionCancelledAlert{} 150 | case "subscription_payment_succeeded": 151 | parsedPayload = &SubscriptionPaymentSucceededAlert{} 152 | case "subscription_payment_failed": 153 | parsedPayload = &SubscriptionPaymentFailedAlert{} 154 | case "subscription_payment_refunded": 155 | parsedPayload = &SubscriptionPaymentRefundedAlert{} 156 | case "payment_succeeded": 157 | parsedPayload = &PaymentSucceededAlert{} 158 | case "payment_refunded": 159 | parsedPayload = &PaymentRefundedAlert{} 160 | case "locker_processed": 161 | parsedPayload = &LockerProcessedAlert{} 162 | case "payment_dispute_created": 163 | parsedPayload = &PaymentDisputeCreatedAlert{} 164 | case "payment_dispute_closed": 165 | parsedPayload = &PaymentDisputeClosedAlert{} 166 | case "high_risk_transaction_created": 167 | parsedPayload = &HighRiskTransactionCreatedAlert{} 168 | case "high_risk_transaction_updated": 169 | parsedPayload = &HighRiskTransactionUpdatedAlert{} 170 | case "transfer_created": 171 | parsedPayload = &TransferCreatedAlert{} 172 | case "transfer_paid": 173 | parsedPayload = &TransferPaidAlert{} 174 | case "new_audience_member": 175 | parsedPayload = &NewAudienceMemberAlert{} 176 | case "update_audience_member": 177 | parsedPayload = &UpdateAudienceMemberAlert{} 178 | case "invoice_paid": 179 | parsedPayload = &InvoicePaidAlert{} 180 | case "invoice_sent": 181 | parsedPayload = &InvoiceSentAlert{} 182 | case "invoice_overdue": 183 | parsedPayload = &InvoiceOverdueAlert{} 184 | default: 185 | return nil, fmt.Errorf("unknown alert_type: %v", alert_type) 186 | } 187 | 188 | // Marshal payload and unmarshal it 189 | j, err := json.Marshal(payload) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | if err := json.Unmarshal(j, &parsedPayload); err != nil { 195 | return nil, err 196 | } 197 | 198 | return parsedPayload, nil 199 | } 200 | -------------------------------------------------------------------------------- /paddle/modifiers.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // ModifiersService handles communication with the modifers related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/modifiers/ 12 | type ModifiersService service 13 | 14 | // Modifier represents a Paddle modifier. 15 | type Modifier struct { 16 | ModifierID *int `json:"modifier_id,omitempty"` 17 | SubscriptionID *int `json:"subscription_id,omitempty"` 18 | Amount *string `json:"amount,omitempty"` 19 | Currency *string `json:"currency,omitempty"` 20 | IsRecurring *bool `json:"is_recurring,omitempty"` 21 | Description *string `json:"description,omitempty"` 22 | } 23 | 24 | type ModifiersResponse struct { 25 | Success bool `json:"success"` 26 | Response []*Modifier `json:"response"` 27 | } 28 | 29 | // ModifiersOptions specifies the optional parameters to the 30 | // ModifiersService.List method. 31 | type ModifiersOptions struct { 32 | // SubscriptionID filters modifiers based on the subscription id. 33 | SubscriptionID int `url:"subscription_id,omitempty"` 34 | // PlanID filters modifiers based on the plan id. 35 | PlanID int `url:"plan_id,omitempty"` 36 | } 37 | 38 | // List all subscription modifiers 39 | // 40 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/modifiers/listmodifiers 41 | func (s *ModifiersService) List(ctx context.Context, options *ModifiersOptions) ([]*Modifier, *http.Response, error) { 42 | u := "2.0/subscription/modifiers" 43 | req, err := s.client.NewRequest("POST", u, options) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | 48 | modifiersResponse := new(ModifiersResponse) 49 | response, err := s.client.Do(ctx, req, modifiersResponse) 50 | if err != nil { 51 | return nil, response, err 52 | } 53 | 54 | return modifiersResponse.Response, response, nil 55 | } 56 | 57 | type ModifierCreate struct { 58 | SubscriptionID int `url:"subscription_id,omitempty"` 59 | ModifierAmount float64 `url:"modifier_amount,omitempty"` 60 | ModifierRecurring bool `url:"modifier_recurring,omitempty"` 61 | ModifierDescription string `url:"modifier_description,omitempty"` 62 | } 63 | 64 | type ModifierCreateOptions struct { 65 | ModifierRecurring bool 66 | ModifierDescription string 67 | } 68 | 69 | type ModifierCreateResponse struct { 70 | Success bool `json:"success"` 71 | Response *Modifier `json:"response"` 72 | } 73 | 74 | // Create a subscription modifier to dynamically change the subscription payment amount 75 | // 76 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/modifiers/createmodifier 77 | func (s *ModifiersService) Create(ctx context.Context, subscriptionID int, modifierAmount float64, options *ModifierCreateOptions) (*Modifier, *http.Response, error) { 78 | u := "2.0/subscription/modifiers/create" 79 | 80 | create := &ModifierCreate{ 81 | SubscriptionID: subscriptionID, 82 | ModifierAmount: modifierAmount, 83 | } 84 | if options != nil { 85 | create.ModifierRecurring = options.ModifierRecurring 86 | create.ModifierDescription = options.ModifierDescription 87 | } 88 | req, err := s.client.NewRequest("POST", u, create) 89 | if err != nil { 90 | return nil, nil, err 91 | } 92 | 93 | modifierCreateResponse := new(ModifierCreateResponse) 94 | response, err := s.client.Do(ctx, req, modifierCreateResponse) 95 | if err != nil { 96 | return nil, response, err 97 | } 98 | 99 | return modifierCreateResponse.Response, response, nil 100 | } 101 | 102 | type ModifierDelete struct { 103 | ModifierID int `url:"modifier_id,omitempty"` 104 | } 105 | 106 | type ModifierDeleteResponse struct { 107 | Success bool `json:"success"` 108 | } 109 | 110 | // Delete an existing subscription modifier 111 | // 112 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/modifiers/deletemodifier 113 | func (s *ModifiersService) Delete(ctx context.Context, modifierID int) (bool, *http.Response, error) { 114 | u := "2.0/subscription/modifiers/delete" 115 | 116 | delete := &ModifierDelete{ 117 | ModifierID: modifierID, 118 | } 119 | req, err := s.client.NewRequest("POST", u, delete) 120 | if err != nil { 121 | return false, nil, err 122 | } 123 | 124 | modifierDeleteResponse := new(ModifierDeleteResponse) 125 | response, err := s.client.Do(ctx, req, modifierDeleteResponse) 126 | if err != nil { 127 | return false, response, err 128 | } 129 | 130 | return modifierDeleteResponse.Success, response, nil 131 | } 132 | -------------------------------------------------------------------------------- /paddle/modifiers_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestModifiersService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/subscription/modifiers", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"plan_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": [{"modifier_id":1}]}`) 19 | }) 20 | 21 | opt := &ModifiersOptions{PlanID: 1} 22 | modifiers, _, err := client.Modifiers.List(context.Background(), opt) 23 | if err != nil { 24 | t.Errorf("Modifiers.List returned error: %v", err) 25 | } 26 | 27 | want := []*Modifier{{ModifierID: Int(1)}} 28 | if !reflect.DeepEqual(modifiers, want) { 29 | t.Errorf("Modifiers.List returned %+v, want %+v", modifiers, want) 30 | } 31 | } 32 | 33 | func TestModifiersService_Create(t *testing.T) { 34 | client, mux, _, teardown := setup() 35 | defer teardown() 36 | 37 | mux.HandleFunc("/2.0/subscription/modifiers/create", func(w http.ResponseWriter, r *http.Request) { 38 | testMethod(t, r, "POST") 39 | testFormValues(t, r, values{"subscription_id": "1", "modifier_amount": "1", "modifier_recurring": "true"}) 40 | fmt.Fprint(w, `{"success":true, "response": {"subscription_id": 1, "modifier_id":1}}`) 41 | }) 42 | 43 | opt := &ModifierCreateOptions{ModifierRecurring: true} 44 | modifier, _, err := client.Modifiers.Create(context.Background(), 1, 1, opt) 45 | if err != nil { 46 | t.Errorf("Modifiers.Create returned error: %v", err) 47 | } 48 | 49 | want := &Modifier{SubscriptionID: Int(1), ModifierID: Int(1)} 50 | if !reflect.DeepEqual(modifier, want) { 51 | t.Errorf("Modifiers.Create returned %+v, want %+v", modifier, want) 52 | } 53 | } 54 | 55 | func TestModifiersService_Delete(t *testing.T) { 56 | client, mux, _, teardown := setup() 57 | defer teardown() 58 | 59 | mux.HandleFunc("/2.0/subscription/modifiers/delete", func(w http.ResponseWriter, r *http.Request) { 60 | testMethod(t, r, "POST") 61 | testFormValues(t, r, values{"modifier_id": "1"}) 62 | fmt.Fprint(w, `{"success":true}`) 63 | }) 64 | 65 | resp, _, err := client.Modifiers.Delete(context.Background(), 1) 66 | if err != nil { 67 | t.Errorf("Modifiers.Delete returned error: %v", err) 68 | } 69 | 70 | want := true 71 | if !reflect.DeepEqual(resp, want) { 72 | t.Errorf("Modifiers.Delete returned %+v, want %+v", resp, want) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /paddle/one_off_charges.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // OneOffChargesService handles communication with the one-off charges related 10 | // methods of the Paddle API. 11 | // 12 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/one-off-charges/ 13 | type OneOffChargesService service 14 | 15 | // OneOffCharge represents a Paddle one-off charge. 16 | type OneOffCharge struct { 17 | InvoiceID *int `json:"invoice_id,omitempty"` 18 | SubscriptionID *int `json:"subscription_id,omitempty"` 19 | Amount *float64 `json:"amount,omitempty"` 20 | Currency *string `json:"currency,omitempty"` 21 | PaymentDate *string `json:"payment_date,omitempty"` 22 | ReceiptUrl *string `json:"receipt_url,omitempty"` 23 | OrderID *string `json:"order_id,omitempty"` 24 | Status *string `json:"status,omitempty"` 25 | } 26 | 27 | type OneOffChargeCreate struct { 28 | Amount float64 `url:"amount,omitempty"` 29 | ChargeName string `url:"charge_name,omitempty"` 30 | } 31 | 32 | type OneOffChargeResponse struct { 33 | Success bool `json:"success"` 34 | Response *OneOffCharge `json:"response"` 35 | } 36 | 37 | // Make an immediate one-off charge on top of an existing user subscription 38 | // 39 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/one-off-charges/createcharge 40 | func (s *OneOffChargesService) Create(ctx context.Context, subscriptionID int, amount float64, chargeName string) (*OneOffCharge, *http.Response, error) { 41 | u := fmt.Sprintf("2.0/subscription/%d/charge", subscriptionID) 42 | 43 | OneOffChargeCreate := &OneOffChargeCreate{ 44 | Amount: amount, 45 | ChargeName: chargeName, 46 | } 47 | 48 | req, err := s.client.NewRequest("POST", u, OneOffChargeCreate) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | oneOffChargeResponse := new(OneOffChargeResponse) 54 | response, err := s.client.Do(ctx, req, oneOffChargeResponse) 55 | if err != nil { 56 | return nil, response, err 57 | } 58 | 59 | return oneOffChargeResponse.Response, response, nil 60 | } 61 | -------------------------------------------------------------------------------- /paddle/one_off_charges_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestOneOffChargesService_Create(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/subscription/1/charge", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"amount": "10", "charge_name": "xyz"}) 18 | fmt.Fprint(w, `{"success":true, "response": {"invoice_id":1, "subscription_id":1}}`) 19 | }) 20 | 21 | oneOffCharge, _, err := client.OneOffCharges.Create(context.Background(), 1, 10, "xyz") 22 | if err != nil { 23 | t.Errorf("OneOffCharges.Create returned error: %v", err) 24 | } 25 | 26 | want := &OneOffCharge{InvoiceID: Int(1), SubscriptionID: Int(1)} 27 | if !reflect.DeepEqual(oneOffCharge, want) { 28 | t.Errorf("OneOffCharges.Create returned %+v, want %+v", oneOffCharge, want) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /paddle/order_details.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // OrderDetailsService handles communication with the order_details related 10 | // methods of the Paddle API. 11 | // 12 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/order-details/ 13 | type OrderDetailsService service 14 | 15 | // OrderDetails represents a Paddle order details. 16 | type OrderDetails struct { 17 | State *string `json:"state,omitempty"` 18 | Checkout *Checkout `json:"checkout,omitempty"` 19 | Order *Order `json:"order,omitempty"` 20 | Lockers []*Locker `json:"lockers,omitempty"` 21 | } 22 | 23 | type Checkout struct { 24 | CheckoutID *string `json:"checkout_id,omitempty"` 25 | ImageURL *string `json:"image_url,omitempty"` 26 | Title *string `json:"title,omitempty"` 27 | } 28 | 29 | type Order struct { 30 | OrderID *int `json:"order_id,omitempty"` 31 | Total *string `json:"total,omitempty"` 32 | TotalTax *string `json:"total_tax,omitempty"` 33 | Currency *string `json:"currency,omitempty"` 34 | FormattedTotal *string `json:"formatted_total,omitempty"` 35 | FormattedTax *string `json:"formatted_tax,omitempty"` 36 | CouponCode *string `json:"coupon_code,omitempty"` 37 | ReceiptUrl *string `json:"receipt_url,omitempty"` 38 | CustomerSuccessRedirectURL *string `json:"customer_success_redirect_url,omitempty"` 39 | HasLocker *bool `json:"rhas_locker,omitempty"` 40 | IsSubscription *bool `json:"is_subscription,omitempty"` 41 | ProductID *int `json:"product_id,omitempty"` 42 | SubscriptionID *int `json:"subscription_id,omitempty"` 43 | SubscriptionOrderID *string `json:"subscription_order_id,omitempty"` 44 | Quantity *int `json:"quantity,omitempty"` 45 | Completed *OrderCompleted `json:"completed,omitempty"` 46 | Customer *Customer `json:"customer,omitempty"` 47 | } 48 | 49 | type OrderCompleted struct { 50 | Date *string `json:"date,omitempty"` 51 | TimeZone *string `json:"time_zone,omitempty"` 52 | TimeZoneType *int `json:"time_zone_type,omitempty"` 53 | } 54 | 55 | type Customer struct { 56 | Email *string `json:"email,omitempty"` 57 | MarketingConsent *bool `json:"marketing_consent,omitempty"` 58 | } 59 | 60 | type Locker struct { 61 | LockerID *int `json:"locker_id,omitempty"` 62 | ProductID *int `json:"product_id,omitempty"` 63 | ProductName *string `json:"product_name,omitempty"` 64 | LicenseCode *string `json:"license_code,omitempty"` 65 | Instructions *string `json:"instructions,omitempty"` 66 | Download *string `json:"download,omitempty"` 67 | } 68 | 69 | type OrderDetailsResponse struct { 70 | Success bool `json:"success"` 71 | Response *OrderDetails `json:"response"` 72 | } 73 | 74 | // Get information about an order after a transaction completes 75 | // 76 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/order-details/getorder 77 | func (s *OrderDetailsService) Get(ctx context.Context, checkoutID string) (*OrderDetails, *http.Response, error) { 78 | u := fmt.Sprintf("1.0/order?checkout_id=%v", checkoutID) 79 | req, err := s.client.NewRequest("GET", u, nil) 80 | if err != nil { 81 | return nil, nil, err 82 | } 83 | 84 | orderResponse := new(OrderDetailsResponse) 85 | response, err := s.client.Do(ctx, req, orderResponse) 86 | if err != nil { 87 | return nil, response, err 88 | } 89 | 90 | return orderResponse.Response, response, nil 91 | } 92 | -------------------------------------------------------------------------------- /paddle/order_details_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestOrderDetailsService_Get(t *testing.T) { 12 | client, mux, _, teardown := checkoutSetup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/1.0/order", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "GET") 17 | testSoftFormValues(t, r, values{"checkout_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": { 19 | "state": "xyz", 20 | "checkout":{"checkout_id": "1"}, 21 | "order": {"order_id": 1, "completed": {"date": "123"}, "customer": {"email": "abc"}}, 22 | "Lockers": [{"locker_id": 1}]}}`) 23 | }) 24 | 25 | order, _, err := client.OrderDetails.Get(context.Background(), "1") 26 | if err != nil { 27 | t.Errorf("OrderDetails.Get returned error: %v", err) 28 | } 29 | 30 | want := &OrderDetails{ 31 | State: String("xyz"), 32 | Checkout: &Checkout{CheckoutID: String("1")}, 33 | Order: &Order{ 34 | OrderID: Int(1), 35 | Completed: &OrderCompleted{Date: String("123")}, 36 | Customer: &Customer{Email: String("abc")}, 37 | }, 38 | Lockers: []*Locker{{LockerID: Int(1)}}, 39 | } 40 | 41 | if !reflect.DeepEqual(order, want) { 42 | t.Errorf("OrderDetails.Get returned %+v, want %+v", order, want) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /paddle/paddle.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | 13 | "github.com/google/go-querystring/query" 14 | ) 15 | 16 | const ( 17 | defaultBaseURL = "https://vendors.paddle.com/api/" 18 | sandboxBaseURL = "https://sandbox-vendors.paddle.com/api/" 19 | 20 | checkoutBaseURL = "https://checkout.paddle.com/api/" 21 | sandboxCheckoutBaseURL = "https://sandbox-checkout.paddle.com/api/" 22 | 23 | vendorIdAttribute = "vendor_id" 24 | vendorAuthCodeAttribute = "vendor_auth_code" 25 | ) 26 | 27 | // A Client manages communication with the Paddle API. 28 | type Client struct { 29 | client *http.Client // HTTP client used to communicate with the API. 30 | 31 | // The vendor ID identifies your seller account. This can be found in Developer Tools > Authentication. 32 | VendorID *string 33 | 34 | // The vendor auth code is a private API key for authenticating API requests. 35 | // This key should never be used in client side code or shared publicly. This can be found in Developer Tools > Authentication. 36 | VendorAuthCode *string 37 | 38 | // Base URL for API requests. BaseURL should always be specified with a trailing slash. 39 | BaseURL *url.URL 40 | 41 | common service // Reuse a single struct instead of allocating one for each service on the heap. 42 | 43 | // Services used for talking to different parts of the Paddle API. 44 | Users *UsersService 45 | Plans *PlansService 46 | Modifiers *ModifiersService 47 | Payments *PaymentsService 48 | OneOffCharges *OneOffChargesService 49 | Webhooks *WebhooksService 50 | OrderDetails *OrderDetailsService 51 | UserHistory *UserHistoryService 52 | Prices *PricesService 53 | Coupons *CouponsService 54 | Products *ProductsService 55 | RefundPayment *RefundPaymentService 56 | PayLink *PayLinkService 57 | } 58 | 59 | type service struct { 60 | client *Client 61 | } 62 | 63 | // ListOptions specifies the optional parameters to various List methods that 64 | // support pagination. 65 | type ListOptions struct { 66 | // For paginated result sets, page of results to retrieve. (minimum: 1) 67 | Page int `url:"page,omitempty"` 68 | 69 | // Number of subscription records to return per page. (minimum: 1, maximum: 200) 70 | ResultsPerPage int `url:"results_per_page,omitempty"` 71 | } 72 | 73 | // NewClient returns a new Paddle API client. It requires a vendor_id 74 | // and a vendor_auth_code arguments. If a nil httpClient is 75 | // provided, http.DefaultClient will be used. 76 | func NewClient(vendorID, vendorAuthCode string, httpClient *http.Client) *Client { 77 | baseURL, _ := url.Parse(defaultBaseURL) 78 | return getClient(httpClient, baseURL, vendorID, vendorAuthCode) 79 | } 80 | 81 | // NewSandboxClient returns a new Paddle API client for the sandbox environment. 82 | // It requires a vendor_id and a vendor_auth_code arguments. If a nil httpClient is 83 | // provided, http.DefaultClient will be used. 84 | func NewSandboxClient(vendorID, vendorAuthCode string, httpClient *http.Client) *Client { 85 | baseURL, _ := url.Parse(sandboxBaseURL) 86 | return getClient(httpClient, baseURL, vendorID, vendorAuthCode) 87 | } 88 | 89 | // getCLient creates and returns a Paddle API client. 90 | func getClient(httpClient *http.Client, baseURL *url.URL, vendorID, vendorAuthCode string) *Client { 91 | if httpClient == nil { 92 | httpClient = http.DefaultClient 93 | } 94 | c := &Client{ 95 | client: httpClient, 96 | BaseURL: baseURL, 97 | VendorID: String(vendorID), 98 | VendorAuthCode: String(vendorAuthCode), 99 | } 100 | 101 | c.common.client = c 102 | c.Users = (*UsersService)(&c.common) 103 | c.Plans = (*PlansService)(&c.common) 104 | c.Modifiers = (*ModifiersService)(&c.common) 105 | c.Payments = (*PaymentsService)(&c.common) 106 | c.OneOffCharges = (*OneOffChargesService)(&c.common) 107 | c.Webhooks = (*WebhooksService)(&c.common) 108 | c.Coupons = (*CouponsService)(&c.common) 109 | c.Products = (*ProductsService)(&c.common) 110 | c.RefundPayment = (*RefundPaymentService)(&c.common) 111 | c.PayLink = (*PayLinkService)(&c.common) 112 | return c 113 | } 114 | 115 | // NewCheckoutClient returns a new Paddle API client for checkouts. 116 | // If a nil httpClient is provided, http.DefaultClient will be used. 117 | func NewCheckoutClient(httpClient *http.Client) *Client { 118 | baseURL, _ := url.Parse(checkoutBaseURL) 119 | return getCheckoutClient(httpClient, baseURL) 120 | } 121 | 122 | // NewSandboxCheckoutClient returns a new Paddle API client for the sandbox checkout enivronement. 123 | // If a nil httpClient is provided, http.DefaultClient will be used. 124 | func NewSandboxCheckoutClient(httpClient *http.Client) *Client { 125 | baseURL, _ := url.Parse(sandboxCheckoutBaseURL) 126 | return getCheckoutClient(httpClient, baseURL) 127 | } 128 | 129 | // getCheckoutCLient creates and returns a checkout Paddle API client. 130 | func getCheckoutClient(httpClient *http.Client, baseURL *url.URL) *Client { 131 | if httpClient == nil { 132 | httpClient = http.DefaultClient 133 | } 134 | c := &Client{ 135 | client: httpClient, 136 | BaseURL: baseURL, 137 | } 138 | 139 | c.common.client = c 140 | c.OrderDetails = (*OrderDetailsService)(&c.common) 141 | c.UserHistory = (*UserHistoryService)(&c.common) 142 | c.Prices = (*PricesService)(&c.common) 143 | return c 144 | } 145 | 146 | // NewRequest creates an API request. A relative URL can be provided in urlStr, 147 | // in which case it is resolved relative to the BaseURL of the Client. 148 | // Relative URLs should always be specified without a preceding slash. If 149 | // specified, the value pointed to by body is url form encoded and included as the 150 | // request body. 151 | func (c *Client) NewRequest(method, urlStr string, options interface{}) (*http.Request, error) { 152 | if !strings.HasSuffix(c.BaseURL.Path, "/") { 153 | return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 154 | } 155 | u, err := c.BaseURL.Parse(urlStr) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | payload, err := newPayload(c.VendorID, c.VendorAuthCode, options) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | req, err := http.NewRequest(method, u.String(), payload) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | if payload.Size() > 0 { 171 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 172 | } 173 | 174 | return req, nil 175 | } 176 | 177 | // newPayload encodes opt into ``URL encoded'' form and return a *strings.Reader. opt 178 | // must be a struct whose fields may contain "url" tags. 179 | // Client's VendorID and VendorAuthCode will be attached to the payload. 180 | func newPayload(vendorID, vendorAuthCode *string, opt interface{}) (*strings.Reader, error) { 181 | data, err := query.Values(opt) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | if vendorID != nil && vendorAuthCode != nil { 187 | data.Set(vendorIdAttribute, *vendorID) 188 | data.Set(vendorAuthCodeAttribute, *vendorAuthCode) 189 | } 190 | 191 | payload := strings.NewReader(data.Encode()) 192 | return payload, nil 193 | } 194 | 195 | // Do sends an API request and returns the API response. The API response is 196 | // JSON decoded and stored in the value pointed to by v, or returned as an 197 | // error if an API error has occurred. If v implements the io.Writer 198 | // interface, the raw response body will be written to v, without attempting to 199 | // first decode it. 200 | // 201 | // The provided ctx must be non-nil. If it is canceled or times out, 202 | // ctx.Err() will be returned. 203 | func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 204 | if ctx == nil { 205 | return nil, errors.New("context must be non-nil") 206 | } 207 | req = req.WithContext(ctx) 208 | 209 | resp, err := c.client.Do(req) 210 | if err != nil { 211 | // If we got an error, and the context has been canceled, 212 | // the context's error is probably more useful. 213 | select { 214 | case <-ctx.Done(): 215 | return nil, ctx.Err() 216 | default: 217 | } 218 | 219 | // If the error type is *url.Error 220 | if e, ok := err.(*url.Error); ok { 221 | if url, err := url.Parse(e.URL); err == nil { 222 | e.URL = url.String() 223 | return nil, e 224 | } 225 | } 226 | 227 | return nil, err 228 | } 229 | defer resp.Body.Close() 230 | 231 | data, err := ioutil.ReadAll(resp.Body) 232 | if err != nil { 233 | return resp, err 234 | } 235 | 236 | if err := checkResponse(resp, data); err != nil { 237 | return resp, err 238 | } 239 | 240 | if v != nil { 241 | if err := json.Unmarshal(data, v); err != nil { 242 | return resp, fmt.Errorf("Unmarshal error %s\n", err) 243 | } 244 | } 245 | return resp, nil 246 | } 247 | 248 | type Error struct { 249 | Code int `json:"code"` 250 | Message string `json:"message"` 251 | } 252 | 253 | // An unsuccessful call to the Dashboard API will return a 200 response containing 254 | // a field success set to false. Additionally an error object will be returned, 255 | // containing a code referencing the error, and a message in a human-readable format. 256 | type ErrorResponse struct { 257 | response *http.Response // HTTP response that caused this error 258 | 259 | Success bool `json:"success"` 260 | ErrorField Error `json:"error"` 261 | } 262 | 263 | // Check wether or not the API response contains an error 264 | func checkResponse(r *http.Response, data []byte) error { 265 | errorResponse := &ErrorResponse{response: r} 266 | if data != nil { 267 | if err := json.Unmarshal(data, errorResponse); err != nil { 268 | return err 269 | } 270 | } 271 | if !errorResponse.Success { 272 | return fmt.Errorf("Error: %v, %s", 273 | errorResponse.ErrorField.Code, 274 | errorResponse.ErrorField.Message) 275 | } 276 | return nil 277 | } 278 | 279 | // Bool is a helper routine that allocates a new bool value 280 | // to store v and returns a pointer to it. 281 | func Bool(v bool) *bool { return &v } 282 | 283 | // Int is a helper routine that allocates a new int value 284 | // to store v and returns a pointer to it. 285 | func Int(v int) *int { return &v } 286 | 287 | // Int64 is a helper routine that allocates a new int64 value 288 | // to store v and returns a pointer to it. 289 | func Int64(v int64) *int64 { return &v } 290 | 291 | // Float64 is a helper routine that allocates a new float64 value 292 | // to store v and returns a pointer to it. 293 | func Float64(v float64) *float64 { return &v } 294 | 295 | // String is a helper routine that allocates a new string value 296 | // to store v and returns a pointer to it. 297 | func String(v string) *string { return &v } 298 | -------------------------------------------------------------------------------- /paddle/paddle_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | // The VendorID used during client initialization 13 | vendorId = "123" 14 | 15 | // The VendorAuthCode used during client initialization 16 | vendorAuthCode = "123" 17 | ) 18 | 19 | // setup sets up a test HTTP server along with a paddle.Client that is 20 | // configured to talk to that test server. Tests should register handlers on 21 | // mux which provide mock responses for the API method being tested. 22 | func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) { 23 | // mux is the HTTP request multiplexer used with the test server. 24 | mux = http.NewServeMux() 25 | 26 | // server is a test HTTP server used to provide mock API responses. 27 | server := httptest.NewServer(mux) 28 | 29 | // client is the Paddle client being tested and is 30 | // configured to use test server. 31 | client = NewClient(vendorId, vendorAuthCode, nil) 32 | url, _ := url.Parse(server.URL + "/") 33 | client.BaseURL = url 34 | 35 | return client, mux, server.URL, server.Close 36 | } 37 | 38 | // checkoutSetup sets up a test HTTP server along with a checkout paddle.Client that is 39 | // configured to talk to that test server. Tests should register handlers on 40 | // mux which provide mock responses for the API method being tested. 41 | func checkoutSetup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) { 42 | // mux is the HTTP request multiplexer used with the test server. 43 | mux = http.NewServeMux() 44 | 45 | // server is a test HTTP server used to provide mock API responses. 46 | server := httptest.NewServer(mux) 47 | 48 | // client is the Paddle client being tested and is 49 | // configured to use test server. 50 | client = NewCheckoutClient(nil) 51 | url, _ := url.Parse(server.URL + "/") 52 | client.BaseURL = url 53 | 54 | return client, mux, server.URL, server.Close 55 | } 56 | 57 | type values map[string]string 58 | 59 | func testFormValues(t *testing.T, r *http.Request, values values) { 60 | want := url.Values{} 61 | for k, v := range values { 62 | want.Set(k, v) 63 | } 64 | 65 | // Extend Form with VendorID and VendorAuthCode passed along client initialization. 66 | want.Set(vendorIdAttribute, vendorId) 67 | want.Set(vendorAuthCodeAttribute, vendorAuthCode) 68 | 69 | r.ParseForm() 70 | if got := r.Form; !reflect.DeepEqual(got, want) { 71 | t.Errorf("Request parameters: %v, want %v", got, want) 72 | } 73 | } 74 | 75 | func testSoftFormValues(t *testing.T, r *http.Request, values values) { 76 | want := url.Values{} 77 | for k, v := range values { 78 | want.Set(k, v) 79 | } 80 | 81 | r.ParseForm() 82 | if got := r.Form; !reflect.DeepEqual(got, want) { 83 | t.Errorf("Request parameters: %v, want %v", got, want) 84 | } 85 | } 86 | 87 | func testMethod(t *testing.T, r *http.Request, want string) { 88 | if got := r.Method; got != want { 89 | t.Errorf("Request method: %v, want %v", got, want) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /paddle/pay_link.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // PayLinkService handles communication with the pay link related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/pay-links 12 | type PayLinkService service 13 | 14 | type PayLinkCreate struct { 15 | ProductID int `url:"product_id,omitempty"` 16 | Title string `url:"title,omitempty"` 17 | WebhookURL string `url:"webhook_url,omitempty"` 18 | Prices string `url:"prices,omitempty"` 19 | RecurringPrices string `url:"recurring_prices,omitempty"` 20 | TrialDays int `url:"trial_days,omitempty"` 21 | CustomMessage string `url:"custom_message,omitempty"` 22 | CouponCode string `url:"coupon_code,omitempty"` 23 | Discountable int `url:"discountable,omitempty"` 24 | ImageURL string `url:"image_url,omitempty"` 25 | ReturnURL string `url:"return_url,omitempty"` 26 | QuantityVariable int `url:"quantity_variable,omitempty"` 27 | Quantity int `url:"quantity,omitempty"` 28 | Expires string `url:"expires,omitempty"` 29 | Affiliates string `url:"affiliates,omitempty"` 30 | RecurringAffiliateLimit int `url:"recurring_affiliate_limit,omitempty"` 31 | MarketingConsent int `url:"marketing_consent,omitempty"` 32 | CustomerEmail string `url:"customer_email,omitempty"` 33 | CustomerCountry string `url:"customer_country,omitempty"` 34 | CustomerPostcode string `url:"customer_postcode,omitempty"` 35 | Passthrough string `url:"passthrough,omitempty"` 36 | VatNumber string `url:"vat_number,omitempty"` 37 | VatCompanyName string `url:"vat_company_name,omitempty"` 38 | VatStreet string `url:"vat_street,omitempty"` 39 | VatCity string `url:"vat_city,omitempty"` 40 | VatState string `url:"vat_state,omitempty"` 41 | VatCountry string `url:"vat_country,omitempty"` 42 | VatPostcode string `url:"vat_postcode,omitempty"` 43 | } 44 | 45 | type PayLinkCreateResponse struct { 46 | Success bool `json:"success"` 47 | Response *struct { 48 | URL *string `json:"url"` 49 | } `json:"response"` 50 | } 51 | 52 | // Generate a link with custom attributes set for a one-time or subscription checkout 53 | // 54 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/pay-links/createpaylink 55 | func (s *PayLinkService) Create(ctx context.Context, payLink *PayLinkCreate) (*string, *http.Response, error) { 56 | u := "2.0/product/generate_pay_link" 57 | 58 | req, err := s.client.NewRequest("POST", u, payLink) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | payLinkCreateResponse := new(PayLinkCreateResponse) 64 | response, err := s.client.Do(ctx, req, payLinkCreateResponse) 65 | if err != nil { 66 | return nil, response, err 67 | } 68 | 69 | return payLinkCreateResponse.Response.URL, response, nil 70 | } 71 | -------------------------------------------------------------------------------- /paddle/pay_link_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestPayLinkService_Create(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/product/generate_pay_link", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"product_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": {"url":"abc"}}`) 19 | }) 20 | 21 | payLink := &PayLinkCreate{ProductID: 1} 22 | url, _, err := client.PayLink.Create(context.Background(), payLink) 23 | if err != nil { 24 | t.Errorf("PayLink.Create returned error: %v", err) 25 | } 26 | 27 | want := "abc" 28 | if !reflect.DeepEqual(*url, want) { 29 | t.Errorf("PayLink.Create returned %+v, want %+v", *url, want) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /paddle/payments.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // PaymentsService handles communication with the payments related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/payments/ 12 | type PaymentsService service 13 | 14 | // Payment represents a Paddle payment. 15 | type Payment struct { 16 | ID *int `json:"id,omitempty"` 17 | SubscriptionID *int `json:"subscription_id,omitempty"` 18 | Amount *int `json:"amount,omitempty"` 19 | Currency *string `json:"currency,omitempty"` 20 | PayoutDate *string `json:"payout_date,omitempty"` 21 | IsPaid *int `json:"is_paid,omitempty"` 22 | ReceiptUrl *string `json:"receipt_url,omitempty"` 23 | IsOneOffCharge *int `json:"is_one_off_charge,omitempty"` 24 | } 25 | 26 | type PaymentsResponse struct { 27 | Success bool `json:"success"` 28 | Response []*Payment `json:"response"` 29 | } 30 | 31 | // PaymentsOptions specifies the optional parameters to the 32 | // Payments.List method. 33 | type PaymentsOptions struct { 34 | // Payments for a specific subscription. 35 | SubscriptionID int `url:"subscription_id,omitempty"` 36 | // The product/plan ID (single or comma-separated values) 37 | Plan int `url:"plan,omitempty"` 38 | // Payment is paid (0 = No, 1 = Yes) 39 | IsPaid int `url:"is_paid,omitempty"` 40 | // Payments starting from (date in format YYYY-MM-DD) 41 | From string `url:"from,omitempty"` 42 | // Payments up to (date in format YYYY-MM-DD) 43 | To string `url:"to,omitempty"` 44 | // Non-recurring payments created from the 45 | IsOneOffCharge bool `url:"is_one_off_charge,omitempty"` 46 | } 47 | 48 | // List all paid and upcoming (unpaid) payments 49 | // 50 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/payments/listpayments 51 | func (s *PaymentsService) List(ctx context.Context, options *PaymentsOptions) ([]*Payment, *http.Response, error) { 52 | u := "2.0/subscription/payments" 53 | req, err := s.client.NewRequest("POST", u, options) 54 | if err != nil { 55 | return nil, nil, err 56 | } 57 | 58 | paymentsResponse := new(PaymentsResponse) 59 | response, err := s.client.Do(ctx, req, paymentsResponse) 60 | if err != nil { 61 | return nil, response, err 62 | } 63 | 64 | return paymentsResponse.Response, response, nil 65 | } 66 | 67 | type PaymentUpdate struct { 68 | PaymentID int `url:"payment_id,omitempty"` 69 | Date string `url:"date,omitempty"` 70 | } 71 | 72 | type PaymentUpdateResponse struct { 73 | Success bool `json:"success"` 74 | } 75 | 76 | // Change the due date of the upcoming subscription payment 77 | // 78 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/payments/updatepayment 79 | func (s *PaymentsService) Update(ctx context.Context, paymentID int, date string) (bool, *http.Response, error) { 80 | u := "2.0/subscription/payments_reschedule" 81 | 82 | update := &PaymentUpdate{ 83 | PaymentID: paymentID, 84 | Date: date, 85 | } 86 | 87 | req, err := s.client.NewRequest("POST", u, update) 88 | if err != nil { 89 | return false, nil, err 90 | } 91 | 92 | paymentUpdateResponse := new(PaymentUpdateResponse) 93 | response, err := s.client.Do(ctx, req, paymentUpdateResponse) 94 | if err != nil { 95 | return false, response, err 96 | } 97 | 98 | return paymentUpdateResponse.Success, response, nil 99 | } 100 | -------------------------------------------------------------------------------- /paddle/payments_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestPaymentsService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/subscription/payments", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"subscription_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": [{"id":1, "subscription_id":1}]}`) 19 | }) 20 | 21 | opt := &PaymentsOptions{SubscriptionID: 1} 22 | payments, _, err := client.Payments.List(context.Background(), opt) 23 | if err != nil { 24 | t.Errorf("Payments.List returned error: %v", err) 25 | } 26 | 27 | want := []*Payment{{ID: Int(1), SubscriptionID: Int(1)}} 28 | if !reflect.DeepEqual(payments, want) { 29 | t.Errorf("Payments.List returned %+v, want %+v", payments, want) 30 | } 31 | } 32 | 33 | func TestPaymentsService_Update(t *testing.T) { 34 | client, mux, _, teardown := setup() 35 | defer teardown() 36 | 37 | mux.HandleFunc("/2.0/subscription/payments_reschedule", func(w http.ResponseWriter, r *http.Request) { 38 | testMethod(t, r, "POST") 39 | testFormValues(t, r, values{"payment_id": "1", "date": "2021-11-11"}) 40 | fmt.Fprint(w, `{"success":true}`) 41 | }) 42 | 43 | ok, _, err := client.Payments.Update(context.Background(), 1, "2021-11-11") 44 | if err != nil { 45 | t.Errorf("Payments.Update returned error: %v", err) 46 | } 47 | 48 | want := true 49 | if !reflect.DeepEqual(ok, want) { 50 | t.Errorf("Payments.List returned %+v, want %+v", ok, want) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /paddle/plans.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // PlansService handles communication with the plans related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/plans 12 | type PlansService service 13 | 14 | // Plan represents a Paddle plan. 15 | type Plan struct { 16 | ID *int `json:"id,omitempty"` 17 | Name *string `json:"name,omitempty"` 18 | BillingType *string `json:"billing_type,omitempty"` 19 | BillingPeriod *int `json:"billing_period,omitempty"` 20 | TrialDays *int `json:"trial_days,omitempty"` 21 | InitialPrice map[string]interface{} `json:"initial_price,omitempty"` 22 | RecurringPrice map[string]interface{} `json:"recurring_price,omitempty"` 23 | } 24 | 25 | type PlansResponse struct { 26 | Success bool `json:"success"` 27 | Response []*Plan `json:"response"` 28 | } 29 | 30 | // PlansOptions specifies the optional parameters to the 31 | // PLansService.List method. 32 | type PlansOptions struct { 33 | // PlanID filters Products/Plans based on their id. 34 | PlanID int `url:"plan,omitempty"` 35 | } 36 | 37 | // List all of the available subscription plans in your account 38 | // 39 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/plans/listplans 40 | func (s *PlansService) List(ctx context.Context, options *PlansOptions) ([]*Plan, *http.Response, error) { 41 | u := "2.0/subscription/plans" 42 | req, err := s.client.NewRequest("POST", u, options) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | plansResponse := new(PlansResponse) 48 | response, err := s.client.Do(ctx, req, plansResponse) 49 | if err != nil { 50 | return nil, response, err 51 | } 52 | 53 | return plansResponse.Response, response, nil 54 | } 55 | 56 | type PlanCreate struct { 57 | PlanName string `url:"plan_name,omitempty"` 58 | PlanLength int `url:"plan_length,omitempty"` 59 | PlanType string `url:"plan_type,omitempty"` 60 | PlanTrialDays int `url:"plan_trial_days,omitempty"` 61 | MainCurrencyCode string `url:"main_currency_code,omitempty"` 62 | RecurringPriceUsd string `url:"recurring_price_usd,omitempty"` 63 | RecurringPriceGbp string `url:"recurring_price_gbp,omitempty"` 64 | RecurringPriceEur string `url:"recurring_price_eur,omitempty"` 65 | } 66 | 67 | type PlanCreateOptions struct { 68 | PlanTrialDays int 69 | MainCurrencyCode string 70 | RecurringPriceUsd string 71 | RecurringPriceGbp string 72 | RecurringPriceEur string 73 | } 74 | 75 | type PlanCreateResponse struct { 76 | Success bool `json:"success"` 77 | Response *Product `json:"response"` 78 | } 79 | 80 | // Create a new subscription plan with the supplied parameters 81 | // 82 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/plans/createplan 83 | func (s *PlansService) Create(ctx context.Context, planName, planType string, planLength int, options *PlanCreateOptions) (*Product, *http.Response, error) { 84 | u := "2.0/subscription/plans_create" 85 | 86 | create := &PlanCreate{ 87 | PlanName: planName, 88 | PlanLength: planLength, 89 | PlanType: planType, 90 | } 91 | if options != nil { 92 | create.PlanTrialDays = options.PlanTrialDays 93 | create.MainCurrencyCode = options.MainCurrencyCode 94 | create.RecurringPriceUsd = options.RecurringPriceUsd 95 | create.RecurringPriceGbp = options.RecurringPriceGbp 96 | create.RecurringPriceEur = options.RecurringPriceEur 97 | } 98 | req, err := s.client.NewRequest("POST", u, create) 99 | if err != nil { 100 | return nil, nil, err 101 | } 102 | 103 | planCreateResponse := new(PlanCreateResponse) 104 | response, err := s.client.Do(ctx, req, planCreateResponse) 105 | if err != nil { 106 | return nil, response, err 107 | } 108 | 109 | return planCreateResponse.Response, response, nil 110 | } 111 | -------------------------------------------------------------------------------- /paddle/plans_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestPlansService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/subscription/plans", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"plan": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": [{"id":1, "initial_price":{"USD": "79.00"}}]}`) 19 | }) 20 | 21 | opt := &PlansOptions{PlanID: 1} 22 | plans, _, err := client.Plans.List(context.Background(), opt) 23 | if err != nil { 24 | t.Errorf("Plans.List returned error: %v", err) 25 | } 26 | 27 | want := []*Plan{{ID: Int(1), InitialPrice: map[string]interface{}{"USD": "79.00"}}} 28 | if !reflect.DeepEqual(plans, want) { 29 | t.Errorf("Plans.List returned %+v, want %+v", plans, want) 30 | } 31 | } 32 | 33 | func TestPlansService_Create(t *testing.T) { 34 | client, mux, _, teardown := setup() 35 | defer teardown() 36 | 37 | mux.HandleFunc("/2.0/subscription/plans_create", func(w http.ResponseWriter, r *http.Request) { 38 | testMethod(t, r, "POST") 39 | testFormValues(t, r, values{"plan_name": "a", "plan_length": "1", "plan_type": "month", "plan_trial_days": "10"}) 40 | fmt.Fprint(w, `{"success":true, "response": {"product_id":1}}`) 41 | }) 42 | 43 | opt := &PlanCreateOptions{PlanTrialDays: 10} 44 | product, _, err := client.Plans.Create(context.Background(), "a", "month", 1, opt) 45 | if err != nil { 46 | t.Errorf("Plans.Create returned error: %v", err) 47 | } 48 | 49 | want := &Product{ProductID: Int(1)} 50 | if !reflect.DeepEqual(product, want) { 51 | t.Errorf("Plans.Create returned %+v, want %+v", product, want) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /paddle/prices.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // PricesService handles communication with the prices related 10 | // methods of the Paddle API. 11 | // 12 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/prices 13 | type PricesService service 14 | 15 | // Prices represents a Paddle order details. 16 | type Prices struct { 17 | CustomerCountry *string `json:"customer_country,omitempty"` 18 | Products []*Product `json:"products,omitempty"` 19 | } 20 | 21 | type Product struct { 22 | ProductID *int `json:"product_id,omitempty"` 23 | ProductTitle *string `json:"product_title,omitempty"` 24 | Currency *string `json:"currency,omitempty"` 25 | VendorSetPricesIncludedTax *bool `json:"vendor_set_prices_included_tax,omitempty"` 26 | Price *Price `json:"price,omitempty"` 27 | ListPrice *Price `json:"list_price,omitempty"` 28 | AppliedCoupon *AppliedCoupon `json:"applied_coupon,omitempty"` 29 | } 30 | 31 | type Price struct { 32 | Gross *float64 `json:"gross,omitempty"` 33 | Net *float64 `json:"net,omitempty"` 34 | Tax *float64 `json:"tax,omitempty"` 35 | } 36 | 37 | type AppliedCoupon struct { 38 | Code *string `json:"code,omitempty"` 39 | Discount *float64 `json:"discount,omitempty"` 40 | } 41 | 42 | type PricesResponse struct { 43 | Success bool `json:"success"` 44 | Response *Prices `json:"response"` 45 | } 46 | 47 | type PricesOptions struct { 48 | CustomerCountry string 49 | CustomerIP string 50 | Coupons string 51 | } 52 | 53 | // Retrieve prices for one or multiple products or plans 54 | // 55 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/prices/getprices 56 | func (s *PricesService) Get(ctx context.Context, productIDs string, options *PricesOptions) (*Prices, *http.Response, error) { 57 | u := fmt.Sprintf("2.0/prices?product_ids=%v", productIDs) 58 | 59 | if options != nil { 60 | if options.CustomerCountry != "" { 61 | u = fmt.Sprintf("%s&customer_country=%v", u, options.CustomerCountry) 62 | } 63 | if options.CustomerIP != "" { 64 | u = fmt.Sprintf("%s&customer_ip=%v", u, options.CustomerIP) 65 | } 66 | if options.Coupons != "" { 67 | u = fmt.Sprintf("%s&coupons=%v", u, options.Coupons) 68 | } 69 | } 70 | 71 | req, err := s.client.NewRequest("GET", u, nil) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | pricesResponse := new(PricesResponse) 77 | response, err := s.client.Do(ctx, req, pricesResponse) 78 | if err != nil { 79 | return nil, response, err 80 | } 81 | 82 | return pricesResponse.Response, response, nil 83 | } 84 | -------------------------------------------------------------------------------- /paddle/prices_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestPricesService_Get(t *testing.T) { 12 | client, mux, _, teardown := checkoutSetup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/prices", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "GET") 17 | testSoftFormValues(t, r, values{"product_ids": "1", "customer_country": "tn"}) 18 | fmt.Fprint(w, `{"success":true, "response": { 19 | "customer_country": "tn", 20 | "products": [{ 21 | "product_id": 1, 22 | "price": {"gross": 10}, 23 | "applied_coupon": {"code": "1"} 24 | }]}}`) 25 | }) 26 | 27 | options := &PricesOptions{CustomerCountry: "tn"} 28 | prices, _, err := client.Prices.Get(context.Background(), "1", options) 29 | if err != nil { 30 | t.Errorf("Prices.Get returned error: %v", err) 31 | } 32 | 33 | want := &Prices{ 34 | CustomerCountry: String("tn"), 35 | Products: []*Product{{ 36 | ProductID: Int(1), 37 | Price: &Price{Gross: Float64(10)}, 38 | AppliedCoupon: &AppliedCoupon{Code: String("1")}, 39 | }}, 40 | } 41 | if !reflect.DeepEqual(prices, want) { 42 | t.Errorf("Prices.Get returned %+v, want %+v", prices, want) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /paddle/products.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // ProductsService handles communication with the products related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/products 12 | type ProductsService service 13 | 14 | // Product represents a Paddle plan. 15 | type OneTimeProducts struct { 16 | Total *int `json:"total,omitempty"` 17 | Count *int `json:"count,omitempty"` 18 | Products []*OneTimeProduct `json:"products,omitempty"` 19 | } 20 | 21 | type OneTimeProduct struct { 22 | ID *int `json:"id,omitempty"` 23 | Name *string `json:"name,omitempty"` 24 | Description *string `json:"description,omitempty"` 25 | BasePrice *float64 `json:"base_price,omitempty"` 26 | SalePrice *string `json:"sale_price,omitempty"` 27 | Screenshots *[]map[string]interface{} `json:"screenshots,omitempty"` 28 | Icon *string `json:"icon,omitempty"` 29 | Currency *string `json:"currency,omitempty"` 30 | } 31 | 32 | type ProductsResponse struct { 33 | Success bool `json:"success"` 34 | Response *OneTimeProducts `json:"response"` 35 | } 36 | 37 | // List all published one-time products in your account 38 | // 39 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/products/getproducts 40 | func (s *ProductsService) List(ctx context.Context) (*OneTimeProducts, *http.Response, error) { 41 | u := "2.0/product/get_products" 42 | req, err := s.client.NewRequest("POST", u, nil) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | productsResponse := new(ProductsResponse) 48 | response, err := s.client.Do(ctx, req, productsResponse) 49 | if err != nil { 50 | return nil, response, err 51 | } 52 | 53 | return productsResponse.Response, response, nil 54 | } 55 | -------------------------------------------------------------------------------- /paddle/products_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestProductsService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/product/get_products", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | fmt.Fprint(w, `{"success":true, "response": {"total":1, "count":1, "products":[{"id": 1}]}}`) 18 | }) 19 | 20 | products, _, err := client.Products.List(context.Background()) 21 | if err != nil { 22 | t.Errorf("Products.List returned error: %v", err) 23 | } 24 | 25 | want := &OneTimeProducts{Total: Int(1), Count: Int(1), Products: []*OneTimeProduct{{ID: Int(1)}}} 26 | if !reflect.DeepEqual(products, want) { 27 | t.Errorf("Products.List returned %+v, want %+v", products, want) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /paddle/refund_payment.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // RefundPaymentService handles communication with the payments refund related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/payments/ 12 | type RefundPaymentService service 13 | 14 | type RefundPaymentResponse struct { 15 | Success bool `json:"success"` 16 | Response *struct { 17 | RefundRequestID *int `json:"refund_request_id"` 18 | } `json:"response"` 19 | } 20 | 21 | type RefundPayment struct { 22 | OrderID string `url:"order_id,omitempty"` 23 | Amount float64 `url:"amount,omitempty"` 24 | Reason string `url:"reason,omitempty"` 25 | } 26 | 27 | // RefundPaymentOptions specifies the optional parameters to the 28 | // RefundPayment.Refund method. 29 | type RefundPaymentOptions struct { 30 | Amount float64 31 | Reason string 32 | } 33 | 34 | // Request a refund for a one-time or subscription payment, either in full or partial 35 | // 36 | // Paddle API docs: https://developer.paddle.com/api-reference/product-api/payments/refundpayment 37 | func (s *RefundPaymentService) Refund(ctx context.Context, orderID string, options *RefundPaymentOptions) (*int, *http.Response, error) { 38 | u := "2.0/payment/refund" 39 | 40 | refund := &RefundPayment{ 41 | OrderID: orderID, 42 | } 43 | if options != nil { 44 | refund.Amount = options.Amount 45 | refund.Reason = options.Reason 46 | } 47 | req, err := s.client.NewRequest("POST", u, refund) 48 | if err != nil { 49 | return nil, nil, err 50 | } 51 | 52 | refundPaymentsResponse := new(RefundPaymentResponse) 53 | response, err := s.client.Do(ctx, req, refundPaymentsResponse) 54 | if err != nil { 55 | return nil, response, err 56 | } 57 | 58 | return refundPaymentsResponse.Response.RefundRequestID, response, nil 59 | } 60 | -------------------------------------------------------------------------------- /paddle/refund_payment_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestRefundPaymentService_Refund(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/payment/refund", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"order_id": "1", "amount": "10"}) 18 | fmt.Fprint(w, `{"success":true, "response": {"refund_request_id": 1}}`) 19 | }) 20 | 21 | opt := &RefundPaymentOptions{Amount: float64(10)} 22 | request_id, _, err := client.RefundPayment.Refund(context.Background(), "1", opt) 23 | if err != nil { 24 | t.Errorf("RefundPayment.Refund returned error: %v", err) 25 | } 26 | 27 | want := 1 28 | if !reflect.DeepEqual(*request_id, want) { 29 | t.Errorf("RefundPayment.Refund returned %+v, want %+v", *request_id, want) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /paddle/user_history.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // UserHistoryService handles communication with the user history related 10 | // methods of the Paddle API. 11 | // 12 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/user-history/ 13 | type UserHistoryService service 14 | 15 | // UserHistory represents a Paddle plan. 16 | type UserHistory struct { 17 | Message *string `json:"message,omitempty"` 18 | Callback *string `json:"callback,omitempty"` 19 | } 20 | 21 | type UserHistoryResponse struct { 22 | Success bool `json:"success"` 23 | Response *UserHistory `json:"response"` 24 | } 25 | 26 | // UserHistoryOptions specifies the optional parameters to the 27 | // UserHistoryService.Get method. 28 | type UserHistoryOptions struct { 29 | VendorID *int64 30 | ProductID *int64 31 | } 32 | 33 | // Send the customer an order history and license recovery email 34 | // 35 | // Paddle API docs: https://developer.paddle.com/api-reference/checkout-api/user-history/getuserhistory 36 | func (s *UserHistoryService) Get(ctx context.Context, email string, options *UserHistoryOptions) (*UserHistory, *http.Response, error) { 37 | u := fmt.Sprintf("2.0/user/history?email=%v", email) 38 | 39 | if options != nil { 40 | if options.VendorID != nil { 41 | u = fmt.Sprintf("%s&vendor_id=%d", u, *options.VendorID) 42 | } 43 | if options.ProductID != nil { 44 | u = fmt.Sprintf("%s&product_id=%d", u, *options.ProductID) 45 | } 46 | } 47 | 48 | req, err := s.client.NewRequest("GET", u, nil) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | userHistoryResponse := new(UserHistoryResponse) 54 | response, err := s.client.Do(ctx, req, userHistoryResponse) 55 | if err != nil { 56 | return nil, response, err 57 | } 58 | 59 | return userHistoryResponse.Response, response, nil 60 | } 61 | -------------------------------------------------------------------------------- /paddle/user_history_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestUserHistoryService_Get(t *testing.T) { 12 | client, mux, _, teardown := checkoutSetup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/user/history", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "GET") 17 | testSoftFormValues(t, r, values{"email": "abc", "product_id": "1"}) 18 | fmt.Fprint(w, `{"success":true, "response": {"message": "abc"}}`) 19 | }) 20 | 21 | options := &UserHistoryOptions{ProductID: Int64(1)} 22 | history, _, err := client.UserHistory.Get(context.Background(), "abc", options) 23 | if err != nil { 24 | t.Errorf("UserHistory.Get returned error: %v", err) 25 | } 26 | 27 | want := &UserHistory{Message: String("abc")} 28 | if !reflect.DeepEqual(history, want) { 29 | t.Errorf("UserHistory.Get returned %+v, want %+v", history, want) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /paddle/users.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // UsersService handles communication with the user related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/users 12 | type UsersService service 13 | 14 | // User represents a Paddle user. 15 | type User struct { 16 | SubscriptionID *int `json:"subscription_id,omitempty"` 17 | PlanID *int `json:"plan_id,omitempty"` 18 | UserID *int `json:"user_id,omitempty"` 19 | UserEmail *string `json:"user_email,omitempty"` 20 | MarketingConsent *bool `json:"marketing_consent,omitempty"` 21 | UpdateURL *string `json:"update_url,omitempty"` 22 | CancelURL *string `json:"cancel_url,omitempty"` 23 | State *string `json:"state,omitempty"` 24 | SignupDate *string `json:"signup_date,omitempty"` 25 | LastPayment *UserPayment `json:"last_payment,omitempty"` 26 | NextPayment *UserPayment `json:"next_payment,omitempty"` 27 | PaymentInformation *PaymentInformation `json:"payment_information,omitempty"` 28 | PausedAt *string `json:"paused_at,omitempty"` 29 | PausedFrom *string `json:"paused_from,omitempty"` 30 | } 31 | 32 | type UserPayment struct { 33 | Amount *float64 `json:"amount,omitempty"` 34 | Currency *string `json:"currency,omitempty"` 35 | Date *string `json:"date,omitempty"` 36 | } 37 | 38 | type PaymentInformation struct { 39 | PaymentMethod *string `json:"payment_method,omitempty"` 40 | CardType *string `json:"card_type,omitempty"` 41 | LastFourDigits *string `json:"last_four_digits,omitempty"` 42 | ExpiryDate *string `json:"expiry_date,omitempty"` 43 | } 44 | 45 | type UsersResponse struct { 46 | Success bool `json:"success"` 47 | Response []*User `json:"response"` 48 | } 49 | 50 | // UsersOptions specifies the optional parameters to the 51 | // UsersService.List method. 52 | type UsersOptions struct { 53 | // SubscriptionID filters users based on their susbscription id. 54 | SubscriptionID string `url:"subscription_id,omitempty"` 55 | 56 | // PlanID filters users by the plan id. 57 | PlanID string `url:"plan_id,omitempty"` 58 | 59 | // State filters users based on the state. Possible values are: active, 60 | // past_due, trialing, paused, deleted. Returns all active, past_due, 61 | // trialing and paused subscription plans if not specified. 62 | State string `url:"state,omitempty"` 63 | 64 | ListOptions 65 | } 66 | 67 | // List all users subscribed to any of your subscription plans 68 | // 69 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/users/listusers 70 | func (s *UsersService) List(ctx context.Context, options *UsersOptions) ([]*User, *http.Response, error) { 71 | u := "2.0/subscription/users" 72 | req, err := s.client.NewRequest("POST", u, options) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | usersResponse := new(UsersResponse) 78 | response, err := s.client.Do(ctx, req, usersResponse) 79 | if err != nil { 80 | return nil, response, err 81 | } 82 | 83 | return usersResponse.Response, response, nil 84 | } 85 | 86 | type UserUpdate struct { 87 | SubscriptionID int `url:"subscription_id,omitempty"` 88 | Quantity int `url:"quantity,omitempty"` 89 | Currency string `url:"currency,omitempty"` 90 | RecurringPrice float64 `url:"recurring_price,omitempty"` 91 | BillImmediately bool `url:"bill_immediately,omitempty"` 92 | PlanID int `url:"plan_id,omitempty"` 93 | Prorate bool `url:"prorate,omitempty"` 94 | KeepModifiers bool `url:"keep_modifiers,omitempty"` 95 | Passthrough string `url:"passthrough,omitempty"` 96 | Pause bool `url:"pause,omitempty"` 97 | } 98 | 99 | type UserUpdateOptions struct { 100 | Currency string 101 | RecurringPrice float64 102 | BillImmediately bool 103 | PlanID int 104 | Prorate bool 105 | KeepModifiers bool 106 | Passthrough string 107 | Pause bool 108 | } 109 | 110 | type UserUpdateResponse struct { 111 | Success bool `json:"success"` 112 | Response *User `json:"response"` 113 | } 114 | 115 | // Update the quantity, price, and/or plan of a user’s subscription 116 | // 117 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/users/updateuser 118 | func (s *UsersService) Update(ctx context.Context, subscriptionID, quantity int, options *UserUpdateOptions) (*User, *http.Response, error) { 119 | u := "2.0/subscription/users/update" 120 | 121 | update := &UserUpdate{ 122 | SubscriptionID: subscriptionID, 123 | Quantity: quantity, 124 | } 125 | if options != nil { 126 | update.Currency = options.Currency 127 | update.RecurringPrice = options.RecurringPrice 128 | update.BillImmediately = options.BillImmediately 129 | update.PlanID = options.PlanID 130 | update.Prorate = options.Prorate 131 | update.KeepModifiers = options.KeepModifiers 132 | update.Passthrough = options.Passthrough 133 | update.Pause = options.Pause 134 | } 135 | req, err := s.client.NewRequest("POST", u, update) 136 | if err != nil { 137 | return nil, nil, err 138 | } 139 | 140 | userUpdateResponse := new(UserUpdateResponse) 141 | response, err := s.client.Do(ctx, req, userUpdateResponse) 142 | if err != nil { 143 | return nil, response, err 144 | } 145 | 146 | return userUpdateResponse.Response, response, nil 147 | } 148 | 149 | type UserCancel struct { 150 | SubscriptionID int `url:"subscription_id,omitempty"` 151 | } 152 | 153 | type UserCancelResponse struct { 154 | Success bool `json:"success"` 155 | } 156 | 157 | // Cancel the specified user’s subscription 158 | // 159 | // Paddle API docs: https://developer.paddle.com/api-reference/subscription-api/users/canceluser 160 | func (s *UsersService) Cancel(ctx context.Context, subscriptionID int) (bool, *http.Response, error) { 161 | u := "2.0/subscription/users_cancel" 162 | 163 | cancel := &UserCancel{ 164 | SubscriptionID: subscriptionID, 165 | } 166 | req, err := s.client.NewRequest("POST", u, cancel) 167 | if err != nil { 168 | return false, nil, err 169 | } 170 | 171 | userCancelResponse := new(UserCancelResponse) 172 | response, err := s.client.Do(ctx, req, userCancelResponse) 173 | if err != nil { 174 | return false, response, err 175 | } 176 | 177 | return userCancelResponse.Success, response, nil 178 | } 179 | -------------------------------------------------------------------------------- /paddle/users_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestUsersService_List(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/subscription/users", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"subscription_id": "1", "page": "2"}) 18 | fmt.Fprint(w, `{"success":true, "response": [{"user_id":2}]}`) 19 | }) 20 | 21 | opt := &UsersOptions{SubscriptionID: "1", ListOptions: ListOptions{Page: 2}} 22 | users, _, err := client.Users.List(context.Background(), opt) 23 | if err != nil { 24 | t.Errorf("Users.List returned error: %v", err) 25 | } 26 | 27 | want := []*User{{UserID: Int(2)}} 28 | if !reflect.DeepEqual(users, want) { 29 | t.Errorf("Users.List returned %+v, want %+v", users, want) 30 | } 31 | } 32 | 33 | func TestUsersService_Update(t *testing.T) { 34 | client, mux, _, teardown := setup() 35 | defer teardown() 36 | 37 | mux.HandleFunc("/2.0/subscription/users/update", func(w http.ResponseWriter, r *http.Request) { 38 | testMethod(t, r, "POST") 39 | testFormValues(t, r, values{"subscription_id": "1", "quantity": "2", "plan_id": "123"}) 40 | fmt.Fprint(w, `{"success":true, "response": {"subscription_id": 1, "user_id":2}}`) 41 | }) 42 | 43 | opt := &UserUpdateOptions{PlanID: 123} 44 | resp, _, err := client.Users.Update(context.Background(), 1, 2, opt) 45 | if err != nil { 46 | t.Errorf("Users.Update returned error: %v", err) 47 | } 48 | 49 | want := &User{SubscriptionID: Int(1), UserID: Int(2)} 50 | if !reflect.DeepEqual(resp, want) { 51 | t.Errorf("Users.Update returned %+v, want %+v", resp, want) 52 | } 53 | } 54 | 55 | func TestUsersService_Cancel(t *testing.T) { 56 | client, mux, _, teardown := setup() 57 | defer teardown() 58 | 59 | mux.HandleFunc("/2.0/subscription/users_cancel", func(w http.ResponseWriter, r *http.Request) { 60 | testMethod(t, r, "POST") 61 | testFormValues(t, r, values{"subscription_id": "1"}) 62 | fmt.Fprint(w, `{"success":true}`) 63 | }) 64 | 65 | resp, _, err := client.Users.Cancel(context.Background(), 1) 66 | if err != nil { 67 | t.Errorf("Users.Cancel returned error: %v", err) 68 | } 69 | 70 | want := true 71 | if !reflect.DeepEqual(resp, want) { 72 | t.Errorf("Users.Cancel returned %+v, want %+v", resp, want) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /paddle/webhooks.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // WebhooksService handles communication with the webhooks related 9 | // methods of the Paddle API. 10 | // 11 | // Paddle API docs: https://developer.paddle.com/api-reference/alert-api/webhooks/ 12 | type WebhooksService service 13 | 14 | // WebhookEvent represents a Paddle plan. 15 | type WebhookEvent struct { 16 | CurrentPage *int `json:"current_page,omitempty"` 17 | TotalPages *int `json:"total_pages,omitempty"` 18 | AlertsPerPage *int `json:"alerts_per_page,omitempty"` 19 | TotalAlerts *int `json:"total_alerts,omitempty"` 20 | QueryHead *string `json:"query_head,omitempty"` 21 | QueryTail *string `json:"query_tail,omitempty"` 22 | Data []*EventData `json:"data,omitempty"` 23 | } 24 | 25 | type EventData struct { 26 | ID *int `json:"id,omitempty"` 27 | AlertName *string `json:"alert_name,omitempty"` 28 | Status *string `json:"status,omitempty"` 29 | CreatedAt *string `json:"created_at,omitempty"` 30 | UpdatedAt *string `json:"updated_at,omitempty"` 31 | Attempts *int `json:"attempts,omitempty"` 32 | Fields *EventField `json:"fields,omitempty"` 33 | } 34 | 35 | type EventField struct { 36 | OrderID *int `json:"order_id,omitempty"` 37 | Amount *string `json:"amount,omitempty"` 38 | Currency *string `json:"currency,omitempty"` 39 | Email *string `json:"email,omitempty"` 40 | MarketingConsent *int `json:"marketing_consent,omitempty"` 41 | } 42 | 43 | type WebhookEventResponse struct { 44 | Success bool `json:"success"` 45 | Response *WebhookEvent `json:"response"` 46 | } 47 | 48 | // WebhookEventOptions specifies the optional parameters to the 49 | // WebhooksService.Get method. 50 | type WebhookEventOptions struct { 51 | // Number of webhook alerts to return per page. Returns 10 alerts by default. 52 | AlertsPerPage string `url:"alerts_per_page,omitempty"` 53 | // The date and time (UTC - Coordinated Universal Time) at which the webhook occurred before (end date). In the format: YYYY-MM-DD HH:MM:SS 54 | QueryHead string `url:"query_head,omitempty"` 55 | // The date and time (UTC - Coordinated Universal Time) at which the webhook occurred after (start date). In the format: YYYY-MM-DD HH:MM:SS 56 | QueryTail string `url:"query_tail,omitempty"` 57 | 58 | ListOptions 59 | } 60 | 61 | // Retrieve past events and alerts that Paddle has sent to webhooks on your account 62 | // 63 | // Paddle API docs: https://developer.paddle.com/api-reference/alert-api/webhooks/webhooks 64 | func (s *WebhooksService) Get(ctx context.Context, options *WebhookEventOptions) (*WebhookEvent, *http.Response, error) { 65 | u := "2.0/alert/webhooks" 66 | req, err := s.client.NewRequest("POST", u, options) 67 | if err != nil { 68 | return nil, nil, err 69 | } 70 | 71 | eventResponse := new(WebhookEventResponse) 72 | response, err := s.client.Do(ctx, req, eventResponse) 73 | if err != nil { 74 | return nil, response, err 75 | } 76 | 77 | return eventResponse.Response, response, nil 78 | } 79 | -------------------------------------------------------------------------------- /paddle/webhooks_test.go: -------------------------------------------------------------------------------- 1 | package paddle 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestWebhooksService_Get(t *testing.T) { 12 | client, mux, _, teardown := setup() 13 | defer teardown() 14 | 15 | mux.HandleFunc("/2.0/alert/webhooks", func(w http.ResponseWriter, r *http.Request) { 16 | testMethod(t, r, "POST") 17 | testFormValues(t, r, values{"page": "1", "alerts_per_page": "2"}) 18 | fmt.Fprint(w, `{"success":true, "response": {"current_page":1, "data":[{"id": 1, "fields": {"order_id": 1}}]}}`) 19 | }) 20 | 21 | opt := &WebhookEventOptions{ListOptions: ListOptions{Page: 1}, AlertsPerPage: "2"} 22 | event, _, err := client.Webhooks.Get(context.Background(), opt) 23 | if err != nil { 24 | t.Errorf("Webhooks.Get returned error: %v", err) 25 | } 26 | 27 | want := &WebhookEvent{CurrentPage: Int(1), Data: []*EventData{{ID: Int(1), Fields: &EventField{OrderID: Int(1)}}}} 28 | if !reflect.DeepEqual(event, want) { 29 | t.Errorf("Webhooks.Get returned %+v, want %+v", event, want) 30 | } 31 | } 32 | --------------------------------------------------------------------------------