├── .all-contributorsrc ├── .github ├── .kodiak.toml ├── renovate.json └── workflows │ └── push.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── README.md ├── dvx.json ├── generate.sh ├── generators ├── generate-api-routes.ts ├── generate-enums.ts └── generate-webhook-alerts.ts ├── jest.config.js ├── package.json ├── src ├── __generated__ │ ├── api-routes.ts │ ├── enums.ts │ └── webhook-alerts.ts ├── exceptions.ts ├── helpers │ ├── converters.ts │ ├── fetch.ts │ └── stableSerialize.ts ├── index.ts ├── interfaces.ts └── metadata.ts ├── tests ├── __snapshots__ │ ├── api-requests.spec.ts.snap │ ├── index.spec.ts.snap │ ├── metadata.spec.ts.snap │ └── parse-webhook-event.spec.ts.snap ├── api-requests.spec.ts ├── fixtures.ts ├── helpers │ ├── __snapshots__ │ │ └── fetch.spec.ts.snap │ ├── converters.spec.ts │ └── fetch.spec.ts ├── index.spec.ts ├── metadata.spec.ts ├── parse-webhook-event.spec.ts └── verify-webhook-event.spec.ts ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "imageSize": 75, 3 | "projectName": "paddle-sdk", 4 | "projectOwner": "devoxa", 5 | "repoType": "github", 6 | "repoHost": "https://github.com", 7 | "skipCi": true, 8 | "contributors": [ 9 | { 10 | "login": "queicherius", 11 | "name": "David Reeß", 12 | "avatar_url": "https://avatars3.githubusercontent.com/u/4615516?v=4", 13 | "profile": "https://www.david-reess.de", 14 | "contributions": ["code", "doc", "test"] 15 | }, 16 | { 17 | "login": "atjeff", 18 | "name": "Jeff Hage", 19 | "avatar_url": "https://avatars1.githubusercontent.com/u/10563763?v=4", 20 | "profile": "https://github.com/atjeff", 21 | "contributions": ["review"] 22 | }, 23 | { 24 | "login": "aradzie", 25 | "name": "Aliaksandr Radzivanovich", 26 | "avatar_url": "https://avatars0.githubusercontent.com/u/44386?v=4", 27 | "profile": "https://github.com/aradzie", 28 | "contributions": ["code", "doc", "test"] 29 | } 30 | ], 31 | "files": ["README.md"], 32 | "contributorsPerLine": 7, 33 | "commitConvention": "none" 34 | } 35 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge] 4 | delete_branch_on_merge = true 5 | priority_merge_label = "priority-automerge" 6 | 7 | [merge.automerge_dependencies] 8 | usernames = ["renovate"] 9 | versions = ["minor", "patch"] 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>devoxa/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test-and-build: 11 | name: 'Test & build' 12 | runs-on: buildjet-4vcpu-ubuntu-2204 13 | timeout-minutes: 30 14 | 15 | steps: 16 | - name: 'Checkout the repository' 17 | uses: actions/checkout@v4 18 | 19 | - name: 'Setup Node.JS' 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '20.9' 23 | 24 | - name: 'Cache dependencies' 25 | uses: buildjet/cache@v4 26 | with: 27 | path: '**/node_modules' 28 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} 29 | 30 | - name: 'Install dependencies' 31 | run: yarn install --frozen-lockfile 32 | 33 | - name: 'Run tests' 34 | run: yarn test --coverage 35 | 36 | - name: 'Save test coverage' 37 | uses: codecov/codecov-action@v4 38 | 39 | - name: 'Check code formatting' 40 | run: yarn format:check 41 | 42 | - name: 'Run linter' 43 | run: yarn lint 44 | 45 | - name: 'Build package' 46 | run: yarn build 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Output 2 | dist/ 3 | 4 | # Tests 5 | coverage/ 6 | 7 | # Dependencies 8 | node_modules/ 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Development Environments 13 | .history/ 14 | .idea/ 15 | .vscode/ 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Tests 2 | coverage/ 3 | 4 | # Dependencies 5 | node_modules/ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Development Environments 10 | .history/ 11 | .idea/ 12 | .vscode/ 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Naming** 2 | 3 | For consistency, make sure that you always convert the following words: 4 | 5 | - `Alert` -> `Event` 6 | - `Plan` -> `Product` 7 | - `Bill` -> `Payment` 8 | - `User` -> `Customer` 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :no_entry: DEPRECATED :no_entry: 2 | 3 | ### This package has been archived and is no longer maintained. While we will not provide any updates or support, the code is still available for reference. If you need this package for your project, we encourage you to fork & republish the code following the license terms. 4 | 5 | --- 6 | 7 | 8 |
19 |
20 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
38 |
39 |
43 | Installation • 44 | Usage • 45 | Contributors • 46 | License 47 |
48 | 49 |David Reeß 💻 📖 ⚠️ |
131 | Jeff Hage 👀 |
132 | Aliaksandr Radzivanovich 💻 📖 ⚠️ |
133 |
(.*?)<\/code>/g, text)
40 | return matches
41 | .map((match) => match.subMatches[0])
42 | .filter((x, i, self) => self.indexOf(x) === i)
43 | .sort()
44 | }
45 |
--------------------------------------------------------------------------------
/generators/generate-webhook-alerts.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 |
3 | const INCLUDE_COMMENTS = true
4 |
5 | const DOCUMENTATION_URLS = [
6 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-created',
7 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-updated',
8 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-cancelled',
9 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-succeeded',
10 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-failed',
11 | 'https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-refunded',
12 | 'https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-succeeded',
13 | 'https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-refunded',
14 | 'https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/order-processing-completed',
15 | 'https://developer.paddle.com/webhook-reference/risk-dispute-alerts/payment-dispute-created',
16 | 'https://developer.paddle.com/webhook-reference/risk-dispute-alerts/payment-dispute-closed',
17 | 'https://developer.paddle.com/webhook-reference/risk-dispute-alerts/high-risk-transaction-created',
18 | 'https://developer.paddle.com/webhook-reference/risk-dispute-alerts/high-risk-transaction-updated',
19 | 'https://developer.paddle.com/webhook-reference/payout-alerts/transfer-created',
20 | 'https://developer.paddle.com/webhook-reference/payout-alerts/transfer-paid',
21 | 'https://developer.paddle.com/webhook-reference/audience-alerts/new-audience-member',
22 | 'https://developer.paddle.com/webhook-reference/audience-alerts/update-audience-member',
23 | 'https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-paid',
24 | 'https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-sent',
25 | 'https://developer.paddle.com/webhook-reference/manual-invoicing-alerts/invoice-overdue',
26 | ]
27 |
28 | interface RouteInfo {
29 | allProps: {
30 | page: {
31 | data: {
32 | blocks: Array<{ type: string; data: T }>
33 | }
34 | }
35 | }
36 | }
37 |
38 | type JsonSchema = {
39 | properties: {
40 | alert_name: { default: string }
41 | [key: string]: JsonSchemaProperty
42 | }
43 | }
44 |
45 | type JsonSchemaProperty = {
46 | description?: string
47 | default?: string
48 | enum?: Array
49 | pattern?: string
50 | }
51 |
52 | run()
53 |
54 | async function run() {
55 | const types = []
56 |
57 | for (const url of DOCUMENTATION_URLS) {
58 | types.push(await buildWebhookAlertType(url))
59 | console.log(`Built type from ${url}`)
60 | }
61 |
62 | const unionType = types.map((x) => x.name).join(' | ')
63 |
64 | const code = `// THIS FILE IS GENERATED AUTOMATICALLY. DO NOT EDIT.
65 |
66 | /** An alert fired by Paddle through a configured webhook */
67 | export type RawPaddleWebhookAlert = ${unionType}
68 |
69 | ${types.map((x) => x.sourceCode).join('\n\n')}`
70 |
71 | fs.writeFileSync(__dirname + '/../src/__generated__/webhook-alerts.ts', code, 'utf-8')
72 | console.log('Written into /src/__generated__/webhook-alerts.ts')
73 | }
74 |
75 | async function buildWebhookAlertType(url: string) {
76 | const routeInfo = await getRouteInfo(url)
77 |
78 | const description = getDescription(routeInfo)
79 | const jsonSchema = getJsonSchema(routeInfo)
80 |
81 | const properties = Object.entries(jsonSchema.properties).map(([propertyName, propertySchema]) => {
82 | const type = inferTypeFromPropertySchema(propertySchema)
83 |
84 | const pattern = 'pattern' in propertySchema ? `\n@pattern ${propertySchema.pattern}` : ''
85 | const comment =
86 | 'description' in propertySchema ? `/** ${propertySchema.description}${pattern} */\n ` : ''
87 | return ` ${INCLUDE_COMMENTS ? comment : ''}${propertyName}: ${type}`
88 | })
89 |
90 | const name = `RawPaddle${toPascalCase(jsonSchema.properties.alert_name.default)}Alert`
91 | const sourceCode = `${INCLUDE_COMMENTS ? `/** ${description} */` : ''}
92 | export type ${name} = {
93 | ${properties.join('\n')}
94 | }`
95 |
96 | return { name, sourceCode }
97 | }
98 |
99 | async function getRouteInfo(url: string) {
100 | const response = await fetch(url)
101 | const text = await response.text()
102 |
103 | const line = text.split('\n').find((x) => x.trim().startsWith('window.__routeInfo ='))
104 |
105 | if (!line) {
106 | throw new Error('Could not find routeInfo line')
107 | }
108 |
109 | const jsonString = line.replace(/^.*window\.__routeInfo = /, '').replace(/;.*$/, '')
110 | return JSON.parse(jsonString)
111 | }
112 |
113 | function getDescription(routeInfo: RouteInfo) {
114 | const textBlock = routeInfo.allProps.page.data.blocks.find((x) => x.type === 'text')
115 |
116 | if (!textBlock) {
117 | throw new Error('Could not find text block for page')
118 | }
119 |
120 | const headingLine = textBlock.data.split('\n').find((x) => x.startsWith('####'))
121 |
122 | if (!headingLine) {
123 | throw new Error('Could not find heading line for page')
124 | }
125 |
126 | return headingLine.replace('#### ', '')
127 | }
128 |
129 | function getJsonSchema(routeInfo: RouteInfo) {
130 | const jsonSchemaBlock = routeInfo.allProps.page.data.blocks.find((x) => x.type === 'jsonSchema')
131 |
132 | if (!jsonSchemaBlock) {
133 | throw new Error('Could not find jsonSchema block for page')
134 | }
135 |
136 | return jsonSchemaBlock.data
137 | }
138 |
139 | function toPascalCase(string: string) {
140 | const camelCase = string.replace(/_(\w)/g, ($, $1) => $1.toUpperCase())
141 | return `${camelCase.charAt(0).toUpperCase()}${camelCase.substr(1)}`
142 | }
143 |
144 | function inferTypeFromPropertySchema(propertySchema: JsonSchemaProperty) {
145 | if (propertySchema.default) {
146 | return `"${propertySchema.default}"`
147 | }
148 |
149 | if (propertySchema.enum) {
150 | return propertySchema.enum.map((x) => `"${x}"`).join(' | ')
151 | }
152 |
153 | return 'string'
154 | }
155 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: { '^.+\\.tsx?$': '@swc/jest' },
3 | coverageProvider: 'v8',
4 | testEnvironment: 'node',
5 | modulePathIgnorePatterns: ['/dist'],
6 | collectCoverageFrom: ['/src/**/*', '!/src/__generated__/**/*'],
7 | coverageThreshold: { global: { branches: 100, functions: 100, lines: 100, statements: 100 } },
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@devoxa/paddle-sdk",
3 | "description": "An SDK to interface with the API and webhooks from paddle.com",
4 | "version": "0.4.3",
5 | "main": "dist/src/index.js",
6 | "license": "MIT",
7 | "repository": {
8 | "url": "https://github.com/devoxa/paddle-sdk"
9 | },
10 | "scripts": {
11 | "test": "jest",
12 | "format": "prettier --ignore-path='.gitignore' --list-different --write .",
13 | "format:check": "prettier --ignore-path='.gitignore' --check .",
14 | "lint": "eslint --ignore-path='.gitignore' '{src,tests}/**/*.ts'",
15 | "build": "rm -rf dist/ && tsc",
16 | "preversion": "yarn build",
17 | "generate": "sh generate.sh"
18 | },
19 | "eslintConfig": {
20 | "extends": "@devoxa",
21 | "rules": {
22 | "@typescript-eslint/camelcase": "off"
23 | }
24 | },
25 | "prettier": "@devoxa/prettier-config",
26 | "dependencies": {
27 | "@devoxa/aes-encryption": "2.0.0",
28 | "dayjs": "1.11.13",
29 | "form-data": "4.0.1",
30 | "php-serialize": "4.1.1"
31 | },
32 | "devDependencies": {
33 | "@devoxa/eslint-config": "3.0.11",
34 | "@devoxa/flocky": "2.2.0",
35 | "@devoxa/prettier-config": "2.0.3",
36 | "@swc/core": "1.9.3",
37 | "@swc/jest": "0.2.37",
38 | "@types/jest": "29.5.14",
39 | "@types/node": "20.9.5",
40 | "eslint": "8.57.1",
41 | "jest": "29.7.0",
42 | "prettier": "3.4.1",
43 | "ts-node": "10.9.2",
44 | "typescript": "5.7.2"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | },
49 | "volta": {
50 | "node": "20.9.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/__generated__/api-routes.ts:
--------------------------------------------------------------------------------
1 | // THIS FILE IS GENERATED AUTOMATICALLY. DO NOT EDIT.
2 |
3 | export const PADDLE_PRODUCT_GENERATE_PAY_LINK = {
4 | method: 'POST' as const,
5 | path: '/2.0/product/generate_pay_link' as const,
6 | }
7 |
8 | export type RawPaddlePostProductGeneratePayLinkRequest = {
9 | /** The Paddle Product ID/Plan ID that you want to base this custom checkout on. Required if not using custom products.
10 |
11 | If no `product_id` is set, custom non-subscription product checkouts can be generated instead by specifying the **required** fields: `title`, `webhook_url` and `prices`. */
12 | product_id?: number
13 | /** The name of the product/title of the checkout. Required if `product_id` is not set. */
14 | title?: string
15 | /** An endpoint that we will call with transaction information upon successful checkout, to allow you to fulfill the purchase.
16 |
17 | Only valid (and required) if `product_id` is not set.
18 |
19 | Note: testing on localhost is not supported. Please use an internet-accessible URL. */
20 | webhook_url?: string
21 | /** Price(s) of the checkout for a one-time purchase or initial payment of a subscription.
22 |
23 | If `product_id` is set, you must also provide the price for the product’s default currency. If a given currency is enabled in the dashboard, it will default to a conversion of the product’s default currency price set in this field unless specified here as well.
24 |
25 | Note: to use the HTTP tester and code generator tool below, pass in each array value on a separate line, for example: *prices[0]:"USD:19.99"*, *prices[1]:"EUR:15.99"* and so on. */
26 | prices?: Array
27 | /** Recurring price(s) of the checkout (excluding the initial payment) only if the `product_id` specified is a subscription. To override the initial payment and all recurring payment amounts, both `prices` and `recurring_prices` must be set.
28 |
29 | You must also provide the price for the subscription’s default currency. If a given currency is enabled in the dashboard, it will default to a conversion of the subscription’s default currency price set in this field unless specified here as well.
30 |
31 | Note: to use the HTTP tester and code generator tool below, pass in each array value on a separate line, for example: *recurring_prices[0]:"USD:19.99"*, *recurring_prices[1]:"EUR:15.99"* and so on. */
32 | recurring_prices?: Array
33 | /** For subscription plans only. The number of days before Paddle starts charging the customer the recurring price. If you leave this field empty, the trial days of the plan will be used. */
34 | trial_days?: number
35 | /** A short message displayed below the product name on the checkout. */
36 | custom_message?: string
37 | /** A coupon to be applied to the checkout. */
38 | coupon_code?: string
39 | /** Specifies if a coupon can be applied to the checkout. "Add Coupon" button on the checkout will be hidden as well if set to `0`. */
40 | discountable?: 0 | 1
41 | /** A URL for the product image/icon displayed on the checkout. */
42 | image_url?: string
43 | /** A URL to redirect to once the checkout is completed. If the variable `{checkout_hash}` is included within the URL (e.g. *https://mysite.com/thanks?checkout={checkout_hash}*), the API will automatically populate the Paddle checkout ID in the redirected URL. */
44 | return_url?: string
45 | /** Specifies if the user is allowed to alter the quantity of the checkout. */
46 | quantity_variable?: 0 | 1
47 | /** Pre-fills the quantity selector on the checkout. Please note that free products/subscription plans are fixed to a quantity of 1. */
48 | quantity?: number
49 | /** Specifies if the checkout link should expire. The generated checkout URL will be accessible until 23:59:59 (UTC) on the date specified (date in format YYYY-MM-DD). */
50 | expires?: string
51 | /** Other Paddle vendor IDs whom you would like to split the funds from this checkout with. */
52 | affiliates?: Array
53 | /** Limit the number of times other Paddle vendors will receive funds from the recurring payments (for subscription products). The initial checkout payment is included in the limit. If you leave this field empty, the limit will not be applied.
Note: if your plan has a trial period, set this to `2` or greater in order for your affiliates to correctly receive their commission on paid payments after the trial. */
54 | recurring_affiliate_limit?: number
55 | /** Whether you have gathered consent to market to the customer. `customer_email` is required if this property is set and you want to opt the customer into marketing. */
56 | marketing_consent?: 0 | 1
57 | /** Pre-fills the customer email field on the checkout. */
58 | customer_email?: string
59 | /** Pre-fills the customer country field on the checkout. See [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries) for the list of supported ISO country codes. */
60 | customer_country?: string
61 | /** Pre-fills the customer postcode field on the checkout.
62 |
63 | This field is required if the `customer_country` requires postcode. See the [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode) for countries requiring this field. */
64 | customer_postcode?: string
65 | /** A string of metadata you wish to store with the checkout. Will be sent alongside all webhooks associated with the order. See the [Pass Parameters](/guides/how-tos/checkout/pass-parameters#sending-additional-user-data) documentation for more information. */
66 | passthrough?: string
67 | /** Pre-fills the sales tax identifier (VAT number) field on the checkout. */
68 | vat_number?: string
69 | /** Pre-fills the Company Name field on the checkout. Required if `vat_number` is set. */
70 | vat_company_name?: string
71 | /** Pre-fills the Street field on the checkout. Required if `vat_number` is set. */
72 | vat_street?: string
73 | /** Pre-fills the Town/City field on the checkout. Required if `vat_number` is set. */
74 | vat_city?: string
75 | /** Pre-fills the State field on the checkout. */
76 | vat_state?: string
77 | /** Pre-fills the Country field on the checkout. Required if `vat_number` is set. See [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries) for the list of supported ISO country codes. */
78 | vat_country?: string
79 | /** Pre-fills the Postcode field on the checkout.
80 |
81 | This field is required if `vat_number` is set ***and*** the `vat_country` requires postcode. See the [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode) for countries requiring this field. */
82 | vat_postcode?: string
83 | }
84 |
85 | export type RawPaddlePostProductGeneratePayLinkResponse = {
86 | url: string
87 | }
88 |
89 | export const PADDLE_SUBSCRIPTION_USERS = {
90 | method: 'POST' as const,
91 | path: '/2.0/subscription/users' as const,
92 | }
93 |
94 | export type RawPaddlePostSubscriptionUsersRequest = {
95 | /** Filter: A specific user subscription ID */
96 | subscription_id?: string
97 | /** Filter: The subscription plan ID */
98 | plan_id?: string
99 | /** Filter: The user subscription status. Returns all `active`, `past_due`, `trialing` and `paused` subscription plans if not specified. A list of possible values and their meanings can be found under [Event Statuses](/reference/platform-parameters/event-statuses). */
100 | state?: 'active' | 'past_due' | 'trialing' | 'paused' | 'deleted'
101 | /** Paginate return results */
102 | page?: number
103 | /** Number of subscription records to return per page. */
104 | results_per_page?: number
105 | }
106 |
107 | export type RawPaddlePostSubscriptionUsersResponse = Array<{
108 | subscription_id: number
109 | plan_id: number
110 | user_id: number
111 | user_email: string
112 | marketing_consent: boolean
113 | state: 'active' | 'past_due' | 'trialing' | 'deleted' | 'paused'
114 | signup_date: string
115 | last_payment: {
116 | amount: number
117 | currency: string
118 | date: string
119 | }
120 | next_payment?: {
121 | amount: number
122 | currency: string
123 | date: string
124 | }
125 | update_url: string
126 | cancel_url: string
127 | paused_at?: string
128 | paused_from?: string
129 | payment_information:
130 | | {
131 | payment_method: 'card'
132 | card_type: string
133 | last_four_digits: string
134 | expiry_date: string
135 | }
136 | | {
137 | payment_method: 'paypal'
138 | }
139 | quantity?: number
140 | }>
141 |
142 | export const PADDLE_SUBSCRIPTION_USERS_UPDATE = {
143 | method: 'POST' as const,
144 | path: '/2.0/subscription/users/update' as const,
145 | }
146 |
147 | export type RawPaddlePostSubscriptionUsersUpdateRequest = {
148 | /** The ID of the subscription you’re updating. */
149 | subscription_id: number
150 | /** The new quantity to be applied to a quantity enabled subscription. */
151 | quantity?: number
152 | /** Optional, but required if setting `recurring_price`. The currency that the recurring price should be charged in. E.g. `USD`, `GBP`, `EUR`, etc. This must be the same as the currency of the existing subscription. */
153 | currency?: string
154 | /** The new recurring price per unit to apply to a quantity enabled subscription. Please note this is a singular price, i.e `11.00`. */
155 | recurring_price?: number
156 | /** If the subscription should bill for the next interval at the revised figures immediately. */
157 | bill_immediately?: boolean
158 | /** The new plan ID to move the subscription to. */
159 | plan_id?: number
160 | /** Whether the change in subscription should be prorated. */
161 | prorate?: boolean
162 | /** Retain the existing modifiers on the user subscription. */
163 | keep_modifiers?: boolean
164 | /** Update the additional data associated with this subscription, like additional features, add-ons and seats. This will be included in all subsequent webhooks, and is often a JSON string of relevant data. */
165 | passthrough?: string
166 | /** Whether a subscription should be paused or restarted. If the subscription is not paused and this is set to `true`, the [subscription status](/reference/platform-parameters/event-statuses) will be changed to "paused" when the subscription's next payment date is reached. */
167 | pause?: boolean
168 | }
169 |
170 | export type RawPaddlePostSubscriptionUsersUpdateResponse = {
171 | subscription_id: number
172 | user_id: number
173 | plan_id: number
174 | next_payment: {
175 | amount: number
176 | currency: string
177 | date: string
178 | }
179 | }
180 |
181 | export const PADDLE_SUBSCRIPTION_USERS_CANCEL = {
182 | method: 'POST' as const,
183 | path: '/2.0/subscription/users_cancel' as const,
184 | }
185 |
186 | export type RawPaddlePostSubscriptionUsersCancelRequest = {
187 | /** The specific user subscription ID. */
188 | subscription_id: number
189 | }
190 |
191 | export type RawPaddlePostSubscriptionUsersCancelResponse = void
192 |
193 | export const PADDLE_SUBSCRIPTION_MODIFIERS_CREATE = {
194 | method: 'POST' as const,
195 | path: '/2.0/subscription/modifiers/create' as const,
196 | }
197 |
198 | export type RawPaddlePostSubscriptionModifiersCreateRequest = {
199 | /** The ID of the subscription that you want to add a modifier for */
200 | subscription_id: number
201 | /** Whether to retain the modifiers on the subscription. By default we retain them, but you can specify this field as false to */
202 | modifier_recurring?: true | false
203 | /** The amount will be in the currency of the subscription. */
204 | modifier_amount: number
205 | /** A description text to be displayed on the buyer's receipt email and invoice. */
206 | modifier_description?: string
207 | }
208 |
209 | export type RawPaddlePostSubscriptionModifiersCreateResponse = {
210 | subscription_id: number
211 | modifier_id: number
212 | }
213 |
--------------------------------------------------------------------------------
/src/__generated__/enums.ts:
--------------------------------------------------------------------------------
1 | // THIS FILE IS GENERATED AUTOMATICALLY. DO NOT EDIT.
2 |
3 | export const RawPaddleEnumCurrencies = {
4 | ARS: 'ARS',
5 | AUD: 'AUD',
6 | BRL: 'BRL',
7 | CAD: 'CAD',
8 | CHF: 'CHF',
9 | CNY: 'CNY',
10 | CZK: 'CZK',
11 | DKK: 'DKK',
12 | EUR: 'EUR',
13 | GBP: 'GBP',
14 | HKD: 'HKD',
15 | HUF: 'HUF',
16 | INR: 'INR',
17 | JPY: 'JPY',
18 | KRW: 'KRW',
19 | MXN: 'MXN',
20 | NOK: 'NOK',
21 | NZD: 'NZD',
22 | PLN: 'PLN',
23 | RUB: 'RUB',
24 | SEK: 'SEK',
25 | SGD: 'SGD',
26 | THB: 'THB',
27 | TWD: 'TWD',
28 | USD: 'USD',
29 | ZAR: 'ZAR',
30 | } as const
31 |
32 | export type RawPaddleEnumCurrencies =
33 | (typeof RawPaddleEnumCurrencies)[keyof typeof RawPaddleEnumCurrencies]
34 |
35 | export const RawPaddleEnumCountries = {
36 | AD: 'AD',
37 | AE: 'AE',
38 | AF: 'AF',
39 | AG: 'AG',
40 | AI: 'AI',
41 | AL: 'AL',
42 | AM: 'AM',
43 | AN: 'AN',
44 | AO: 'AO',
45 | AR: 'AR',
46 | AS: 'AS',
47 | AT: 'AT',
48 | AU: 'AU',
49 | AW: 'AW',
50 | AZ: 'AZ',
51 | BA: 'BA',
52 | BB: 'BB',
53 | BD: 'BD',
54 | BE: 'BE',
55 | BF: 'BF',
56 | BG: 'BG',
57 | BH: 'BH',
58 | BI: 'BI',
59 | BJ: 'BJ',
60 | BM: 'BM',
61 | BN: 'BN',
62 | BO: 'BO',
63 | BR: 'BR',
64 | BS: 'BS',
65 | BT: 'BT',
66 | BV: 'BV',
67 | BW: 'BW',
68 | BY: 'BY',
69 | BZ: 'BZ',
70 | CA: 'CA',
71 | CC: 'CC',
72 | CF: 'CF',
73 | CG: 'CG',
74 | CH: 'CH',
75 | CI: 'CI',
76 | CK: 'CK',
77 | CL: 'CL',
78 | CM: 'CM',
79 | CN: 'CN',
80 | CO: 'CO',
81 | CR: 'CR',
82 | CU: 'CU',
83 | CV: 'CV',
84 | CW: 'CW',
85 | CX: 'CX',
86 | CY: 'CY',
87 | CZ: 'CZ',
88 | DE: 'DE',
89 | DJ: 'DJ',
90 | DK: 'DK',
91 | DM: 'DM',
92 | DO: 'DO',
93 | DZ: 'DZ',
94 | EC: 'EC',
95 | EE: 'EE',
96 | EG: 'EG',
97 | EH: 'EH',
98 | ER: 'ER',
99 | ES: 'ES',
100 | ET: 'ET',
101 | FI: 'FI',
102 | FJ: 'FJ',
103 | FK: 'FK',
104 | FM: 'FM',
105 | FO: 'FO',
106 | FR: 'FR',
107 | GA: 'GA',
108 | GB: 'GB',
109 | GD: 'GD',
110 | GE: 'GE',
111 | GF: 'GF',
112 | GG: 'GG',
113 | GH: 'GH',
114 | GI: 'GI',
115 | GL: 'GL',
116 | GM: 'GM',
117 | GN: 'GN',
118 | GP: 'GP',
119 | GQ: 'GQ',
120 | GR: 'GR',
121 | GS: 'GS',
122 | GT: 'GT',
123 | GU: 'GU',
124 | GW: 'GW',
125 | GY: 'GY',
126 | HK: 'HK',
127 | HM: 'HM',
128 | HN: 'HN',
129 | HR: 'HR',
130 | HT: 'HT',
131 | HU: 'HU',
132 | ID: 'ID',
133 | IE: 'IE',
134 | IL: 'IL',
135 | IN: 'IN',
136 | IO: 'IO',
137 | IQ: 'IQ',
138 | IR: 'IR',
139 | IS: 'IS',
140 | IT: 'IT',
141 | JE: 'JE',
142 | JM: 'JM',
143 | JO: 'JO',
144 | JP: 'JP',
145 | KE: 'KE',
146 | KG: 'KG',
147 | KH: 'KH',
148 | KI: 'KI',
149 | KM: 'KM',
150 | KN: 'KN',
151 | KP: 'KP',
152 | KR: 'KR',
153 | KW: 'KW',
154 | KY: 'KY',
155 | KZ: 'KZ',
156 | LA: 'LA',
157 | LB: 'LB',
158 | LC: 'LC',
159 | LI: 'LI',
160 | LK: 'LK',
161 | LR: 'LR',
162 | LS: 'LS',
163 | LT: 'LT',
164 | LU: 'LU',
165 | LV: 'LV',
166 | LY: 'LY',
167 | MA: 'MA',
168 | MC: 'MC',
169 | MD: 'MD',
170 | ME: 'ME',
171 | MG: 'MG',
172 | MH: 'MH',
173 | MK: 'MK',
174 | ML: 'ML',
175 | MM: 'MM',
176 | MN: 'MN',
177 | MO: 'MO',
178 | MP: 'MP',
179 | MQ: 'MQ',
180 | MR: 'MR',
181 | MS: 'MS',
182 | MT: 'MT',
183 | MU: 'MU',
184 | MV: 'MV',
185 | MW: 'MW',
186 | MX: 'MX',
187 | MY: 'MY',
188 | MZ: 'MZ',
189 | NA: 'NA',
190 | NC: 'NC',
191 | NE: 'NE',
192 | NF: 'NF',
193 | NG: 'NG',
194 | NI: 'NI',
195 | NL: 'NL',
196 | NO: 'NO',
197 | NP: 'NP',
198 | NR: 'NR',
199 | NU: 'NU',
200 | NZ: 'NZ',
201 | OM: 'OM',
202 | PA: 'PA',
203 | PE: 'PE',
204 | PF: 'PF',
205 | PG: 'PG',
206 | PH: 'PH',
207 | PK: 'PK',
208 | PL: 'PL',
209 | PM: 'PM',
210 | PN: 'PN',
211 | PR: 'PR',
212 | PS: 'PS',
213 | PT: 'PT',
214 | PW: 'PW',
215 | PY: 'PY',
216 | QA: 'QA',
217 | RE: 'RE',
218 | RO: 'RO',
219 | RS: 'RS',
220 | RU: 'RU',
221 | RW: 'RW',
222 | SA: 'SA',
223 | SB: 'SB',
224 | SC: 'SC',
225 | SD: 'SD',
226 | SE: 'SE',
227 | SG: 'SG',
228 | SH: 'SH',
229 | SI: 'SI',
230 | SJ: 'SJ',
231 | SK: 'SK',
232 | SL: 'SL',
233 | SM: 'SM',
234 | SN: 'SN',
235 | SO: 'SO',
236 | SR: 'SR',
237 | ST: 'ST',
238 | SV: 'SV',
239 | SY: 'SY',
240 | SZ: 'SZ',
241 | TC: 'TC',
242 | TD: 'TD',
243 | TF: 'TF',
244 | TG: 'TG',
245 | TH: 'TH',
246 | TJ: 'TJ',
247 | TK: 'TK',
248 | TL: 'TL',
249 | TM: 'TM',
250 | TN: 'TN',
251 | TO: 'TO',
252 | TR: 'TR',
253 | TT: 'TT',
254 | TV: 'TV',
255 | TW: 'TW',
256 | TZ: 'TZ',
257 | UA: 'UA',
258 | UG: 'UG',
259 | UM: 'UM',
260 | US: 'US',
261 | UY: 'UY',
262 | UZ: 'UZ',
263 | VA: 'VA',
264 | VC: 'VC',
265 | VE: 'VE',
266 | VG: 'VG',
267 | VI: 'VI',
268 | VN: 'VN',
269 | VU: 'VU',
270 | WF: 'WF',
271 | WS: 'WS',
272 | YE: 'YE',
273 | YT: 'YT',
274 | ZA: 'ZA',
275 | ZM: 'ZM',
276 | ZW: 'ZW',
277 | } as const
278 |
279 | export type RawPaddleEnumCountries =
280 | (typeof RawPaddleEnumCountries)[keyof typeof RawPaddleEnumCountries]
281 |
--------------------------------------------------------------------------------
/src/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class PaddleSdkException extends Error {}
2 |
3 | export class PaddleSdkApiException extends Error {}
4 |
--------------------------------------------------------------------------------
/src/helpers/converters.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import customParseFormat from 'dayjs/plugin/customParseFormat'
3 | import utc from 'dayjs/plugin/utc'
4 | import {
5 | RawPaddlePaymentRefundedAlert,
6 | RawPaddleSubscriptionCreatedAlert,
7 | RawPaddleSubscriptionPaymentSucceededAlert,
8 | RawPaddleSubscriptionUpdatedAlert,
9 | } from '../__generated__/webhook-alerts'
10 | import {
11 | PaddleSdkCardBrand,
12 | PaddleSdkCountry,
13 | PaddleSdkCurrency,
14 | PaddleSdkPausedReason,
15 | PaddleSdkPaymentMethod,
16 | PaddleSdkRefundType,
17 | PaddleSdkSubscriptionStatus,
18 | } from '../interfaces'
19 |
20 | dayjs.extend(customParseFormat)
21 | dayjs.extend(utc)
22 |
23 | export function convertApiInteger(integerString: string): number {
24 | return parseInt(integerString, 10)
25 | }
26 |
27 | export function convertApiFloat(floatString: string): number {
28 | return parseFloat(floatString)
29 | }
30 |
31 | export function convertApiBoolean(booleanString: '0' | '1' | 'false' | 'true'): boolean {
32 | return booleanString === '1' || booleanString === 'true'
33 | }
34 |
35 | export function convertSdkBoolean(boolean: boolean): 0 | 1 {
36 | return boolean ? 1 : 0
37 | }
38 |
39 | export function convertApiDate(
40 | dateString: string,
41 | type: 'DATE' | 'DATE_TIME' | 'EXPIRY_DATE'
42 | ): Date {
43 | switch (type) {
44 | case 'DATE':
45 | return dayjs.utc(dateString, 'YYYY-MM-DD').toDate()
46 | case 'DATE_TIME':
47 | return dayjs.utc(dateString, 'YYYY-MM-DD HH:mm:ss').toDate()
48 | case 'EXPIRY_DATE':
49 | return dayjs.utc(dateString, 'MM/YYYY').toDate()
50 | }
51 | }
52 |
53 | export function convertSdkDate(date: Date, type: 'DATE' | 'DATE_TIME' | 'EXPIRY_DATE'): string {
54 | switch (type) {
55 | case 'DATE':
56 | return dayjs.utc(date).format('YYYY-MM-DD')
57 | case 'DATE_TIME':
58 | return dayjs.utc(date).format('YYYY-MM-DD HH:mm:ss')
59 | case 'EXPIRY_DATE':
60 | return dayjs.utc(date).format('MM/YYYY')
61 | }
62 | }
63 |
64 | export function convertApiSubscriptionStatus(
65 | subscriptionStatus: RawPaddleSubscriptionCreatedAlert['status']
66 | ): PaddleSdkSubscriptionStatus {
67 | switch (subscriptionStatus) {
68 | case 'active':
69 | return PaddleSdkSubscriptionStatus.ACTIVE
70 | case 'trialing':
71 | return PaddleSdkSubscriptionStatus.TRIALING
72 | case 'past_due':
73 | return PaddleSdkSubscriptionStatus.PAST_DUE
74 | case 'paused':
75 | return PaddleSdkSubscriptionStatus.PAUSED
76 | case 'deleted':
77 | return PaddleSdkSubscriptionStatus.CANCELLED
78 | }
79 | }
80 |
81 | export function convertSdkSubscriptionStatus(
82 | subscriptionStatus: PaddleSdkSubscriptionStatus
83 | ): RawPaddleSubscriptionCreatedAlert['status'] {
84 | switch (subscriptionStatus) {
85 | case PaddleSdkSubscriptionStatus.ACTIVE:
86 | return 'active'
87 | case PaddleSdkSubscriptionStatus.TRIALING:
88 | return 'trialing'
89 | case PaddleSdkSubscriptionStatus.PAST_DUE:
90 | return 'past_due'
91 | case PaddleSdkSubscriptionStatus.PAUSED:
92 | return 'paused'
93 | case PaddleSdkSubscriptionStatus.CANCELLED:
94 | return 'deleted'
95 | }
96 | }
97 |
98 | export function convertApiPausedReason(
99 | pausedReason: Exclude
100 | ): PaddleSdkPausedReason {
101 | switch (pausedReason) {
102 | case 'delinquent':
103 | return PaddleSdkPausedReason.DELINQUENT
104 | case 'voluntary':
105 | return PaddleSdkPausedReason.VOLUNTARY
106 | }
107 | }
108 |
109 | export function convertApiCurrency(currency: string): PaddleSdkCurrency {
110 | // These are the currencies already returned by paddle, we just make them type-safe
111 | return currency as PaddleSdkCurrency
112 | }
113 |
114 | export function convertApiCountry(country: string): PaddleSdkCountry {
115 | // These are the countries already returned by paddle, we just make them type-safe
116 | return country as PaddleSdkCountry
117 | }
118 |
119 | export function convertApiPaymentMethod(
120 | paymentMethod: RawPaddleSubscriptionPaymentSucceededAlert['payment_method']
121 | ): PaddleSdkPaymentMethod {
122 | switch (paymentMethod) {
123 | case 'card':
124 | return PaddleSdkPaymentMethod.CARD
125 | case 'paypal':
126 | return PaddleSdkPaymentMethod.PAYPAL
127 | case 'apple-pay':
128 | return PaddleSdkPaymentMethod.APPLE_PAY
129 | case 'wire-transfer':
130 | return PaddleSdkPaymentMethod.WIRE_TRANSFER
131 | case 'free':
132 | return PaddleSdkPaymentMethod.FREE
133 | }
134 | }
135 |
136 | export function convertApiCardBrand(cardBrand: string): PaddleSdkCardBrand {
137 | switch (cardBrand) {
138 | case 'visa':
139 | return PaddleSdkCardBrand.VISA
140 | case 'american_express':
141 | return PaddleSdkCardBrand.AMERICAN_EXPRESS
142 | case 'discover':
143 | return PaddleSdkCardBrand.DISCOVER
144 | case 'jcb':
145 | return PaddleSdkCardBrand.JCB
146 | case 'elo':
147 | return PaddleSdkCardBrand.ELO
148 | case 'master':
149 | case 'mastercard':
150 | return PaddleSdkCardBrand.MASTERCARD
151 | case 'maestro':
152 | return PaddleSdkCardBrand.MAESTRO
153 | case 'diners_club':
154 | return PaddleSdkCardBrand.DINERS_CLUB
155 | }
156 |
157 | return PaddleSdkCardBrand.UNKNOWN
158 | }
159 |
160 | export function convertApiRefundType(
161 | refundType: RawPaddlePaymentRefundedAlert['refund_type']
162 | ): PaddleSdkRefundType {
163 | switch (refundType) {
164 | case 'full':
165 | return PaddleSdkRefundType.FULL
166 | case 'vat':
167 | return PaddleSdkRefundType.VAT
168 | case 'partial':
169 | return PaddleSdkRefundType.PARTIAL
170 | }
171 | }
172 |
173 | export function convertSdkPriceList(
174 | currencyList: Array<[PaddleSdkCurrency, number]>
175 | ): Array {
176 | return currencyList.map(([currency, amount]) => `${currency}:${amount}`)
177 | }
178 |
--------------------------------------------------------------------------------
/src/helpers/fetch.ts:
--------------------------------------------------------------------------------
1 | import FormData from 'form-data'
2 |
3 | type Body = Record
4 |
5 | export async function fetch(url: string, options: { method: string; body: Body }): Promise {
6 | const response = await global.fetch(url, {
7 | method: options.method,
8 | body: objectToFormData(options.body),
9 | })
10 |
11 | return response.json() as T
12 | }
13 |
14 | function objectToFormData(object: Body): FormData {
15 | const formData = new FormData()
16 |
17 | Object.entries(object).forEach(([key, value]) => {
18 | if (typeof value === 'undefined') return
19 |
20 | formData.append(key, value.toString())
21 | })
22 |
23 | return formData
24 | }
25 |
--------------------------------------------------------------------------------
/src/helpers/stableSerialize.ts:
--------------------------------------------------------------------------------
1 | import { serialize as phpSerialize } from 'php-serialize'
2 |
3 | export function stableSerialize(object: Record): string {
4 | // 1) Sort the object alphabetically by it's keys
5 | object = sortByKey(object)
6 |
7 | // 2) Encode arrays in their string form: `[1, 2, 3]` -> `'1, 2, 3'`
8 | // 3) Encode any non-strings as their JSON stringified version: `3` -> `'3'`
9 | const encodedObject: Record = {}
10 | for (const property in object) {
11 | const value = object[property]
12 |
13 | if (Array.isArray(value)) {
14 | encodedObject[property] = value.join(', ')
15 | } else if (typeof value !== 'string') {
16 | encodedObject[property] = JSON.stringify(value)
17 | } else {
18 | encodedObject[property] = value
19 | }
20 | }
21 |
22 | // 4) Serialize in the way that PHP would
23 | return phpSerialize(encodedObject)
24 | }
25 |
26 | function sortByKey(object: Record) {
27 | const keys = Object.keys(object).sort()
28 |
29 | const sortedObject: Record = {}
30 | for (const i in keys) {
31 | sortedObject[keys[i]] = object[keys[i]]
32 | }
33 |
34 | return sortedObject
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createVerify } from 'crypto'
2 | import {
3 | PADDLE_PRODUCT_GENERATE_PAY_LINK,
4 | PADDLE_SUBSCRIPTION_MODIFIERS_CREATE,
5 | PADDLE_SUBSCRIPTION_USERS,
6 | PADDLE_SUBSCRIPTION_USERS_CANCEL,
7 | PADDLE_SUBSCRIPTION_USERS_UPDATE,
8 | RawPaddlePostProductGeneratePayLinkRequest,
9 | RawPaddlePostProductGeneratePayLinkResponse,
10 | RawPaddlePostSubscriptionModifiersCreateRequest,
11 | RawPaddlePostSubscriptionModifiersCreateResponse,
12 | RawPaddlePostSubscriptionUsersCancelRequest,
13 | RawPaddlePostSubscriptionUsersCancelResponse,
14 | RawPaddlePostSubscriptionUsersRequest,
15 | RawPaddlePostSubscriptionUsersResponse,
16 | RawPaddlePostSubscriptionUsersUpdateRequest,
17 | RawPaddlePostSubscriptionUsersUpdateResponse,
18 | } from './__generated__/api-routes'
19 | import {
20 | RawPaddlePaymentRefundedAlert,
21 | RawPaddlePaymentSucceededAlert,
22 | RawPaddleSubscriptionCancelledAlert,
23 | RawPaddleSubscriptionCreatedAlert,
24 | RawPaddleSubscriptionPaymentSucceededAlert,
25 | RawPaddleSubscriptionUpdatedAlert,
26 | RawPaddleWebhookAlert,
27 | } from './__generated__/webhook-alerts'
28 | import { PaddleSdkApiException, PaddleSdkException } from './exceptions'
29 | import {
30 | convertApiBoolean,
31 | convertApiCardBrand,
32 | convertApiCountry,
33 | convertApiCurrency,
34 | convertApiDate,
35 | convertApiFloat,
36 | convertApiInteger,
37 | convertApiPausedReason,
38 | convertApiPaymentMethod,
39 | convertApiRefundType,
40 | convertApiSubscriptionStatus,
41 | convertSdkBoolean,
42 | convertSdkDate,
43 | convertSdkPriceList,
44 | convertSdkSubscriptionStatus,
45 | } from './helpers/converters'
46 | import { fetch } from './helpers/fetch'
47 | import { stableSerialize } from './helpers/stableSerialize'
48 | import {
49 | PaddleSdkCancelSubscriptionRequest,
50 | PaddleSdkCancelSubscriptionResponse,
51 | PaddleSdkCreateProductPayLinkRequest,
52 | PaddleSdkCreateProductPayLinkResponse,
53 | PaddleSdkCreateSubscriptionModifierRequest,
54 | PaddleSdkCreateSubscriptionModifierResponse,
55 | PaddleSdkListSubscriptionsRequest,
56 | PaddleSdkListSubscriptionsResponse,
57 | PaddleSdkPaymentMethod,
58 | PaddleSdkPaymentRefundedEvent,
59 | PaddleSdkPaymentSucceededEvent,
60 | PaddleSdkSubscriptionCancelledEvent,
61 | PaddleSdkSubscriptionCreatedEvent,
62 | PaddleSdkSubscriptionPaymentSucceededEvent,
63 | PaddleSdkSubscriptionUpdatedEvent,
64 | PaddleSdkUpdateSubscriptionRequest,
65 | PaddleSdkUpdateSubscriptionResponse,
66 | PaddleSdkWebhookEventType,
67 | } from './interfaces'
68 | import { MetadataCodec } from './metadata'
69 |
70 | export * from './exceptions'
71 | export * from './interfaces'
72 | export * from './metadata'
73 |
74 | export interface PaddleSdkOptions {
75 | /**
76 | * The base URL of the paddle API
77 | * @default https://vendors.paddle.com/api
78 | */
79 | readonly baseUrl?: string
80 |
81 | /** Public key from the paddle dashboard to validate webhook requests */
82 | readonly publicKey: string
83 |
84 | /** Vendor ID from the paddle dashboard to authenticate API requests */
85 | readonly vendorId: number
86 |
87 | /** Vendor auth code from the paddle dashboard to authenticate API requests */
88 | readonly vendorAuthCode: string
89 |
90 | /**
91 | * Metadata codec encodes and decodes additional pass-through data for an order
92 | *
93 | * @see https://developer.paddle.com/guides/how-tos/checkout/pass-parameters
94 | */
95 | readonly metadataCodec: MetadataCodec
96 | }
97 |
98 | export class PaddleSdk {
99 | private readonly baseUrl: string
100 | private readonly publicKey: string
101 | private readonly vendorId: number
102 | private readonly vendorAuthCode: string
103 | private readonly metadataCodec: MetadataCodec
104 |
105 | constructor(options: PaddleSdkOptions) {
106 | if (!options.publicKey) {
107 | throw new PaddleSdkException('PaddleSdk was called without a publicKey')
108 | }
109 | if (!options.vendorId) {
110 | throw new PaddleSdkException('PaddleSdk was called without a vendorId')
111 | }
112 | if (!options.vendorAuthCode) {
113 | throw new PaddleSdkException('PaddleSdk was called without a vendorAuthCode')
114 | }
115 | if (!options.metadataCodec) {
116 | throw new PaddleSdkException('PaddleSdk was called without a metadataCodec')
117 | }
118 |
119 | this.baseUrl = options.baseUrl || 'https://vendors.paddle.com/api'
120 | this.publicKey = options.publicKey
121 | this.vendorId = options.vendorId
122 | this.vendorAuthCode = options.vendorAuthCode
123 | this.metadataCodec = options.metadataCodec
124 | }
125 |
126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
127 | verifyWebhookEvent(body: any): body is RawPaddleWebhookAlert {
128 | if (typeof body !== 'object') return false
129 |
130 | const { p_signature: signature, ...postBodyRest } = body || {}
131 | if (!signature || typeof signature !== 'string') return false
132 |
133 | const serializedPostBody = stableSerialize(postBodyRest)
134 |
135 | const verifier = createVerify('sha1')
136 | verifier.update(serializedPostBody)
137 | verifier.end()
138 |
139 | return verifier.verify(this.publicKey, signature, 'base64')
140 | }
141 |
142 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
143 | parseWebhookEvent(body: any) {
144 | if (!this.verifyWebhookEvent(body)) {
145 | throw new PaddleSdkException('Failed validating webhook event body')
146 | }
147 |
148 | switch (body.alert_name) {
149 | case 'payment_succeeded':
150 | return this.parsePaymentSucceededWebhookEvent(body)
151 | case 'payment_refunded':
152 | return this.parsePaymentRefundedWebhookEvent(body)
153 | case 'subscription_created':
154 | return this.parseSubscriptionCreatedWebhookEvent(body)
155 | case 'subscription_updated':
156 | return this.parseSubscriptionUpdatedWebhookEvent(body)
157 | case 'subscription_cancelled':
158 | return this.parseSubscriptionCancelledWebhookEvent(body)
159 | case 'subscription_payment_succeeded':
160 | return this.parseSubscriptionPaymentSucceededWebhookEvent(body)
161 | }
162 |
163 | throw new PaddleSdkException(
164 | `Implementation missing: Can not parse event of type ${body.alert_name}`
165 | )
166 | }
167 |
168 | private stringifyMetadata(value: TMetadata): string {
169 | return this.metadataCodec.stringify(value)
170 | }
171 |
172 | private parseMetadata(value: string): TMetadata {
173 | return this.metadataCodec.parse(value)
174 | }
175 |
176 | private parsePaymentSucceededWebhookEvent(
177 | body: RawPaddlePaymentSucceededAlert
178 | ): PaddleSdkPaymentSucceededEvent {
179 | return {
180 | // EVENT ---
181 |
182 | eventType: PaddleSdkWebhookEventType.PAYMENT_SUCCEEDED,
183 | eventId: convertApiInteger(body.alert_id),
184 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
185 |
186 | // ORDER ---
187 |
188 | metadata: this.parseMetadata(body.passthrough),
189 | orderId: body.order_id,
190 | checkoutId: body.checkout_id,
191 | coupon: body.coupon,
192 | receiptUrl: body.receipt_url,
193 | productId: convertApiInteger(body.product_id),
194 | productName: body.product_name,
195 | quantity: convertApiInteger(body.quantity),
196 | paymentMethod: convertApiPaymentMethod(body.payment_method),
197 | currency: convertApiCurrency(body.currency),
198 | gross: convertApiFloat(body.sale_gross),
199 | tax: convertApiFloat(body.payment_tax),
200 | fee: convertApiFloat(body.fee),
201 | earnings: convertApiFloat(body.earnings),
202 | usedPriceOverride: convertApiBoolean(body.used_price_override),
203 |
204 | // CUSTOMER ---
205 |
206 | customerName: body.customer_name,
207 | customerEmail: body.email,
208 | customerCountry: convertApiCountry(body.country),
209 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
210 |
211 | // BALANCE ---
212 |
213 | balanceCurrency: convertApiCurrency(body.balance_currency),
214 | balanceGross: convertApiFloat(body.balance_gross),
215 | balanceTax: convertApiFloat(body.balance_tax),
216 | balanceFee: convertApiFloat(body.balance_fee),
217 | balanceEarnings: convertApiFloat(body.balance_earnings),
218 | }
219 | }
220 |
221 | private parsePaymentRefundedWebhookEvent(
222 | body: RawPaddlePaymentRefundedAlert
223 | ): PaddleSdkPaymentRefundedEvent {
224 | return {
225 | // EVENT ---
226 |
227 | eventType: PaddleSdkWebhookEventType.PAYMENT_REFUNDED,
228 | eventId: convertApiInteger(body.alert_id),
229 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
230 |
231 | // ORDER ---
232 |
233 | metadata: this.parseMetadata(body.passthrough),
234 | orderId: body.order_id,
235 | checkoutId: body.checkout_id,
236 | refundType: convertApiRefundType(body.refund_type),
237 | refundReason: body.refund_reason,
238 | quantity: convertApiInteger(body.quantity),
239 | currency: convertApiCurrency(body.currency),
240 | amount: convertApiFloat(body.amount),
241 | taxRefund: convertApiFloat(body.tax_refund),
242 | feeRefund: convertApiFloat(body.fee_refund),
243 | grossRefund: convertApiFloat(body.gross_refund),
244 | earningsDecrease: convertApiFloat(body.earnings_decrease),
245 |
246 | // CUSTOMER ---
247 |
248 | customerEmail: body.email,
249 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
250 |
251 | // BALANCE ---
252 |
253 | balanceCurrency: convertApiCurrency(body.balance_currency),
254 | balanceGrossRefund: convertApiFloat(body.balance_gross_refund),
255 | balanceTaxRefund: convertApiFloat(body.balance_tax_refund),
256 | balanceFeeRefund: convertApiFloat(body.balance_fee_refund),
257 | balanceEarningsDecrease: convertApiFloat(body.balance_earnings_decrease),
258 | }
259 | }
260 |
261 | private parseSubscriptionCreatedWebhookEvent(
262 | body: RawPaddleSubscriptionCreatedAlert
263 | ): PaddleSdkSubscriptionCreatedEvent {
264 | const quantity = convertApiInteger(body.quantity)
265 | const unitPrice = convertApiFloat(body.unit_price)
266 |
267 | return {
268 | eventId: convertApiInteger(body.alert_id),
269 | eventType: PaddleSdkWebhookEventType.SUBSCRIPTION_CREATED,
270 | cancelUrl: body.cancel_url,
271 | checkoutId: body.checkout_id,
272 | currency: convertApiCurrency(body.currency),
273 | customerEmail: body.email,
274 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
275 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
276 | nextPaymentDate: convertApiDate(body.next_bill_date, 'DATE'),
277 | metadata: this.parseMetadata(body.passthrough),
278 | quantity,
279 | referrerUrl: body.source,
280 | status: convertApiSubscriptionStatus(body.status),
281 | subscriptionId: convertApiInteger(body.subscription_id),
282 | productId: convertApiInteger(body.subscription_plan_id),
283 | unitPrice,
284 | price: quantity * unitPrice,
285 | updateUrl: body.update_url,
286 | customerId: convertApiInteger(body.user_id),
287 | }
288 | }
289 |
290 | private parseSubscriptionUpdatedWebhookEvent(
291 | body: RawPaddleSubscriptionUpdatedAlert
292 | ): PaddleSdkSubscriptionUpdatedEvent {
293 | return {
294 | eventId: convertApiInteger(body.alert_id),
295 | eventType: PaddleSdkWebhookEventType.SUBSCRIPTION_UPDATED,
296 | cancelUrl: body.cancel_url,
297 | checkoutId: body.checkout_id,
298 | currency: convertApiCurrency(body.currency),
299 | customerEmail: body.email,
300 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
301 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
302 | nextPaymentDate: convertApiDate(body.next_bill_date, 'DATE'),
303 | price: convertApiFloat(body.new_price),
304 | quantity: convertApiInteger(body.new_quantity),
305 | status: convertApiSubscriptionStatus(body.status),
306 | productId: convertApiInteger(body.subscription_plan_id),
307 | unitPrice: convertApiFloat(body.new_unit_price),
308 | oldNextPaymentDate: convertApiDate(body.old_next_bill_date, 'DATE'),
309 | oldPrice: convertApiFloat(body.old_price),
310 | oldQuantity: convertApiInteger(body.old_quantity),
311 | oldStatus: convertApiSubscriptionStatus(body.old_status),
312 | oldProductId: convertApiInteger(body.old_subscription_plan_id),
313 | oldUnitPrice: convertApiFloat(body.old_unit_price),
314 | metadata: this.parseMetadata(body.passthrough),
315 | pausedAt: body.paused_at ? convertApiDate(body.paused_at, 'DATE_TIME') : null,
316 | pausedFrom: body.paused_from ? convertApiDate(body.paused_from, 'DATE_TIME') : null,
317 | pausedReason: body.paused_reason ? convertApiPausedReason(body.paused_reason) : null,
318 | subscriptionId: convertApiInteger(body.subscription_id),
319 | updateUrl: body.update_url,
320 | customerId: convertApiInteger(body.user_id),
321 | }
322 | }
323 |
324 | private parseSubscriptionCancelledWebhookEvent(
325 | body: RawPaddleSubscriptionCancelledAlert
326 | ): PaddleSdkSubscriptionCancelledEvent {
327 | const quantity = convertApiInteger(body.quantity)
328 | const unitPrice = convertApiFloat(body.unit_price)
329 |
330 | return {
331 | eventId: convertApiInteger(body.alert_id),
332 | eventType: PaddleSdkWebhookEventType.SUBSCRIPTION_CANCELLED,
333 | cancelledFrom: convertApiDate(body.cancellation_effective_date, 'DATE'),
334 | checkoutId: body.checkout_id,
335 | currency: convertApiCurrency(body.currency),
336 | customerEmail: body.email,
337 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
338 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
339 | metadata: this.parseMetadata(body.passthrough),
340 | quantity,
341 | status: convertApiSubscriptionStatus(body.status),
342 | subscriptionId: convertApiInteger(body.subscription_id),
343 | productId: convertApiInteger(body.subscription_plan_id),
344 | unitPrice,
345 | price: quantity * unitPrice,
346 | customerId: convertApiInteger(body.user_id),
347 | }
348 | }
349 |
350 | private parseSubscriptionPaymentSucceededWebhookEvent(
351 | body: RawPaddleSubscriptionPaymentSucceededAlert
352 | ): PaddleSdkSubscriptionPaymentSucceededEvent {
353 | const quantity = convertApiInteger(body.quantity)
354 | const unitPrice = convertApiFloat(body.unit_price)
355 |
356 | return {
357 | eventId: convertApiInteger(body.alert_id),
358 | eventType: PaddleSdkWebhookEventType.SUBSCRIPTION_PAYMENT_SUCCEEDED,
359 | balanceCurrency: convertApiCurrency(body.balance_currency),
360 | balanceEarnings: convertApiFloat(body.balance_earnings),
361 | balanceFee: convertApiFloat(body.balance_fee),
362 | balanceGross: convertApiFloat(body.balance_gross),
363 | balanceTax: convertApiFloat(body.balance_tax),
364 | checkoutId: body.checkout_id,
365 | customerCountry: convertApiCountry(body.country),
366 | coupon: body.coupon,
367 | currency: convertApiCurrency(body.currency),
368 | customerName: body.customer_name,
369 | earnings: convertApiFloat(body.earnings),
370 | customerEmail: body.email,
371 | eventTime: convertApiDate(body.event_time, 'DATE_TIME'),
372 | fee: convertApiFloat(body.fee),
373 | isInitialPayment: convertApiBoolean(body.initial_payment),
374 | installments: convertApiInteger(body.instalments),
375 | hasMarketingConsent: convertApiBoolean(body.marketing_consent),
376 | nextPaymentDate: convertApiDate(body.next_bill_date, 'DATE'),
377 | nextPaymentAmount: convertApiFloat(body.next_payment_amount),
378 | orderId: body.order_id,
379 | metadata: this.parseMetadata(body.passthrough),
380 | paymentMethod: convertApiPaymentMethod(body.payment_method),
381 | tax: convertApiFloat(body.payment_tax),
382 | quantity,
383 | receiptUrl: body.receipt_url,
384 | gross: convertApiFloat(body.sale_gross),
385 | status: convertApiSubscriptionStatus(body.status),
386 | subscriptionId: convertApiInteger(body.subscription_id),
387 | subscriptionPaymentId: convertApiInteger(body.subscription_payment_id),
388 | productId: convertApiInteger(body.subscription_plan_id),
389 | unitPrice,
390 | price: quantity * unitPrice,
391 | customerId: convertApiInteger(body.user_id),
392 | }
393 | }
394 |
395 | private async apiRequest(
396 | path: string,
397 | method: 'GET' | 'POST',
398 | body: TRequest
399 | ): Promise {
400 | const url = this.baseUrl + path
401 |
402 | const json = await fetch<
403 | | {
404 | success: true
405 | response: any // eslint-disable-line @typescript-eslint/no-explicit-any
406 | }
407 | | {
408 | success: false
409 | error: { message: string }
410 | }
411 | >(url, {
412 | method,
413 | body: {
414 | vendor_id: this.vendorId,
415 | vendor_auth_code: this.vendorAuthCode,
416 | ...body,
417 | },
418 | })
419 |
420 | // Turn errors from the Paddle API into a unique exception with the error message
421 | if (!json.success) {
422 | throw new PaddleSdkApiException(json.error.message)
423 | }
424 |
425 | return json.response
426 | }
427 |
428 | async createProductPayLink(
429 | data: PaddleSdkCreateProductPayLinkRequest
430 | ): Promise {
431 | const convertProductPayLinkRequest = (
432 | request: PaddleSdkCreateProductPayLinkRequest
433 | ): RawPaddlePostProductGeneratePayLinkRequest => {
434 | return {
435 | product_id: request.productId,
436 | title: request.title,
437 | webhook_url: request.webhookUrl,
438 | prices: request.prices ? convertSdkPriceList(request.prices) : undefined,
439 | recurring_prices: request.recurringPrices
440 | ? convertSdkPriceList(request.recurringPrices)
441 | : undefined,
442 | trial_days: request.trialDays,
443 | custom_message: request.customMessage,
444 | coupon_code: request.populateCoupon,
445 | discountable:
446 | typeof request.isDiscountable !== 'undefined'
447 | ? convertSdkBoolean(request.isDiscountable)
448 | : undefined,
449 | image_url: request.imageUrl,
450 | return_url: request.returnUrl,
451 | quantity_variable:
452 | typeof request.isQuantityVariable !== 'undefined'
453 | ? convertSdkBoolean(request.isQuantityVariable)
454 | : undefined,
455 | quantity: request.populateQuantity,
456 | expires: request.expirationDate
457 | ? convertSdkDate(request.expirationDate, 'DATE')
458 | : undefined,
459 | affiliates: request.affiliates?.map((x) => x.toString()),
460 | recurring_affiliate_limit: request.recurringAffiliateLimit,
461 | marketing_consent:
462 | typeof request.populateHasMarketingConsent !== 'undefined'
463 | ? convertSdkBoolean(request.populateHasMarketingConsent)
464 | : undefined,
465 | customer_email: request.populateCustomerEmail,
466 | customer_country: request.populateCustomerCountry,
467 | customer_postcode: request.populateCustomerPostcode,
468 | vat_number: request.populateVatNumber,
469 | vat_company_name: request.populateVatCompanyName,
470 | vat_street: request.populateVatStreet,
471 | vat_city: request.populateVatCity,
472 | vat_state: request.populateVatState,
473 | vat_country: request.populateVatCountry,
474 | vat_postcode: request.populateVatPostcode,
475 | passthrough: request.metadata ? this.stringifyMetadata(request.metadata) : undefined,
476 | }
477 | }
478 |
479 | return this.apiRequest<
480 | RawPaddlePostProductGeneratePayLinkRequest,
481 | RawPaddlePostProductGeneratePayLinkResponse
482 | >(
483 | PADDLE_PRODUCT_GENERATE_PAY_LINK.path,
484 | PADDLE_PRODUCT_GENERATE_PAY_LINK.method,
485 | convertProductPayLinkRequest(data)
486 | )
487 | }
488 |
489 | async listSubscriptions(
490 | data: PaddleSdkListSubscriptionsRequest
491 | ): Promise {
492 | const convertListSubscriptionsRequest = (
493 | request: PaddleSdkListSubscriptionsRequest
494 | ): RawPaddlePostSubscriptionUsersRequest => {
495 | return {
496 | subscription_id: request.subscriptionId?.toString(),
497 | plan_id: request.productId?.toString(),
498 | state: request.status ? convertSdkSubscriptionStatus(request.status) : undefined,
499 | page: request.page,
500 | results_per_page: request.resultsPerPage,
501 | }
502 | }
503 |
504 | const convertPaymentInformation = (
505 | paymentInformation: RawPaddlePostSubscriptionUsersResponse[0]['payment_information']
506 | ) => {
507 | if (paymentInformation.payment_method === 'card') {
508 | return {
509 | paymentMethod: PaddleSdkPaymentMethod.CARD,
510 | cardBrand: convertApiCardBrand(paymentInformation.card_type),
511 | cardLastFour: paymentInformation.last_four_digits,
512 | cardExpirationDate: convertApiDate(paymentInformation.expiry_date, 'EXPIRY_DATE'),
513 | }
514 | }
515 |
516 | // istanbul ignore else
517 | if (paymentInformation.payment_method === 'paypal') {
518 | return {
519 | paymentMethod: PaddleSdkPaymentMethod.PAYPAL,
520 | cardBrand: null,
521 | cardLastFour: null,
522 | cardExpirationDate: null,
523 | }
524 | }
525 |
526 | // @ts-expect-error TS errors here because we handled all types that should exist according to the API docs
527 | throw new PaddleSdkException(`Unknown payment method "${paymentInformation.payment_method}"`)
528 | }
529 |
530 | const convertListSubscriptionsResponseElement = (
531 | subscription: RawPaddlePostSubscriptionUsersResponse[0]
532 | ) => {
533 | return {
534 | subscriptionId: subscription.subscription_id,
535 | productId: subscription.plan_id,
536 | customerId: subscription.user_id,
537 | customerEmail: subscription.user_email,
538 | hasMarketingConsent: subscription.marketing_consent,
539 | status: convertApiSubscriptionStatus(subscription.state),
540 | quantity: subscription.quantity || 1,
541 | signupDate: convertApiDate(subscription.signup_date, 'DATE_TIME'),
542 | updateUrl: subscription.update_url,
543 | cancelUrl: subscription.cancel_url,
544 | pausedAt: subscription.paused_at
545 | ? convertApiDate(subscription.paused_at, 'DATE_TIME')
546 | : null,
547 | pausedFrom: subscription.paused_from
548 | ? convertApiDate(subscription.paused_from, 'DATE_TIME')
549 | : null,
550 | lastPaymentAmount: subscription.last_payment.amount,
551 | lastPaymentCurrency: convertApiCurrency(subscription.last_payment.currency),
552 | lastPaymentDate: convertApiDate(subscription.last_payment.date, 'DATE'),
553 | nextPaymentAmount: subscription.next_payment ? subscription.next_payment.amount : null,
554 | nextPaymentCurrency: subscription.next_payment
555 | ? convertApiCurrency(subscription.next_payment.currency)
556 | : null,
557 | nextPaymentDate: subscription.next_payment
558 | ? convertApiDate(subscription.next_payment.date, 'DATE')
559 | : null,
560 | ...convertPaymentInformation(subscription.payment_information),
561 | }
562 | }
563 |
564 | return this.apiRequest<
565 | RawPaddlePostSubscriptionUsersRequest,
566 | RawPaddlePostSubscriptionUsersResponse
567 | >(
568 | PADDLE_SUBSCRIPTION_USERS.path,
569 | PADDLE_SUBSCRIPTION_USERS.method,
570 | convertListSubscriptionsRequest(data)
571 | ).then((subscriptions) => subscriptions.map(convertListSubscriptionsResponseElement))
572 | }
573 |
574 | async updateSubscription(
575 | data: PaddleSdkUpdateSubscriptionRequest
576 | ): Promise {
577 | const convertUpdateSubscriptionRequest = (
578 | request: PaddleSdkUpdateSubscriptionRequest
579 | ): RawPaddlePostSubscriptionUsersUpdateRequest => {
580 | return {
581 | subscription_id: request.subscriptionId,
582 | quantity: request.quantity,
583 | currency: request.currency,
584 | recurring_price: request.unitPrice,
585 | bill_immediately: request.shouldMakeImmediatePayment,
586 | plan_id: request.productId,
587 | prorate: request.shouldProrate,
588 | keep_modifiers: request.shouldKeepModifiers,
589 | passthrough: request.metadata ? this.stringifyMetadata(request.metadata) : undefined,
590 | pause: request.shouldPause,
591 | }
592 | }
593 |
594 | const convertUpdateSubscriptionResponse = (
595 | response: RawPaddlePostSubscriptionUsersUpdateResponse
596 | ): PaddleSdkUpdateSubscriptionResponse => {
597 | return {
598 | subscriptionId: response.subscription_id,
599 | customerId: response.user_id,
600 | productId: response.plan_id,
601 | nextPaymentAmount: response.next_payment.amount,
602 | nextPaymentCurrency: convertApiCurrency(response.next_payment.currency),
603 | nextPaymentDate: convertApiDate(response.next_payment.date, 'DATE'),
604 | }
605 | }
606 |
607 | return this.apiRequest<
608 | RawPaddlePostSubscriptionUsersUpdateRequest,
609 | RawPaddlePostSubscriptionUsersUpdateResponse
610 | >(
611 | PADDLE_SUBSCRIPTION_USERS_UPDATE.path,
612 | PADDLE_SUBSCRIPTION_USERS_UPDATE.method,
613 | convertUpdateSubscriptionRequest(data)
614 | ).then((x) => convertUpdateSubscriptionResponse(x))
615 | }
616 |
617 | async cancelSubscription(
618 | data: PaddleSdkCancelSubscriptionRequest
619 | ): Promise {
620 | const convertCancelSubscriptionRequest = (
621 | request: PaddleSdkCancelSubscriptionRequest
622 | ): RawPaddlePostSubscriptionUsersCancelRequest => {
623 | return {
624 | subscription_id: request.subscriptionId,
625 | }
626 | }
627 |
628 | return this.apiRequest<
629 | RawPaddlePostSubscriptionUsersCancelRequest,
630 | RawPaddlePostSubscriptionUsersCancelResponse
631 | >(
632 | PADDLE_SUBSCRIPTION_USERS_CANCEL.path,
633 | PADDLE_SUBSCRIPTION_USERS_CANCEL.method,
634 | convertCancelSubscriptionRequest(data)
635 | )
636 | }
637 |
638 | async createSubscriptionModifier(
639 | data: PaddleSdkCreateSubscriptionModifierRequest
640 | ): Promise {
641 | const convertCreateSubscriptionModifierRequest = (
642 | request: PaddleSdkCreateSubscriptionModifierRequest
643 | ): RawPaddlePostSubscriptionModifiersCreateRequest => {
644 | return {
645 | subscription_id: request.subscriptionId,
646 | modifier_recurring: request.isRecurring,
647 | modifier_amount: request.amount,
648 | modifier_description: request.description,
649 | }
650 | }
651 |
652 | const convertCreateSubscriptionModifierResponse = (
653 | response: RawPaddlePostSubscriptionModifiersCreateResponse
654 | ): PaddleSdkCreateSubscriptionModifierResponse => {
655 | return {
656 | subscriptionId: response.subscription_id,
657 | modifierId: response.modifier_id,
658 | }
659 | }
660 |
661 | return this.apiRequest<
662 | RawPaddlePostSubscriptionModifiersCreateRequest,
663 | RawPaddlePostSubscriptionModifiersCreateResponse
664 | >(
665 | PADDLE_SUBSCRIPTION_MODIFIERS_CREATE.path,
666 | PADDLE_SUBSCRIPTION_MODIFIERS_CREATE.method,
667 | convertCreateSubscriptionModifierRequest(data)
668 | ).then((response) => convertCreateSubscriptionModifierResponse(response))
669 | }
670 | }
671 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RawPaddleEnumCountries as PaddleSdkCountry,
3 | RawPaddleEnumCurrencies as PaddleSdkCurrency,
4 | } from './__generated__/enums'
5 |
6 | // ----------------------------------------------------------------------------
7 | // ENUMS
8 | // ----------------------------------------------------------------------------
9 |
10 | /** A type of a webhook event */
11 | export const PaddleSdkWebhookEventType = {
12 | PAYMENT_SUCCEEDED: 'PAYMENT_SUCCEEDED',
13 | PAYMENT_REFUNDED: 'PAYMENT_REFUNDED',
14 | SUBSCRIPTION_CREATED: 'SUBSCRIPTION_CREATED',
15 | SUBSCRIPTION_UPDATED: 'SUBSCRIPTION_UPDATED',
16 | SUBSCRIPTION_CANCELLED: 'SUBSCRIPTION_CANCELLED',
17 | SUBSCRIPTION_PAYMENT_SUCCEEDED: 'SUBSCRIPTION_PAYMENT_SUCCEEDED',
18 | } as const
19 |
20 | export type PaddleSdkWebhookEventType =
21 | (typeof PaddleSdkWebhookEventType)[keyof typeof PaddleSdkWebhookEventType]
22 |
23 | /**
24 | * A status of a subscription
25 | *
26 | * - ACTIVE: Indicates an active subscription, payments are up-to-date.
27 | * - TRIALING: Indicates the subscription is in the trial period, will change to ACTIVE once the first recurring payment is successfully completed.
28 | * - PAST_DUE: Indicates a payment for this subscription has failed. The payment will be retried and the status will change to ACTIVE, PAUSED or CANCELLED depending on your dunning settings.
29 | * - PAUSED: Indicates that this subscription has been paused. The customer will not be charged for subsequent payments. The status will change to ACTIVE once the subscription is restarted.
30 | * - CANCELLED: Indicates that this subscription has been cancelled.
31 | */
32 | export const PaddleSdkSubscriptionStatus = {
33 | ACTIVE: 'ACTIVE',
34 | TRIALING: 'TRIALING',
35 | PAST_DUE: 'PAST_DUE',
36 | PAUSED: 'PAUSED',
37 | CANCELLED: 'CANCELLED',
38 | } as const
39 |
40 | export type PaddleSdkSubscriptionStatus =
41 | (typeof PaddleSdkSubscriptionStatus)[keyof typeof PaddleSdkSubscriptionStatus]
42 |
43 | /** A three-letter ISO currency code */
44 | export { PaddleSdkCurrency }
45 | /** A two letter ISO country code */
46 | export { PaddleSdkCountry }
47 |
48 | /**
49 | * A reason why a subscription was paused
50 | *
51 | * - DELINQUENT: The payment failed and the rule specified in the dunning settings was to pause the subscription.
52 | * - VOLUNTARY: The subscription was paused via the API.
53 | */
54 | export const PaddleSdkPausedReason = {
55 | DELINQUENT: 'DELINQUENT',
56 | VOLUNTARY: 'VOLUNTARY',
57 | } as const
58 |
59 | export type PaddleSdkPausedReason =
60 | (typeof PaddleSdkPausedReason)[keyof typeof PaddleSdkPausedReason]
61 |
62 | /** A payment method used to make a transaction */
63 | export const PaddleSdkPaymentMethod = {
64 | CARD: 'CARD',
65 | PAYPAL: 'PAYPAL',
66 | APPLE_PAY: 'APPLE_PAY',
67 | WIRE_TRANSFER: 'WIRE_TRANSFER',
68 | FREE: 'FREE',
69 | } as const
70 |
71 | export type PaddleSdkPaymentMethod =
72 | (typeof PaddleSdkPaymentMethod)[keyof typeof PaddleSdkPaymentMethod]
73 |
74 | /** A brand of card used to make a transaction */
75 | export const PaddleSdkCardBrand = {
76 | AMERICAN_EXPRESS: 'AMERICAN_EXPRESS',
77 | DINERS_CLUB: 'DINERS_CLUB',
78 | DISCOVER: 'DISCOVER',
79 | ELO: 'ELO',
80 | JCB: 'JCB',
81 | MAESTRO: 'MAESTRO',
82 | MASTERCARD: 'MASTERCARD',
83 | VISA: 'VISA',
84 | UNKNOWN: 'UNKNOWN',
85 | } as const
86 |
87 | export type PaddleSdkCardBrand = (typeof PaddleSdkCardBrand)[keyof typeof PaddleSdkCardBrand]
88 |
89 | /** Refund type */
90 | export const PaddleSdkRefundType = {
91 | FULL: 'FULL',
92 | VAT: 'VAT',
93 | PARTIAL: 'PARTIAL',
94 | } as const
95 |
96 | export type PaddleSdkRefundType = (typeof PaddleSdkRefundType)[keyof typeof PaddleSdkRefundType]
97 |
98 | // ----------------------------------------------------------------------------
99 | // WEBHOOKS
100 | // ----------------------------------------------------------------------------
101 |
102 | /** An event fired when a one-off purchase payment is made */
103 | export type PaddleSdkPaymentSucceededEvent = {
104 | // EVENT ---
105 |
106 | /** The type of this event */
107 | eventType: typeof PaddleSdkWebhookEventType.PAYMENT_SUCCEEDED
108 |
109 | /** The unique ID for this event */
110 | eventId: number
111 |
112 | /** The date and time the event was fired */
113 | eventTime: Date
114 |
115 | // ORDER ---
116 |
117 | /** The value passed into the pay link / set via the API using the `metadata` parameter */
118 | metadata: TMetadata
119 |
120 | /** The unique order ID for this payment */
121 | orderId: string
122 |
123 | /** The unique checkout ID of the order */
124 | checkoutId: string
125 |
126 | /** The coupon code used on this order */
127 | coupon: string
128 |
129 | /** The URL containing the customer receipt for this order */
130 | receiptUrl: string
131 |
132 | /** The dashboard ID of the product purchased in this order */
133 | productId: number
134 |
135 | /** The name of the product included in the order */
136 | productName: string
137 |
138 | /** The number of products or in this order */
139 | quantity: number
140 |
141 | /** The payment method used to make the transaction */
142 | paymentMethod: PaddleSdkPaymentMethod
143 |
144 | /** The currency of the order */
145 | currency: PaddleSdkCurrency
146 |
147 | /** The total amount the customer was charged for this payment */
148 | gross: number
149 |
150 | /** The amount of tax paid for this payment */
151 | tax: number
152 |
153 | /** The amount of fees paid for this payment */
154 | fee: number
155 |
156 | /** The total amount (after taxes and fees) earned from this payment */
157 | earnings: number
158 |
159 | /** Whether the dashboard price was overridden */
160 | usedPriceOverride: boolean
161 |
162 | // CUSTOMER ---
163 |
164 | /** The name of the customer */
165 | customerName: string
166 |
167 | /** The email address of the customer */
168 | customerEmail: string
169 |
170 | /** The country of the customer */
171 | customerCountry: PaddleSdkCountry
172 |
173 | /** Whether the customer has agreed to receive marketing messages */
174 | hasMarketingConsent: boolean
175 |
176 | // BALANCE ---
177 |
178 | /** The currency of the vendor */
179 | balanceCurrency: PaddleSdkCurrency
180 |
181 | /** The total amount received from the customer (in the vendor's currency) */
182 | balanceGross: number
183 |
184 | /** The amount of tax received from the customer (in the vendor's currency) */
185 | balanceTax: number
186 |
187 | /** The amount of fees taken from the vendor (in the vendor's currency) */
188 | balanceFee: number
189 |
190 | /** The amount earned from this payment (in the vendor's currency) */
191 | balanceEarnings: number
192 | }
193 |
194 | /** An event fired when a one-off purchase payment is refunded */
195 | export type PaddleSdkPaymentRefundedEvent = {
196 | // EVENT ---
197 |
198 | /** The type of this event */
199 | eventType: typeof PaddleSdkWebhookEventType.PAYMENT_REFUNDED
200 |
201 | /** The unique ID for this event */
202 | eventId: number
203 |
204 | /** The date and time the event was fired */
205 | eventTime: Date
206 |
207 | // ORDER ---
208 |
209 | /** The value passed into the pay link / set via the API using the `metadata` parameter */
210 | metadata: TMetadata
211 |
212 | /** The unique order ID for this payment */
213 | orderId: string
214 |
215 | /** The unique checkout ID of the order */
216 | checkoutId: string
217 |
218 | /** The type of refund */
219 | refundType: PaddleSdkRefundType
220 |
221 | /** Refund reason note */
222 | refundReason: string
223 |
224 | /** The number of products or subscription seats sold in the transaction */
225 | quantity: number
226 |
227 | /** The currency of the order */
228 | currency: PaddleSdkCurrency
229 |
230 | /** The amount refunded, partial refunds are possible */
231 | amount: number
232 |
233 | /** The amount of tax returned to the customer, in the currency of the original transaction */
234 | taxRefund: number
235 |
236 | /** The fee amount returned to the vendor, in the currency of the original transaction */
237 | feeRefund: number
238 |
239 | /** The total amount returned to the customer as a result of this refund, in the currency of the original transaction */
240 | grossRefund: number
241 |
242 | /** The amount of revenue taken from the vendor’s earnings as a result of this refund, in the currency of the original transaction */
243 | earningsDecrease: number
244 |
245 | // CUSTOMER ---
246 |
247 | /** The email address of the customer */
248 | customerEmail: string
249 |
250 | /** Whether the customer has agreed to receive marketing messages */
251 | hasMarketingConsent: boolean
252 |
253 | // BALANCE ---
254 |
255 | /** The currency of the vendor */
256 | balanceCurrency: PaddleSdkCurrency
257 |
258 | /** The total amount returned to the customer as a result of this refund, in the vendor’s default currency at the time of the transaction */
259 | balanceGrossRefund: number
260 |
261 | /** The amount of tax returned to the customer, in the vendor’s default currency at the time of the transaction */
262 | balanceTaxRefund: number
263 |
264 | /** The fee amount returned to the vendor, in the vendor’s default currency at the time of the transaction */
265 | balanceFeeRefund: number
266 |
267 | /** The amount of revenue taken from the vendor’s earnings as a result of this refund, in the vendor’s default currency at the time of the transaction */
268 | balanceEarningsDecrease: number
269 | }
270 |
271 | /** An event fired when a subscription is created */
272 | export type PaddleSdkSubscriptionCreatedEvent = {
273 | // EVENT ---
274 |
275 | /** The type of this event */
276 | eventType: typeof PaddleSdkWebhookEventType.SUBSCRIPTION_CREATED
277 |
278 | /** The unique ID for this event */
279 | eventId: number
280 |
281 | /** The date and time the event was fired */
282 | eventTime: Date
283 |
284 | // ORDER ---
285 |
286 | /** The value passed into the pay link using the `metadata` parameter */
287 | metadata: TMetadata
288 |
289 | /** The unique checkout ID of the order */
290 | checkoutId: string
291 |
292 | /** The currency of the order */
293 | currency: PaddleSdkCurrency
294 |
295 | /** The referrer URL(s) from where the order originated from */
296 | referrerUrl: string
297 |
298 | // SUBSCRIPTION ---
299 |
300 | /** The unique ID of the subscription */
301 | subscriptionId: number
302 |
303 | /** The ID of the product the subscription is for */
304 | productId: number
305 |
306 | /** The status of the subscription */
307 | status: PaddleSdkSubscriptionStatus
308 |
309 | /** The number of subscription seats */
310 | quantity: number
311 |
312 | /** The price per seat of the subscription */
313 | unitPrice: number
314 |
315 | /** The total price of the subscription */
316 | price: number
317 |
318 | /** The date the next payment is due for the subscription */
319 | nextPaymentDate: Date
320 |
321 | /** The URL of the "Update Billing Information" page for the subscription */
322 | updateUrl: string
323 |
324 | /** The URL of the "Cancellation" page for the subscription */
325 | cancelUrl: string
326 |
327 | // CUSTOMER ---
328 |
329 | /** The unique ID of the customer */
330 | customerId: number
331 |
332 | /** The email address of the customer */
333 | customerEmail: string
334 |
335 | /** Whether the customer has agreed to receive marketing messages */
336 | hasMarketingConsent: boolean
337 | }
338 |
339 | /** An event fired when a subscription is updated */
340 | export type PaddleSdkSubscriptionUpdatedEvent = {
341 | // EVENT ---
342 |
343 | /** The type of this event */
344 | eventType: typeof PaddleSdkWebhookEventType.SUBSCRIPTION_UPDATED
345 |
346 | /** The unique ID for this event */
347 | eventId: number
348 |
349 | /** The date and time the event was fired */
350 | eventTime: Date
351 |
352 | // ORDER ---
353 |
354 | /** The value passed into the pay link / set via the API using the `metadata` parameter */
355 | metadata: TMetadata
356 |
357 | /** The unique checkout ID of the order */
358 | checkoutId: string
359 |
360 | /** The currency of the order */
361 | currency: PaddleSdkCurrency
362 |
363 | // SUBSCRIPTION ---
364 |
365 | /** The unique ID of the subscription */
366 | subscriptionId: number
367 |
368 | /** The old ID of the product the subscription was for */
369 | oldProductId: number
370 |
371 | /** The ID of the product the subscription is for */
372 | productId: number
373 |
374 | /** The old status of the subscription */
375 | oldStatus: PaddleSdkSubscriptionStatus
376 |
377 | /** The status of the subscription */
378 | status: PaddleSdkSubscriptionStatus
379 |
380 | /** The old number of subscription seats */
381 | oldQuantity: number
382 |
383 | /** The number of subscription seats */
384 | quantity: number
385 |
386 | /** The old price per seat of the subscription */
387 | oldUnitPrice: number
388 |
389 | /** The price per seat of the subscription */
390 | unitPrice: number
391 |
392 | /** The old total price of the subscription */
393 | oldPrice: number
394 |
395 | /** The total price of the subscription */
396 | price: number
397 |
398 | /** The old date the next payment was due for the subscription */
399 | oldNextPaymentDate: Date
400 |
401 | /** The date the next payment is due for the subscription */
402 | nextPaymentDate: Date
403 |
404 | /** The URL of the "Update Billing Information" page for the subscription */
405 | updateUrl: string
406 |
407 | /** The URL of the "Cancellation" page for the subscription */
408 | cancelUrl: string
409 |
410 | /** The date and time when the subscription was requested to be paused */
411 | pausedAt: Date | null
412 |
413 | /**
414 | * The date the pause comes into effect, taking the customer’s balance into account.
415 | * The customer should be able to use the service they've subscribed to up until this date.
416 | */
417 | pausedFrom: Date | null
418 |
419 | /** The reason why the subscription is paused */
420 | pausedReason: PaddleSdkPausedReason | null
421 |
422 | // CUSTOMER ---
423 |
424 | /** The unique ID of the customer */
425 | customerId: number
426 |
427 | /** The email address of the customer */
428 | customerEmail: string
429 |
430 | /** Whether the customer has agreed to receive marketing messages */
431 | hasMarketingConsent: boolean
432 | }
433 |
434 | /** An event fired when a subscription is cancelled */
435 | export type PaddleSdkSubscriptionCancelledEvent = {
436 | // EVENT ---
437 |
438 | /** The type of this event */
439 | eventType: typeof PaddleSdkWebhookEventType.SUBSCRIPTION_CANCELLED
440 |
441 | /** The unique ID for this event */
442 | eventId: number
443 |
444 | /** The date and time the event was fired */
445 | eventTime: Date
446 |
447 | // ORDER ---
448 |
449 | /** The value passed into the pay link / set via the API using the `metadata` parameter */
450 | metadata: TMetadata
451 |
452 | /** The unique checkout ID of the order */
453 | checkoutId: string
454 |
455 | /** The currency of the order */
456 | currency: PaddleSdkCurrency
457 |
458 | // SUBSCRIPTION ---
459 |
460 | /** The unique ID of the subscription */
461 | subscriptionId: number
462 |
463 | /** The ID of the product the subscription is for */
464 | productId: number
465 |
466 | /** The status of the subscription */
467 | status: PaddleSdkSubscriptionStatus
468 |
469 | /** The number of subscription seats */
470 | quantity: number
471 |
472 | /** The price per seat of the subscription */
473 | unitPrice: number
474 |
475 | /** The total price of the subscription */
476 | price: number
477 |
478 | /**
479 | * The date the cancellation comes into effect, taking the customer’s balance into account.
480 | * The customer should be able to use the service they've subscribed to up until this date.
481 | */
482 | cancelledFrom: Date
483 |
484 | // CUSTOMER ---
485 |
486 | /** The unique ID of the customer */
487 | customerId: number
488 |
489 | /** The email address of the customer */
490 | customerEmail: string
491 |
492 | /** Whether the customer has agreed to receive marketing messages */
493 | hasMarketingConsent: boolean
494 | }
495 |
496 | /**
497 | * An event fired when a subscription payment is made
498 | * Both the normal recurring subscription payment as well as extra charges trigger this event
499 | */
500 | export type PaddleSdkSubscriptionPaymentSucceededEvent = {
501 | // EVENT ---
502 |
503 | /** The type of this event */
504 | eventType: typeof PaddleSdkWebhookEventType.SUBSCRIPTION_PAYMENT_SUCCEEDED
505 |
506 | /** The unique ID for this event */
507 | eventId: number
508 |
509 | /** The date and time the event was fired */
510 | eventTime: Date
511 |
512 | // ORDER ---
513 |
514 | /** The value passed into the pay link / set via the API using the `metadata` parameter */
515 | metadata: TMetadata
516 |
517 | /** The unique order ID for this payment */
518 | orderId: string
519 |
520 | /** The unique checkout ID of the order */
521 | checkoutId: string
522 |
523 | /** The coupon code used on this order */
524 | coupon: string
525 |
526 | /** The URL containing the customer receipt for this order */
527 | receiptUrl: string
528 |
529 | /** Whether this is the customer’s first payment for this subscription */
530 | isInitialPayment: boolean
531 |
532 | /** The number of payments made to date */
533 | installments: number
534 |
535 | /** The payment method used to make the transaction */
536 | paymentMethod: PaddleSdkPaymentMethod
537 |
538 | /** The currency of the order */
539 | currency: PaddleSdkCurrency
540 |
541 | /** The total amount the customer was charged for this payment */
542 | gross: number
543 |
544 | /** The amount of tax paid for this payment */
545 | tax: number
546 |
547 | /** The amount of fees paid for this payment */
548 | fee: number
549 |
550 | /** The total amount (after taxes and fees) earned from this payment */
551 | earnings: number
552 |
553 | // SUBSCRIPTION ---
554 |
555 | /** The unique ID of the subscription */
556 | subscriptionId: number
557 |
558 | /** The unique ID of the subscription payment */
559 | subscriptionPaymentId: number
560 |
561 | /** The ID of the product the subscription is for */
562 | productId: number
563 |
564 | /** The status of the subscription */
565 | status: PaddleSdkSubscriptionStatus
566 |
567 | /** The number of subscription seats */
568 | quantity: number
569 |
570 | /** The price per seat of the subscription */
571 | unitPrice: number
572 |
573 | /** The total price of the subscription */
574 | price: number
575 |
576 | /** The date the next payment is due for the subscription */
577 | nextPaymentDate: Date
578 |
579 | /** The total amount charged for the next payment of the subscription */
580 | nextPaymentAmount: number
581 |
582 | // CUSTOMER ---
583 |
584 | /** The unique ID of the customer */
585 | customerId: number
586 |
587 | /** The name of the customer */
588 | customerName: string
589 |
590 | /** The email address of the customer */
591 | customerEmail: string
592 |
593 | /** The country of the customer */
594 | customerCountry: PaddleSdkCountry
595 |
596 | /** Whether the customer has agreed to receive marketing messages */
597 | hasMarketingConsent: boolean
598 |
599 | // BALANCE ---
600 |
601 | /** The currency of the vendor */
602 | balanceCurrency: PaddleSdkCurrency
603 |
604 | /** The total amount received from the customer (in the vendor's currency) */
605 | balanceGross: number
606 |
607 | /** The amount of tax received from the customer (in the vendor's currency) */
608 | balanceTax: number
609 |
610 | /** The amount of fees taken from the vendor (in the vendor's currency) */
611 | balanceFee: number
612 |
613 | /** The amount earned from this payment (in the vendor's currency) */
614 | balanceEarnings: number
615 | }
616 |
617 | // ----------------------------------------------------------------------------
618 | // API REQUESTS
619 | // ----------------------------------------------------------------------------
620 |
621 | /** The API request parameters for creating a product pay link */
622 | export type PaddleSdkCreateProductPayLinkRequest = {
623 | /** The ID of the product to base the pay link on */
624 | productId?: number
625 |
626 | /**
627 | * The metadata stored with the checkout, will be sent with all events associated with the order
628 | * This field is used to link payments/subscriptions to existing application entities
629 | */
630 | metadata?: TMetadata
631 |
632 | // CUSTOM PRODUCT ---
633 |
634 | /** The name of the product / title of the checkout, required if `productId` is not set */
635 | title?: string
636 |
637 | /** The short message displayed below the product name on the checkout */
638 | customMessage?: string
639 |
640 | /** The URL for the product image displayed on the checkout */
641 | imageUrl?: string
642 |
643 | /** The URL called with events upon successful checkout, only valid and required if `productId` is not set */
644 | webhookUrl?: string
645 |
646 | /**
647 | * The price(s) of the checkout for a one-time purchase or initial payment of a subscription.
648 | *
649 | * If `productId` is set, you must provide the price for the product’s default currency. If a
650 | * given currency is enabled in the dashboard, it will default to a conversion of the product’s
651 | * default currency price set in this field unless specified here as well.
652 | */
653 | prices?: Array<[PaddleSdkCurrency, number]>
654 |
655 | /**
656 | * The recurring price(s) of the checkout (excluding the initial payment), only valid if the `productId`
657 | * specified is a subscription.
658 | *
659 | * You must provide the price for the subscription’s default currency. If a given currency is enabled
660 | * in the dashboard, it will default to a conversion of the subscription’s default currency
661 | * price set in this field unless specified here as well.
662 | *
663 | * To override the initial payment and all recurring payment amounts, both `prices` and
664 | * `recurringPrices` must be set.
665 | */
666 | recurringPrices?: Array<[PaddleSdkCurrency, number]>
667 |
668 | /** The number of days before charging the customer the recurring price, only valid for subscriptions */
669 | trialDays?: number
670 |
671 | /** Whether a coupon can be applied to the checkout */
672 | isDiscountable?: boolean
673 |
674 | /** The URL to redirect to once the checkout is completed */
675 | returnUrl?: string
676 |
677 | /** Whether the customer is allowed to alter the quantity of the checkout */
678 | isQuantityVariable?: boolean
679 |
680 | /** The expiration date of the checkout link should expire */
681 | expirationDate?: Date
682 |
683 | /** The other vendor IDs whom you would like to split the funds from this checkout with */
684 | affiliates?: Array
685 |
686 | /**
687 | * The number of times other vendors will receive funds from the recurring payments for subscription products
688 | *
689 | * The initial checkout payment is included in the limit. If this field is not set, a limit will not be applied.
690 | * If your product has a trial period, set this to `2` or greater in order for your affiliates to correctly receive
691 | * their commission on payments after the trial.
692 | */
693 | recurringAffiliateLimit?: number
694 |
695 | // POPULATE CHECKOUT ---
696 |
697 | /**
698 | * Populates the quantity selector on the checkout
699 | * Free products & subscription products are fixed to a quantity of 1
700 | */
701 | populateQuantity?: number
702 |
703 | /** Populates the "Coupon" field on the checkout */
704 | populateCoupon?: string
705 |
706 | /** Populates whether the customer has agreed to receive marketing messages */
707 | populateHasMarketingConsent?: boolean
708 |
709 | /** Populates the "Email" field on the checkout, required if `populateHasMarketingConsent` if set */
710 | populateCustomerEmail?: string
711 |
712 | /** Populates the "Country" field on the checkout */
713 | populateCustomerCountry?: PaddleSdkCountry
714 |
715 | /**
716 | * Populates the "Postcode" field on the checkout, required if the `populateCustomerCountry` requires a postcode
717 | *
718 | * See the [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode) for countries requiring this field.
719 | */
720 | populateCustomerPostcode?: string
721 |
722 | /** Populates the "VAT Number" field on the checkout */
723 | populateVatNumber?: string
724 |
725 | /** Populates the "VAT Company Name" field on the checkout, required if `populateVatNumber` is set */
726 | populateVatCompanyName?: string
727 |
728 | /** Populates the "VAT Street" field on the checkout, required if `populateVatNumber` is set */
729 | populateVatStreet?: string
730 |
731 | /** Populates the "VAT Town/City" field on the checkout, required if `populateVatNumber` is set */
732 | populateVatCity?: string
733 |
734 | /** Populates the "VAT State" field on the checkout */
735 | populateVatState?: string
736 |
737 | /** Populates the "VAT Country" field on the checkout, required if `populateVatNumber` is set */
738 | populateVatCountry?: PaddleSdkCountry
739 |
740 | /**
741 | * Populates the "VAT Postcode" field on the checkout, required if `populateVatNumber` is set and
742 | * the `populateVatCountry` requires a postcode
743 | *
744 | * See the [Supported Countries](https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode) for countries requiring this field.
745 | */
746 | populateVatPostcode?: string
747 | }
748 |
749 | /** The API response for creating a product pay link */
750 | export type PaddleSdkCreateProductPayLinkResponse = {
751 | /** The generated product pay link URL */
752 | url: string
753 | }
754 |
755 | /** The API request parameters for listing subscriptions */
756 | export type PaddleSdkListSubscriptionsRequest = {
757 | /** Filter by the unique ID of the subscription */
758 | subscriptionId?: number
759 |
760 | /** Filter by the ID of the product the subscription is for */
761 | productId?: number
762 |
763 | /** Filter by the subscription status */
764 | status?: PaddleSdkSubscriptionStatus
765 |
766 | /** The requested page of the result set */
767 | page?: number
768 |
769 | /** The number of records to return per page */
770 | resultsPerPage?: number
771 | }
772 |
773 | /** The API response for listing subscriptions */
774 | export type PaddleSdkListSubscriptionsResponse = Array<{
775 | // ORDER ---
776 |
777 | /** The payment method used to make the transaction */
778 | paymentMethod: typeof PaddleSdkPaymentMethod.CARD | typeof PaddleSdkPaymentMethod.PAYPAL
779 |
780 | /** The brand of the card, set if `paymentMethod` is "CARD" */
781 | cardBrand: PaddleSdkCardBrand | null
782 |
783 | /** The last four digits of the card, set if `paymentMethod` is "CARD" */
784 | cardLastFour: string | null
785 |
786 | /** The expiration date of the card, set if `paymentMethod` is "CARD" */
787 | cardExpirationDate: Date | null
788 |
789 | // SUBSCRIPTION ---
790 |
791 | /** The unique ID of the subscription */
792 | subscriptionId: number
793 |
794 | /** The ID of the product the subscription is for */
795 | productId: number
796 |
797 | /** The status of the subscription */
798 | status: PaddleSdkSubscriptionStatus
799 |
800 | /** The number of subscription seats */
801 | quantity: number
802 |
803 | /** The date and time the subscription was created */
804 | signupDate: Date
805 |
806 | /** The date the last payment was due for the subscription */
807 | lastPaymentDate: Date
808 |
809 | /** The currency of the last payment of the subscription */
810 | lastPaymentCurrency: PaddleSdkCurrency
811 |
812 | /** The total amount charged for the last payment of the subscription */
813 | lastPaymentAmount: number
814 |
815 | /** The date the next payment is due for the subscription */
816 | nextPaymentDate: Date | null
817 |
818 | /** The currency of the next payment of the subscription */
819 | nextPaymentCurrency: PaddleSdkCurrency | null
820 |
821 | /** The total amount charged for the next payment of the subscription */
822 | nextPaymentAmount: number | null
823 |
824 | /** The URL of the "Update Billing Information" page for the subscription */
825 | updateUrl: string
826 |
827 | /** The URL of the "Cancellation" page for the subscription */
828 | cancelUrl: string
829 |
830 | /** The date and time when the subscription was requested to be paused */
831 | pausedAt: Date | null
832 |
833 | /**
834 | * The date the pause comes into effect, taking the customer’s balance into account.
835 | * The customer should be able to use the service they've subscribed to up until this date.
836 | */
837 | pausedFrom: Date | null
838 |
839 | // CUSTOMER ---
840 |
841 | /** The unique ID of the customer */
842 | customerId: number
843 |
844 | /** The email address of the customer */
845 | customerEmail: string
846 |
847 | /** Whether the customer has agreed to receive marketing messages */
848 | hasMarketingConsent: boolean
849 | }>
850 |
851 | /** The API request parameters for updating a subscription */
852 | export type PaddleSdkUpdateSubscriptionRequest = {
853 | /** The unique ID of the subscription to be updated */
854 | subscriptionId: number
855 |
856 | /** The new ID of the product to move the subscription to */
857 | productId?: number
858 |
859 | /** The new number of subscription seats, only valid for quantity enabled subscriptions */
860 | quantity?: number
861 |
862 | /** The new price per unit to apply to a quantity enabled subscription */
863 | unitPrice?: number
864 |
865 | /**
866 | * The currency of the unit price, required if `unitPrice` is set
867 | * This must be the same as the currency of the existing subscription.
868 | */
869 | currency?: PaddleSdkCurrency
870 |
871 | /** Whether the subscription should make a payment for the next interval immediately */
872 | shouldMakeImmediatePayment?: boolean
873 |
874 | /** Whether the change in subscription should be prorated */
875 | shouldProrate?: boolean
876 |
877 | /** Whether to keep the existing modifiers on the subscription */
878 | shouldKeepModifiers?: boolean
879 |
880 | /**
881 | * The metadata data stored with the checkout, will be sent with all events associated with the order
882 | * This field is used to link payments/subscriptions to existing application entities
883 | */
884 | metadata?: TMetadata
885 |
886 | /**
887 | * Whether a subscription should be paused (true) or restarted (false)
888 | *
889 | * If the subscription is paused, the status will be changed to "PAUSED" when the subscription's
890 | * next payment date is reached.
891 | */
892 | shouldPause?: boolean
893 | }
894 |
895 | /** The API response for updating a subscription */
896 | export type PaddleSdkUpdateSubscriptionResponse = {
897 | /** The unique ID of the subscription */
898 | subscriptionId: number
899 |
900 | /** The unique ID of the customer */
901 | customerId: number
902 |
903 | /** The ID of the product the subscription is for */
904 | productId: number
905 |
906 | /** The date the next payment is due for the subscription */
907 | nextPaymentDate: Date | null
908 |
909 | /** The currency of the next payment of the subscription */
910 | nextPaymentCurrency: PaddleSdkCurrency | null
911 |
912 | /** The total amount charged for the next payment of the subscription */
913 | nextPaymentAmount: number | null
914 | }
915 |
916 | /** The API request parameters for creating a subscription modifier */
917 | export type PaddleSdkCreateSubscriptionModifierRequest = {
918 | /** The unique ID of the subscription to add a modifier for */
919 | subscriptionId: number
920 |
921 | /** Whether this modifier will be added to all future subscription payments */
922 | isRecurring: boolean
923 |
924 | /**
925 | * The amount this modifier adds to (positive) or removes from (negative) the subscription payment,
926 | * in the currency of the subscription
927 | */
928 | amount: number
929 |
930 | /** The text to be displayed on the buyer's receipt email and invoice */
931 | description: string
932 | }
933 |
934 | /** The API response for creating a subscription modifier */
935 | export type PaddleSdkCreateSubscriptionModifierResponse = {
936 | /** The unique ID of the subscription */
937 | subscriptionId: number
938 |
939 | /** The unique ID of the modifier */
940 | modifierId: number
941 | }
942 |
943 | /** The API request parameters for cancelling a subscription */
944 | export type PaddleSdkCancelSubscriptionRequest = {
945 | /** The unique ID of the subscription to be cancelled */
946 | subscriptionId: number
947 | }
948 |
949 | /** The API response for cancelling a subscription */
950 | export type PaddleSdkCancelSubscriptionResponse = void
951 |
--------------------------------------------------------------------------------
/src/metadata.ts:
--------------------------------------------------------------------------------
1 | import * as aes from '@devoxa/aes-encryption'
2 | import { PaddleSdkException } from './exceptions'
3 |
4 | /**
5 | * Encodes and decodes additional pass-through data for an order
6 | *
7 | * @see https://developer.paddle.com/guides/how-tos/checkout/pass-parameters
8 | */
9 | export type MetadataCodec = {
10 | /** Stringifies the given metadata value to a `passthrough` string */
11 | readonly stringify: (metadata: TMetadata) => string
12 |
13 | /** Parses the given `passthrough` string to a metadata value */
14 | readonly parse: (passthrough: string) => TMetadata
15 | }
16 |
17 | /**
18 | * Creates a new metadata codec whose `stringify` method always returns empty string
19 | * and `parse` method always returns `null`
20 | */
21 | export function ignoreMetadata(): MetadataCodec {
22 | return {
23 | stringify: () => '',
24 | parse: () => null,
25 | }
26 | }
27 |
28 | /** Creates a new metadata codec which passes through metadata strings unmodified */
29 | export function passthroughMetadata(): MetadataCodec {
30 | return {
31 | stringify: (metadata) => metadata,
32 | parse: (passthrough) => passthrough,
33 | }
34 | }
35 |
36 | /** Creates a new metadata codec which uses `JSON` to stringify and parse metadata values */
37 | export function stringifyMetadata(): MetadataCodec {
38 | return {
39 | stringify: (metadata) => JSON.stringify(metadata),
40 | parse: (passthrough) => {
41 | try {
42 | return JSON.parse(passthrough) as TMetadata
43 | } catch (err) {
44 | throw new PaddleSdkException('Failed parsing metadata: ' + err.message)
45 | }
46 | },
47 | }
48 | }
49 |
50 | /**
51 | * Applies symmetric encryption to the given metadata codec
52 | *
53 | * @example
54 | * ```
55 | * // Apply encryption to plain string metadata values.
56 | * const codec1 = encryptMetadata(passthroughMetadata(), encryptionKey);
57 | *
58 | * // Apply encryption to JSON-stringified metadata values.
59 | * const codec2 = encryptMetadata(stringifyMetadata(), encryptionKey);
60 | * ```
61 | */
62 | export function encryptMetadata(
63 | codec: MetadataCodec,
64 | encryptionKey: string
65 | ): MetadataCodec {
66 | if (!encryptionKey || encryptionKey.length !== 32) {
67 | throw new PaddleSdkException('PaddleSdk was called with an invalid encryption key')
68 | }
69 | return {
70 | stringify: (metadata) => aes.encrypt(encryptionKey, codec.stringify(metadata)),
71 | parse: (passthrough) => {
72 | let decrypted
73 | try {
74 | decrypted = aes.decrypt(encryptionKey, passthrough)
75 | } catch (err) {
76 | throw new PaddleSdkException('Failed decrypting metadata: ' + err.message)
77 | }
78 | return codec.parse(decrypted)
79 | },
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/__snapshots__/api-requests.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`api requests can cancel a subscription 1`] = `undefined`;
4 |
5 | exports[`api requests can cancel a subscription 2`] = `
6 | [
7 | "https://vendors.paddle.com/api/2.0/subscription/users_cancel",
8 | {
9 | "body": {
10 | "subscription_id": 123,
11 | "vendor_auth_code": "FooBarBaz",
12 | "vendor_id": 123456,
13 | },
14 | "method": "POST",
15 | },
16 | ]
17 | `;
18 |
19 | exports[`api requests can create a product pay link 1`] = `
20 | {
21 | "url": "https://checkout.paddle.com/checkout/custom/5686a6515f1eb5c9c679dc5494dd1b6b",
22 | }
23 | `;
24 |
25 | exports[`api requests can create a product pay link 2`] = `
26 | [
27 | "https://vendors.paddle.com/api/2.0/product/generate_pay_link",
28 | {
29 | "body": {
30 | "affiliates": [
31 | "123",
32 | ],
33 | "coupon_code": "COUPON-AAA",
34 | "custom_message": "Custom message under the product name",
35 | "customer_country": "DE",
36 | "customer_email": "david@devoxa.io",
37 | "customer_postcode": "37688",
38 | "discountable": 1,
39 | "expires": "2020-07-03",
40 | "image_url": "https://devoxa.io/image.png",
41 | "marketing_consent": 1,
42 | "passthrough": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
43 | "prices": [
44 | "USD:9.99",
45 | ],
46 | "product_id": 123,
47 | "quantity": 9,
48 | "quantity_variable": 1,
49 | "recurring_affiliate_limit": 2,
50 | "recurring_prices": [
51 | "EUR:12.99",
52 | ],
53 | "return_url": "https://devoxa.io/return",
54 | "title": "Product name",
55 | "trial_days": 14,
56 | "vat_city": "Ripon",
57 | "vat_company_name": "Devoxa",
58 | "vat_country": "GB",
59 | "vat_number": "123456789",
60 | "vat_postcode": "HG4 1LH",
61 | "vat_state": "Yorkshire",
62 | "vat_street": "4 Stonebridgegate",
63 | "vendor_auth_code": "FooBarBaz",
64 | "vendor_id": 123456,
65 | "webhook_url": "https://webhook.devoxa.io",
66 | },
67 | "method": "POST",
68 | },
69 | ]
70 | `;
71 |
72 | exports[`api requests can create a subscription modifier 1`] = `
73 | {
74 | "modifierId": 2,
75 | "subscriptionId": 1,
76 | }
77 | `;
78 |
79 | exports[`api requests can create a subscription modifier 2`] = `
80 | [
81 | "https://vendors.paddle.com/api/2.0/subscription/modifiers/create",
82 | {
83 | "body": {
84 | "modifier_amount": 9.99,
85 | "modifier_description": "Extra sparkles",
86 | "modifier_recurring": true,
87 | "subscription_id": 123,
88 | "vendor_auth_code": "FooBarBaz",
89 | "vendor_id": 123456,
90 | },
91 | "method": "POST",
92 | },
93 | ]
94 | `;
95 |
96 | exports[`api requests can list the subscriptions with filters 1`] = `
97 | [
98 | {
99 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937",
100 | "cardBrand": null,
101 | "cardExpirationDate": null,
102 | "cardLastFour": null,
103 | "customerEmail": "foo@bar.com",
104 | "customerId": 18124595,
105 | "hasMarketingConsent": false,
106 | "lastPaymentAmount": 2.99,
107 | "lastPaymentCurrency": "USD",
108 | "lastPaymentDate": 2020-08-08T00:00:00.000Z,
109 | "nextPaymentAmount": null,
110 | "nextPaymentCurrency": null,
111 | "nextPaymentDate": null,
112 | "pausedAt": 2020-09-01T23:15:48.000Z,
113 | "pausedFrom": 2020-09-08T23:15:48.000Z,
114 | "paymentMethod": "PAYPAL",
115 | "productId": 124890,
116 | "quantity": 1,
117 | "signupDate": 2020-08-08T23:15:48.000Z,
118 | "status": "PAUSED",
119 | "subscriptionId": 7908253,
120 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A",
121 | },
122 | {
123 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937",
124 | "cardBrand": "VISA",
125 | "cardExpirationDate": 2021-08-01T00:00:00.000Z,
126 | "cardLastFour": "1824",
127 | "customerEmail": "foo@bar.com",
128 | "customerId": 18124595,
129 | "hasMarketingConsent": false,
130 | "lastPaymentAmount": 2.99,
131 | "lastPaymentCurrency": "USD",
132 | "lastPaymentDate": 2020-08-08T00:00:00.000Z,
133 | "nextPaymentAmount": 2.99,
134 | "nextPaymentCurrency": "USD",
135 | "nextPaymentDate": 2020-09-08T00:00:00.000Z,
136 | "pausedAt": null,
137 | "pausedFrom": null,
138 | "paymentMethod": "CARD",
139 | "productId": 124890,
140 | "quantity": 1,
141 | "signupDate": 2020-08-08T23:15:48.000Z,
142 | "status": "ACTIVE",
143 | "subscriptionId": 7908253,
144 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A",
145 | },
146 | {
147 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937",
148 | "cardBrand": "VISA",
149 | "cardExpirationDate": 2021-08-01T00:00:00.000Z,
150 | "cardLastFour": "1824",
151 | "customerEmail": "foo@bar.com",
152 | "customerId": 18124595,
153 | "hasMarketingConsent": false,
154 | "lastPaymentAmount": 2.99,
155 | "lastPaymentCurrency": "USD",
156 | "lastPaymentDate": 2020-08-08T00:00:00.000Z,
157 | "nextPaymentAmount": null,
158 | "nextPaymentCurrency": null,
159 | "nextPaymentDate": null,
160 | "pausedAt": null,
161 | "pausedFrom": null,
162 | "paymentMethod": "CARD",
163 | "productId": 124890,
164 | "quantity": 1,
165 | "signupDate": 2020-08-08T23:15:48.000Z,
166 | "status": "CANCELLED",
167 | "subscriptionId": 7908253,
168 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A",
169 | },
170 | ]
171 | `;
172 |
173 | exports[`api requests can list the subscriptions with filters 2`] = `
174 | [
175 | "https://vendors.paddle.com/api/2.0/subscription/users",
176 | {
177 | "body": {
178 | "page": 0,
179 | "plan_id": "123",
180 | "results_per_page": 100,
181 | "state": "deleted",
182 | "subscription_id": "123",
183 | "vendor_auth_code": "FooBarBaz",
184 | "vendor_id": 123456,
185 | },
186 | "method": "POST",
187 | },
188 | ]
189 | `;
190 |
191 | exports[`api requests can list the subscriptions with no filters 1`] = `
192 | [
193 | "https://vendors.paddle.com/api/2.0/subscription/users",
194 | {
195 | "body": {
196 | "page": undefined,
197 | "plan_id": undefined,
198 | "results_per_page": undefined,
199 | "state": undefined,
200 | "subscription_id": undefined,
201 | "vendor_auth_code": "FooBarBaz",
202 | "vendor_id": 123456,
203 | },
204 | "method": "POST",
205 | },
206 | ]
207 | `;
208 |
209 | exports[`api requests can update a subscription (all fields) 1`] = `
210 | {
211 | "customerId": 2,
212 | "nextPaymentAmount": 9.99,
213 | "nextPaymentCurrency": "EUR",
214 | "nextPaymentDate": 2020-03-04T00:00:00.000Z,
215 | "productId": 3,
216 | "subscriptionId": 1,
217 | }
218 | `;
219 |
220 | exports[`api requests can update a subscription (all fields) 2`] = `
221 | [
222 | "https://vendors.paddle.com/api/2.0/subscription/users/update",
223 | {
224 | "body": {
225 | "bill_immediately": false,
226 | "currency": "EUR",
227 | "keep_modifiers": true,
228 | "passthrough": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
229 | "pause": true,
230 | "plan_id": 3,
231 | "prorate": true,
232 | "quantity": 9,
233 | "recurring_price": 9.99,
234 | "subscription_id": 1,
235 | "vendor_auth_code": "FooBarBaz",
236 | "vendor_id": 123456,
237 | },
238 | "method": "POST",
239 | },
240 | ]
241 | `;
242 |
243 | exports[`api requests can update a subscription (single field) 1`] = `
244 | {
245 | "customerId": 2,
246 | "nextPaymentAmount": 9.99,
247 | "nextPaymentCurrency": "EUR",
248 | "nextPaymentDate": 2020-03-04T00:00:00.000Z,
249 | "productId": 3,
250 | "subscriptionId": 1,
251 | }
252 | `;
253 |
254 | exports[`api requests can update a subscription (single field) 2`] = `
255 | [
256 | "https://vendors.paddle.com/api/2.0/subscription/users/update",
257 | {
258 | "body": {
259 | "bill_immediately": undefined,
260 | "currency": undefined,
261 | "keep_modifiers": undefined,
262 | "passthrough": undefined,
263 | "pause": true,
264 | "plan_id": undefined,
265 | "prorate": undefined,
266 | "quantity": undefined,
267 | "recurring_price": undefined,
268 | "subscription_id": 1,
269 | "vendor_auth_code": "FooBarBaz",
270 | "vendor_id": 123456,
271 | },
272 | "method": "POST",
273 | },
274 | ]
275 | `;
276 |
277 | exports[`api requests errors if the payment method is not known 1`] = `[Error: Unknown payment method "foobar"]`;
278 |
279 | exports[`api requests throws on API failure 1`] = `[Error: Foo]`;
280 |
--------------------------------------------------------------------------------
/tests/__snapshots__/index.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`PaddleSDK throws an error when initialized with a wrong metadata encryption key 1`] = `"PaddleSdk was called with an invalid encryption key"`;
4 |
5 | exports[`PaddleSDK throws an error when initialized without a metadata codec 1`] = `"PaddleSdk was called without a metadataCodec"`;
6 |
7 | exports[`PaddleSDK throws an error when initialized without a public key 1`] = `"PaddleSdk was called without a publicKey"`;
8 |
9 | exports[`PaddleSDK throws an error when initialized without a vendor auth code 1`] = `"PaddleSdk was called without a vendorAuthCode"`;
10 |
11 | exports[`PaddleSDK throws an error when initialized without a vendor id 1`] = `"PaddleSdk was called without a vendorId"`;
12 |
--------------------------------------------------------------------------------
/tests/__snapshots__/metadata.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`metadata codecs "encrypt" codec throws error on decryption failure 1`] = `"Failed decrypting metadata: Invalid authentication tag length: 0"`;
4 |
5 | exports[`metadata codecs "stringify" codec throws error on parse failure 1`] = `"Failed parsing metadata: Unexpected token 'g', "garbage" is not valid JSON"`;
6 |
--------------------------------------------------------------------------------
/tests/__snapshots__/parse-webhook-event.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`parse webhook event errors if the event type is not known 1`] = `"Implementation missing: Can not parse event of type foo_bar"`;
4 |
5 | exports[`parse webhook event errors if the metadata can not be parsed 1`] = `"Failed decrypting metadata: Invalid authentication tag length: 0"`;
6 |
7 | exports[`parse webhook event errors if the webhook signature can not be validated 1`] = `"Failed validating webhook event body"`;
8 |
9 | exports[`parse webhook event parses a "payment refunded" event correctly 1`] = `
10 | {
11 | "amount": 414.43,
12 | "balanceCurrency": "USD",
13 | "balanceEarningsDecrease": 0.34,
14 | "balanceFeeRefund": 0.11,
15 | "balanceGrossRefund": 0.28,
16 | "balanceTaxRefund": 0.96,
17 | "checkoutId": "9-a568055ec8edc7b-e57313c2c0",
18 | "currency": "EUR",
19 | "customerEmail": "foo@bar.com",
20 | "earningsDecrease": 0.08,
21 | "eventId": 123456789,
22 | "eventTime": 2020-08-12T21:01:30.000Z,
23 | "eventType": "PAYMENT_REFUNDED",
24 | "feeRefund": 0.86,
25 | "grossRefund": 0.61,
26 | "hasMarketingConsent": true,
27 | "metadata": {
28 | "foo": "bar",
29 | },
30 | "orderId": "8",
31 | "quantity": 68,
32 | "refundReason": "Example Reason",
33 | "refundType": "FULL",
34 | "taxRefund": 0.76,
35 | }
36 | `;
37 |
38 | exports[`parse webhook event parses a "payment succeeded" event correctly 1`] = `
39 | {
40 | "balanceCurrency": "USD",
41 | "balanceEarnings": 317.23,
42 | "balanceFee": 875.73,
43 | "balanceGross": 800.96,
44 | "balanceTax": 412.02,
45 | "checkoutId": "9-656d6c95693cd5b-77ea2a55f9",
46 | "coupon": "Coupon 9",
47 | "currency": "EUR",
48 | "customerCountry": "AU",
49 | "customerEmail": "foo@bar.com",
50 | "customerName": "Customer Name",
51 | "earnings": 140.26,
52 | "eventId": 123456789,
53 | "eventTime": 2020-08-12T21:01:30.000Z,
54 | "eventType": "PAYMENT_SUCCEEDED",
55 | "fee": 0.75,
56 | "gross": 804.76,
57 | "hasMarketingConsent": true,
58 | "metadata": {
59 | "foo": "bar",
60 | },
61 | "orderId": "3",
62 | "paymentMethod": "PAYPAL",
63 | "productId": 4,
64 | "productName": "Example Product Name",
65 | "quantity": 77,
66 | "receiptUrl": "https://my.paddle.com/receipt/5/2fe1b313345427e-2e6b05993f",
67 | "tax": 0.48,
68 | "usedPriceOverride": true,
69 | }
70 | `;
71 |
72 | exports[`parse webhook event parses a "subscription cancelled" event correctly 1`] = `
73 | {
74 | "cancelledFrom": 2020-08-12T00:00:00.000Z,
75 | "checkoutId": "1-25b286e64f4a228-65f3aadbbb",
76 | "currency": "GBP",
77 | "customerEmail": "monroe84@example.com",
78 | "customerId": 5,
79 | "eventId": 1523571645,
80 | "eventTime": 2020-08-12T02:13:06.000Z,
81 | "eventType": "SUBSCRIPTION_CANCELLED",
82 | "hasMarketingConsent": false,
83 | "metadata": {
84 | "foo": "bar",
85 | },
86 | "price": 464.07,
87 | "productId": 5,
88 | "quantity": 93,
89 | "status": "CANCELLED",
90 | "subscriptionId": 8,
91 | "unitPrice": 4.99,
92 | }
93 | `;
94 |
95 | exports[`parse webhook event parses a "subscription created" event correctly 1`] = `
96 | {
97 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=6&subscription=1&hash=acb15fc760b9d4cb4fae9abf67b7b4d52f12441e",
98 | "checkoutId": "2-461ecbe710cab0e-f2e284ef00",
99 | "currency": "GBP",
100 | "customerEmail": "gordon35@example.org",
101 | "customerId": 9,
102 | "eventId": 2091557455,
103 | "eventTime": 2020-08-09T22:11:09.000Z,
104 | "eventType": "SUBSCRIPTION_CREATED",
105 | "hasMarketingConsent": true,
106 | "metadata": {
107 | "foo": "bar",
108 | },
109 | "nextPaymentDate": 2020-09-03T00:00:00.000Z,
110 | "price": 229.77,
111 | "productId": 7,
112 | "quantity": 23,
113 | "referrerUrl": "https://genesis.devoxa.io/",
114 | "status": "TRIALING",
115 | "subscriptionId": 6,
116 | "unitPrice": 9.99,
117 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=3&subscription=3&hash=ce39e713407ccffbc867dba87e39e88324114274",
118 | }
119 | `;
120 |
121 | exports[`parse webhook event parses a "subscription payment succeeded" event correctly 1`] = `
122 | {
123 | "balanceCurrency": "GBP",
124 | "balanceEarnings": 233.34,
125 | "balanceFee": 812.01,
126 | "balanceGross": 915.81,
127 | "balanceTax": 865.97,
128 | "checkoutId": "8-329f6daa7e07bf2-5793a78be0",
129 | "coupon": "COUPON_CODE",
130 | "currency": "EUR",
131 | "customerCountry": "FR",
132 | "customerEmail": "dietrich.karlie@example.com",
133 | "customerId": 6,
134 | "customerName": "Ela Example",
135 | "earnings": 432.01,
136 | "eventId": 507897344,
137 | "eventTime": 2020-08-12T21:01:30.000Z,
138 | "eventType": "SUBSCRIPTION_PAYMENT_SUCCEEDED",
139 | "fee": 0.24,
140 | "gross": 900.3,
141 | "hasMarketingConsent": true,
142 | "installments": 9,
143 | "isInitialPayment": true,
144 | "metadata": {
145 | "foo": "bar",
146 | },
147 | "nextPaymentAmount": 4.99,
148 | "nextPaymentDate": 2020-08-20T00:00:00.000Z,
149 | "orderId": "4",
150 | "paymentMethod": "PAYPAL",
151 | "price": 174.65,
152 | "productId": 1,
153 | "quantity": 35,
154 | "receiptUrl": "https://my.paddle.com/receipt/5/7dce0d96bae94f3-9bb70f9885",
155 | "status": "ACTIVE",
156 | "subscriptionId": 4,
157 | "subscriptionPaymentId": 4,
158 | "tax": 0.48,
159 | "unitPrice": 4.99,
160 | }
161 | `;
162 |
163 | exports[`parse webhook event parses a "subscription updated (paused)" event correctly 1`] = `
164 | {
165 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=7&subscription=1&hash=539134462da7ff8bce9581704ff3f462994caf05",
166 | "checkoutId": "6-1d95a8cd27317a6-f8810efc48",
167 | "currency": "USD",
168 | "customerEmail": "rconnelly@example.org",
169 | "customerId": 3,
170 | "eventId": 1251568959,
171 | "eventTime": 2020-08-12T02:12:03.000Z,
172 | "eventType": "SUBSCRIPTION_UPDATED",
173 | "hasMarketingConsent": true,
174 | "metadata": {
175 | "foo": "bar",
176 | },
177 | "nextPaymentDate": 2020-09-04T00:00:00.000Z,
178 | "oldNextPaymentDate": 2020-08-02T00:00:00.000Z,
179 | "oldPrice": 4.99,
180 | "oldProductId": 2,
181 | "oldQuantity": 1,
182 | "oldStatus": "TRIALING",
183 | "oldUnitPrice": 4.99,
184 | "pausedAt": 2020-01-01T12:13:14.000Z,
185 | "pausedFrom": 2020-02-01T12:13:14.000Z,
186 | "pausedReason": "DELINQUENT",
187 | "price": 19.98,
188 | "productId": 3,
189 | "quantity": 2,
190 | "status": "ACTIVE",
191 | "subscriptionId": 1,
192 | "unitPrice": 9.99,
193 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=7&subscription=9&hash=89fc4d8da91884f626157cc6095dc857510a1d21",
194 | }
195 | `;
196 |
197 | exports[`parse webhook event parses a "subscription updated" event correctly 1`] = `
198 | {
199 | "cancelUrl": "https://checkout.paddle.com/subscription/cancel?user=7&subscription=1&hash=539134462da7ff8bce9581704ff3f462994caf05",
200 | "checkoutId": "6-1d95a8cd27317a6-f8810efc48",
201 | "currency": "USD",
202 | "customerEmail": "rconnelly@example.org",
203 | "customerId": 3,
204 | "eventId": 1251568959,
205 | "eventTime": 2020-08-12T02:12:03.000Z,
206 | "eventType": "SUBSCRIPTION_UPDATED",
207 | "hasMarketingConsent": true,
208 | "metadata": {
209 | "foo": "bar",
210 | },
211 | "nextPaymentDate": 2020-09-04T00:00:00.000Z,
212 | "oldNextPaymentDate": 2020-08-02T00:00:00.000Z,
213 | "oldPrice": 4.99,
214 | "oldProductId": 2,
215 | "oldQuantity": 1,
216 | "oldStatus": "TRIALING",
217 | "oldUnitPrice": 4.99,
218 | "pausedAt": null,
219 | "pausedFrom": null,
220 | "pausedReason": null,
221 | "price": 19.98,
222 | "productId": 3,
223 | "quantity": 2,
224 | "status": "ACTIVE",
225 | "subscriptionId": 1,
226 | "unitPrice": 9.99,
227 | "updateUrl": "https://checkout.paddle.com/subscription/update?user=7&subscription=9&hash=89fc4d8da91884f626157cc6095dc857510a1d21",
228 | }
229 | `;
230 |
--------------------------------------------------------------------------------
/tests/api-requests.spec.ts:
--------------------------------------------------------------------------------
1 | import { PaddleSdkApiException } from '../src/exceptions'
2 | import { fetch } from '../src/helpers/fetch'
3 | import {
4 | PaddleSdk,
5 | PaddleSdkCountry,
6 | PaddleSdkCurrency,
7 | PaddleSdkSubscriptionStatus,
8 | } from '../src/index'
9 | import { encryptMetadata, stringifyMetadata } from '../src/metadata'
10 | import * as FIXTURES from './fixtures'
11 |
12 | jest.mock('../src/helpers/fetch', () => ({ fetch: jest.fn() }))
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | function mockNextApiResponse(response: any) {
16 | ;(fetch as jest.Mock).mockImplementationOnce(async () => ({ success: true, response }))
17 | }
18 |
19 | function getLastApiRequest() {
20 | return (fetch as jest.Mock).mock.calls[0]
21 | }
22 |
23 | describe('api requests', () => {
24 | const paddleSdk = new PaddleSdk({
25 | publicKey: FIXTURES.publicKey,
26 | vendorId: FIXTURES.vendorId,
27 | vendorAuthCode: FIXTURES.vendorAuthCode,
28 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
29 | })
30 |
31 | beforeEach(() => {
32 | ;(fetch as jest.Mock).mockClear()
33 | })
34 |
35 | test('can create a product pay link', async () => {
36 | mockNextApiResponse(FIXTURES.createProductPayLinkApiResponse)
37 |
38 | const response = await paddleSdk.createProductPayLink({
39 | productId: 123,
40 | title: 'Product name',
41 | webhookUrl: 'https://webhook.devoxa.io',
42 | prices: [[PaddleSdkCurrency.USD, 9.99]],
43 | recurringPrices: [[PaddleSdkCurrency.EUR, 12.99]],
44 | trialDays: 14,
45 | customMessage: 'Custom message under the product name',
46 | populateCoupon: 'COUPON-AAA',
47 | isDiscountable: true,
48 | imageUrl: 'https://devoxa.io/image.png',
49 | returnUrl: 'https://devoxa.io/return',
50 | isQuantityVariable: true,
51 | populateQuantity: 9,
52 | expirationDate: new Date('2020-07-03'),
53 | affiliates: [123],
54 | recurringAffiliateLimit: 2,
55 | populateHasMarketingConsent: true,
56 | populateCustomerEmail: 'david@devoxa.io',
57 | populateCustomerCountry: PaddleSdkCountry.DE,
58 | populateCustomerPostcode: '37688',
59 | populateVatNumber: '123456789',
60 | populateVatCompanyName: 'Devoxa',
61 | populateVatStreet: '4 Stonebridgegate',
62 | populateVatCity: 'Ripon',
63 | populateVatState: 'Yorkshire',
64 | populateVatCountry: PaddleSdkCountry.GB,
65 | populateVatPostcode: 'HG4 1LH',
66 | metadata: { foo: 'bar' },
67 | })
68 |
69 | expect(response).toMatchSnapshot()
70 |
71 | const request = getLastApiRequest()
72 | request[1].body.passthrough = request[1].body.passthrough.replace(/./gi, 'X')
73 | expect(request).toMatchSnapshot()
74 | })
75 |
76 | test('can list the subscriptions with filters', async () => {
77 | mockNextApiResponse(FIXTURES.listSubscriptionsApiResponse)
78 |
79 | const response = await paddleSdk.listSubscriptions({
80 | subscriptionId: 123,
81 | productId: 123,
82 | status: PaddleSdkSubscriptionStatus.CANCELLED,
83 | page: 0,
84 | resultsPerPage: 100,
85 | })
86 |
87 | expect(response).toMatchSnapshot()
88 | expect(getLastApiRequest()).toMatchSnapshot()
89 | })
90 |
91 | test('can list the subscriptions with no filters', async () => {
92 | mockNextApiResponse(FIXTURES.listSubscriptionsApiResponse)
93 |
94 | await paddleSdk.listSubscriptions({})
95 |
96 | expect(getLastApiRequest()).toMatchSnapshot()
97 | })
98 |
99 | test('errors if the payment method is not known', async () => {
100 | const fixture = FIXTURES.listSubscriptionsApiResponse
101 | fixture[0].payment_information.payment_method = 'foobar' as 'paypal'
102 |
103 | mockNextApiResponse(fixture)
104 |
105 | let error
106 | try {
107 | await paddleSdk.listSubscriptions({})
108 | } catch (err) {
109 | error = err
110 | }
111 |
112 | expect(error).toMatchSnapshot()
113 | })
114 |
115 | test('can update a subscription (all fields)', async () => {
116 | mockNextApiResponse(FIXTURES.updateSubscriptionApiResponse)
117 |
118 | const response = await paddleSdk.updateSubscription({
119 | subscriptionId: 1,
120 | quantity: 9,
121 | currency: PaddleSdkCurrency.EUR,
122 | unitPrice: 9.99,
123 | shouldMakeImmediatePayment: false,
124 | productId: 3,
125 | shouldProrate: true,
126 | shouldKeepModifiers: true,
127 | metadata: { foo: 'bar' },
128 | shouldPause: true,
129 | })
130 |
131 | expect(response).toMatchSnapshot()
132 |
133 | const request = getLastApiRequest()
134 | request[1].body.passthrough = request[1].body.passthrough.replace(/./gi, 'X')
135 | expect(request).toMatchSnapshot()
136 | })
137 |
138 | test('can update a subscription (single field)', async () => {
139 | mockNextApiResponse(FIXTURES.updateSubscriptionApiResponse)
140 |
141 | const response = await paddleSdk.updateSubscription({
142 | subscriptionId: 1,
143 | shouldPause: true,
144 | })
145 |
146 | expect(response).toMatchSnapshot()
147 | expect(getLastApiRequest()).toMatchSnapshot()
148 | })
149 |
150 | test('can cancel a subscription', async () => {
151 | mockNextApiResponse(FIXTURES.cancelSubscriptionApiResponse)
152 |
153 | const response = await paddleSdk.cancelSubscription({
154 | subscriptionId: 123,
155 | })
156 |
157 | expect(response).toMatchSnapshot()
158 | expect(getLastApiRequest()).toMatchSnapshot()
159 | })
160 |
161 | test('can create a subscription modifier', async () => {
162 | mockNextApiResponse(FIXTURES.createSubscriptionModifierApiResponse)
163 |
164 | const response = await paddleSdk.createSubscriptionModifier({
165 | subscriptionId: 123,
166 | amount: 9.99,
167 | isRecurring: true,
168 | description: 'Extra sparkles',
169 | })
170 |
171 | expect(response).toMatchSnapshot()
172 | expect(getLastApiRequest()).toMatchSnapshot()
173 | })
174 |
175 | test('throws on API failure', async () => {
176 | ;(fetch as jest.Mock).mockImplementationOnce(async () => ({
177 | success: false,
178 | error: { message: 'Foo' },
179 | }))
180 |
181 | let error
182 | try {
183 | await paddleSdk.createProductPayLink({ productId: 123 })
184 | } catch (err) {
185 | error = err
186 | }
187 |
188 | expect(error).toBeInstanceOf(PaddleSdkApiException)
189 | expect(error).toMatchSnapshot()
190 | })
191 | })
192 |
--------------------------------------------------------------------------------
/tests/fixtures.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RawPaddlePostProductGeneratePayLinkResponse,
3 | RawPaddlePostSubscriptionModifiersCreateResponse,
4 | RawPaddlePostSubscriptionUsersCancelResponse,
5 | RawPaddlePostSubscriptionUsersResponse,
6 | RawPaddlePostSubscriptionUsersUpdateResponse,
7 | } from '../src/__generated__/api-routes'
8 | import {
9 | RawPaddlePaymentRefundedAlert,
10 | RawPaddlePaymentSucceededAlert,
11 | RawPaddleSubscriptionCancelledAlert,
12 | RawPaddleSubscriptionCreatedAlert,
13 | RawPaddleSubscriptionPaymentSucceededAlert,
14 | RawPaddleSubscriptionUpdatedAlert,
15 | } from '../src/__generated__/webhook-alerts'
16 |
17 | export const publicKey = `-----BEGIN PUBLIC KEY-----
18 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvTyIlXs2V2WVVtIdl62o
19 | sjghcb6yT1XjWvWgIYuv8bEY7PaYnSm32SWkwi50qsHxz0khDhPlsIf9nmkrzMJS
20 | LI46PDE1ccfnCIlRkwEyr3Cc4vn1YLSBXuv+faSkcPWFZKcaBO4c+pE7ttoYxl12
21 | Ft1dvDNyEe8qofWSuQg5/AfrBx73Csn7YxQCmOZQeN9TWpzNOq/hPgNL+icUJVfC
22 | kkch3yg5j6epxUgM7EDP2UYYH7LlHWR4naA77d8bzIt80AeBnia+HwAwpHL0LN8L
23 | biglrxzX5pIR+4lPkjiAfil6qfB1UuC1l1Xnsl4cXVhr760VYqTftvMT6bhWuzrt
24 | MYyo97LfO8rb0k3uB9DLcJAgvCo/27J1ijoQMX9L/6Pf1q/hgmtJ0JVe8SwaFQTo
25 | 247L6m9tJFF/tsvAuFJTT9L7KQsoJB/sgUXvRJvcqVcaFeTSkw4uRKk0aGtG7S5i
26 | zRoqnkJFHV71N59bZCgDMGhKwdKhr43W/E9E557y+GQmrqkfd8kHd5SRLbE6WruT
27 | k/pePP6oiYQMFUMLn3OVlph/Wqt3sRUKVrDwJDPxw8GCzBNkKYPjyS/Ow0Em3JQ/
28 | rZQd8jVtIZHUkZnggGsCHVgrp+n5wvc4EA3dsdaazV7u8oDfk1EpYraGlNjsjnTY
29 | VpEfcjiOIG6t/VmGu32pCDECAwEAAQ==
30 | -----END PUBLIC KEY-----`
31 |
32 | export const vendorId = 123456
33 |
34 | export const vendorAuthCode = 'FooBarBaz'
35 |
36 | export const metadataEncryptionKey = 'ZtdDl3Ex7ycFfgdbAC3uTLNk8eLVDcEd'
37 |
38 | export const paymentSucceededEvent: RawPaddlePaymentSucceededAlert = {
39 | alert_id: `123456789`,
40 | alert_name: `payment_succeeded`,
41 | balance_currency: `USD`,
42 | balance_earnings: `317.23`,
43 | balance_fee: `875.73`,
44 | balance_gross: `800.96`,
45 | balance_tax: `412.02`,
46 | checkout_id: `9-656d6c95693cd5b-77ea2a55f9`,
47 | country: `AU`,
48 | coupon: `Coupon 9`,
49 | currency: `EUR`,
50 | customer_name: `Customer Name`,
51 | earnings: `140.26`,
52 | email: `foo@bar.com`,
53 | event_time: `2020-08-12 21:01:30`,
54 | fee: `0.75`,
55 | ip: `89.207.217.18`,
56 | marketing_consent: `1`,
57 | order_id: `3`,
58 | p_signature: `GLv7wxZi4E1GLo+SmFnq3OAiZ7ib8wcU/WBh/8sXlmocTIBBx5yBjnvDbE0qaNgxMlew/esYfDv1ApHAnz93J/MziloBoLmu/8YCjDddVFs1R/Y+2sbIhkI5xu84Uz1BobkIPtR/+GVgI0k2XeLudG3weCc6/eUa/T3dxc0p9b9aenKGig1HxHpWTCBi5fnr121c7HDlly6eyPlGrmk5yrwtbfTGBmvrwU3c3X7Nh0lVGEMt1Y2KEqgfFsaGwdg9JnT/xDg33NyEKDPOu0asvb6cPmRS+q+ojUnzBldFCd/+/H9TjE/Qjoqy8bNNjAIKH3kReyZls42631Hf3DuqsTLg2uR6xQCXTxLvRukdkUKsidu2sadLJ5NgibpnWsplgptS7AFXwm7uwvarFmbwSrN1YgI3vvN/Hm3/3xWHlpbfhUKDeOu2O5JW8drsgFNY2DIdE57HqseutKypp0shgZBfDn+aYyk6o0GhbmkY54dmtFRqxCFNFiy5TXfKQcFKi71nvYdd6ILQOj43HKvNScKm4cuiCqoLNOtYs3kY1k8nlvgyTY6EV1vYf+I84bt/BbKDgwNTmJoHwAeRACwVg59wb1hV5upCbHFy/Qn3/M189Nl6FiutlGqnuTwEwxapdGbFegjAd062OA8Ye4yixIOnsPIWGsTGcIkHEQyRLN0=`,
59 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
60 | payment_method: `paypal`,
61 | payment_tax: `0.48`,
62 | product_id: `4`,
63 | product_name: `Example Product Name`,
64 | quantity: `77`,
65 | receipt_url: `https://my.paddle.com/receipt/5/2fe1b313345427e-2e6b05993f`,
66 | sale_gross: `804.76`,
67 | used_price_override: `true`,
68 | }
69 |
70 | export const paymentRefundedEvent: RawPaddlePaymentRefundedAlert = {
71 | alert_id: `123456789`,
72 | alert_name: 'payment_refunded',
73 | amount: `414.43`,
74 | balance_currency: `USD`,
75 | balance_earnings_decrease: `0.34`,
76 | balance_fee_refund: `0.11`,
77 | balance_gross_refund: `0.28`,
78 | balance_tax_refund: `0.96`,
79 | checkout_id: `9-a568055ec8edc7b-e57313c2c0`,
80 | currency: `EUR`,
81 | earnings_decrease: `0.08`,
82 | email: `foo@bar.com`,
83 | event_time: `2020-08-12 21:01:30`,
84 | fee_refund: `0.86`,
85 | gross_refund: `0.61`,
86 | marketing_consent: `1`,
87 | order_id: `8`,
88 | p_signature: `GLv7wxZi4E1GLo+SmFnq3OAiZ7ib8wcU/WBh/8sXlmocTIBBx5yBjnvDbE0qaNgxMlew/esYfDv1ApHAnz93J/MziloBoLmu/8YCjDddVFs1R/Y+2sbIhkI5xu84Uz1BobkIPtR/+GVgI0k2XeLudG3weCc6/eUa/T3dxc0p9b9aenKGig1HxHpWTCBi5fnr121c7HDlly6eyPlGrmk5yrwtbfTGBmvrwU3c3X7Nh0lVGEMt1Y2KEqgfFsaGwdg9JnT/xDg33NyEKDPOu0asvb6cPmRS+q+ojUnzBldFCd/+/H9TjE/Qjoqy8bNNjAIKH3kReyZls42631Hf3DuqsTLg2uR6xQCXTxLvRukdkUKsidu2sadLJ5NgibpnWsplgptS7AFXwm7uwvarFmbwSrN1YgI3vvN/Hm3/3xWHlpbfhUKDeOu2O5JW8drsgFNY2DIdE57HqseutKypp0shgZBfDn+aYyk6o0GhbmkY54dmtFRqxCFNFiy5TXfKQcFKi71nvYdd6ILQOj43HKvNScKm4cuiCqoLNOtYs3kY1k8nlvgyTY6EV1vYf+I84bt/BbKDgwNTmJoHwAeRACwVg59wb1hV5upCbHFy/Qn3/M189Nl6FiutlGqnuTwEwxapdGbFegjAd062OA8Ye4yixIOnsPIWGsTGcIkHEQyRLN0=`,
89 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
90 | quantity: `68`,
91 | refund_reason: `Example Reason`,
92 | refund_type: `full`,
93 | tax_refund: `0.76`,
94 | }
95 |
96 | export const subscriptionCreatedEvent: RawPaddleSubscriptionCreatedAlert = {
97 | alert_id: `2091557455`,
98 | alert_name: `subscription_created`,
99 | cancel_url: `https://checkout.paddle.com/subscription/cancel?user=6&subscription=1&hash=acb15fc760b9d4cb4fae9abf67b7b4d52f12441e`,
100 | checkout_id: `2-461ecbe710cab0e-f2e284ef00`,
101 | currency: `GBP`,
102 | email: `gordon35@example.org`,
103 | event_time: `2020-08-09 22:11:09`,
104 | linked_subscriptions: `8, 9, 4`,
105 | marketing_consent: `1`,
106 | next_bill_date: `2020-09-03`,
107 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
108 | quantity: `23`,
109 | source: `https://genesis.devoxa.io/`,
110 | status: `trialing`,
111 | subscription_id: `6`,
112 | subscription_plan_id: `7`,
113 | unit_price: `9.99`,
114 | update_url: `https://checkout.paddle.com/subscription/update?user=3&subscription=3&hash=ce39e713407ccffbc867dba87e39e88324114274`,
115 | user_id: `9`,
116 | p_signature: `D2jcpJvuB1RWUR8nZvf9y6v3l7p3Mn1H5uBOmQ7VInYF1ZRnoIKliedaHWMEI6JG4NkfJewwzeNtEl4wMSd9W+c5MkccUAibvpFTY3yCm+PgyVTGcXxHNTab2bbJRuV+MBvV1h4MR9tGAgf4iAS+MjQN1y0YBv0WV+EFKhoFJsdHjatVLRFmpTkjdwsW75Sm7td7r76mccd2WqCGLyHpnn0wbDzs0xt3xNtS6YSdIKZw6y0G+Pu0vFSKvVYjlbSRbzvjGjNMhlTwcvbFDdya3aFmi0OC0xFhP0BG/iqk3amznshOotj4UMyW8GtTwMCFXAJSxkl1wDp/3H7mdl792g+Ui1fsnpMpMVLpxGMLzCcsUXy1FO2n9U6JrtckSHLmEFifls75e8hW6OOxn3RN8KLWOMi5HLjtxOGOPYYY/hd82QYVHYmDm3tWdK4jFUhc/lePoepzBFdnymGvSkE0s3V9KfMDDwjMAeEhy42KhlAGCCs1TCqsc7uJ8LQLKjGNzbuq2GXFE9TwCNH/iBGhKPPwFVFFKOr6++SJnRHLoqRWo8+jFBl8P4tGwYkFjAOD3sFXWRJQqEFVZ5Qvre/F1OnfCdY/wOKUDOLPrXMIgXde9ROhmgMaFNOZEr6ePzpiTDJ5uH0Qy4ghYghxlCEGUoRKdBFIDJq/0pwbDWUQhi0=`,
117 | }
118 |
119 | export const subscriptionUpdatedEvent: RawPaddleSubscriptionUpdatedAlert = {
120 | alert_id: `1251568959`,
121 | alert_name: `subscription_updated`,
122 | cancel_url: `https://checkout.paddle.com/subscription/cancel?user=7&subscription=1&hash=539134462da7ff8bce9581704ff3f462994caf05`,
123 | checkout_id: `6-1d95a8cd27317a6-f8810efc48`,
124 | currency: `USD`,
125 | email: `rconnelly@example.org`,
126 | event_time: `2020-08-12 02:12:03`,
127 | linked_subscriptions: `6, 7, 1`,
128 | marketing_consent: `1`,
129 | new_price: `19.98`,
130 | new_quantity: `2`,
131 | new_unit_price: `9.99`,
132 | next_bill_date: `2020-09-04`,
133 | old_next_bill_date: `2020-08-02`,
134 | old_price: `4.99`,
135 | old_quantity: `1`,
136 | old_status: `trialing`,
137 | old_subscription_plan_id: `2`,
138 | old_unit_price: `4.99`,
139 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
140 | status: `active`,
141 | subscription_id: `1`,
142 | subscription_plan_id: `3`,
143 | update_url: `https://checkout.paddle.com/subscription/update?user=7&subscription=9&hash=89fc4d8da91884f626157cc6095dc857510a1d21`,
144 | user_id: `3`,
145 | p_signature: `axnbEGrHYKeCbHPnoYsrKcqWQ7kho8mLJofDORDCA1JP9Ur7/DfG7PWCxAYDRGvlmUCXA1JgjUZ96J+oDtcLqchX6yWuUBb3N9fhz9mt0j7P4Om0p5pFh3tB5iMjNoRm4uh8fZxRnGSZzdaWnA0pj6Iz3M3lk8Uzcj5dfZiEq0bJ9MxL24eB0CmeP8fCN7mhExaaDXu1BmchpJxJhjuG5yiZgwow2P+ccSvHfDjD6MkEMfEk4LvHt6MI99afQxUo92XBiLX0tqfQ3KRqBjEnY6SRXvFq8PbhvetdHwNP02h1qa57Re25Io7KN2lEe3B5uc27g4aicwojCRLzuSHUlnokghbmkg5Vb4vpo62F3ZmQArxd0RuMFHHje5Q+gpN83D/liVR3XtrdDoNSsSMib9Uu7jRpFpJkWgnwScOvh4jMUoHQgWaa6hSJzgj1dAhBwN9y/cKnjkSJijBvj93883XNaNPNZ63BD/LsHFBWtcKdn10DmlJcJ0355IeUb4YHYWqHma3eAud5xxbtwJ5Yz1njaDnn0mLHfifN2tTVfUAYa2xaxCadoUJ06RRCgkve/DbGCHUJ8g7KjXkyq33JUdx204sx0rH0pg5koU5/T7lfDw7CpUDULhWXgTIWVbRecR6f0a66RHOYlF7qDT0vWRbI5BGpRyk2QR5N1XRfEpA=`,
146 | }
147 |
148 | export const subscriptionCancelledEvent: RawPaddleSubscriptionCancelledAlert = {
149 | alert_id: `1523571645`,
150 | alert_name: `subscription_cancelled`,
151 | cancellation_effective_date: `2020-08-12 19:51:38`,
152 | checkout_id: `1-25b286e64f4a228-65f3aadbbb`,
153 | currency: `GBP`,
154 | email: `monroe84@example.com`,
155 | event_time: `2020-08-12 02:13:06`,
156 | linked_subscriptions: `1, 7, 6`,
157 | marketing_consent: `0`,
158 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
159 | quantity: `93`,
160 | status: `deleted`,
161 | subscription_id: `8`,
162 | subscription_plan_id: `5`,
163 | unit_price: `4.99`,
164 | user_id: `5`,
165 | p_signature: `lHnMWYrwAjuVNu2QOOPMriwHd81XyLRfyN+Y/Jks1jHSzvXG9CripAOB9dEJri6XNQjJFr1RKGAeqSCo6hO4WpgCYcdQx+DKO2aBFkhOrrfTIcOCf2nzWGqDjXKs1w0CmGKgq0aZPcLs3fXlid/vWFw0qBd4MY9rwQ2J+7GGdDHjwaAuzGvI3SddTmKARwGQOiWovG0GLkpMBrLgVt72BkGcdj0pKQ9wzMQDmjQzvT7HxqftVaYc55HGgZvGMBCgvJeBMgJ7es3NWDXi9qRT1k0Cj9xss7q1+rwyfIpFKJqXP4neuSs9NGABxSYzxXjFmAgK2/xEeOnit1B1xnAZlbUor6iR4bbHyKF/mwumWXqhdpfbXM6xXZSQI1VmTi5I2Sv5i5hhnlcm15/3Cd4WvmcQuVKTidd6LLQ9Py8jOcVxCJrjjwvLVkCZWHqpmC7opSRlTwarqEpUiMyKXy4UkTFVHzWNb/DPi6TB1+LXcw1eCn3fHACO7OWj4kXOZg4m67pUZ34Xl9ncdc8gD4xWzcpvwLtvgAEzK23rnMEZUcpCytJBZ9ipRt8ppRsXrX4zdnbq/UZuFmLgnvLSQ5cPKyYkQdBkAFKlA0x3Uo9GyuvfK641Yd6I73NtIqnG0WRj8JWg0ktVYdYI/Hos0cA1QQp9mjXd1tV178IC3MvBTgg=`,
166 | }
167 |
168 | export const subscriptionPaymentSucceededEvent: RawPaddleSubscriptionPaymentSucceededAlert = {
169 | alert_id: `507897344`,
170 | alert_name: `subscription_payment_succeeded`,
171 | balance_currency: `GBP`,
172 | balance_earnings: `233.34`,
173 | balance_fee: `812.01`,
174 | balance_gross: `915.81`,
175 | balance_tax: `865.97`,
176 | checkout_id: `8-329f6daa7e07bf2-5793a78be0`,
177 | country: `FR`,
178 | coupon: `COUPON_CODE`,
179 | currency: `EUR`,
180 | customer_name: `Ela Example`,
181 | earnings: `432.01`,
182 | email: `dietrich.karlie@example.com`,
183 | event_time: `2020-08-12 21:01:30`,
184 | fee: `0.24`,
185 | initial_payment: `1`,
186 | instalments: `9`,
187 | marketing_consent: `1`,
188 | next_bill_date: `2020-08-20`,
189 | next_payment_amount: `4.99`,
190 | order_id: `4`,
191 | p_signature: `GLv7wxZi4E1GLo+SmFnq3OAiZ7ib8wcU/WBh/8sXlmocTIBBx5yBjnvDbE0qaNgxMlew/esYfDv1ApHAnz93J/MziloBoLmu/8YCjDddVFs1R/Y+2sbIhkI5xu84Uz1BobkIPtR/+GVgI0k2XeLudG3weCc6/eUa/T3dxc0p9b9aenKGig1HxHpWTCBi5fnr121c7HDlly6eyPlGrmk5yrwtbfTGBmvrwU3c3X7Nh0lVGEMt1Y2KEqgfFsaGwdg9JnT/xDg33NyEKDPOu0asvb6cPmRS+q+ojUnzBldFCd/+/H9TjE/Qjoqy8bNNjAIKH3kReyZls42631Hf3DuqsTLg2uR6xQCXTxLvRukdkUKsidu2sadLJ5NgibpnWsplgptS7AFXwm7uwvarFmbwSrN1YgI3vvN/Hm3/3xWHlpbfhUKDeOu2O5JW8drsgFNY2DIdE57HqseutKypp0shgZBfDn+aYyk6o0GhbmkY54dmtFRqxCFNFiy5TXfKQcFKi71nvYdd6ILQOj43HKvNScKm4cuiCqoLNOtYs3kY1k8nlvgyTY6EV1vYf+I84bt/BbKDgwNTmJoHwAeRACwVg59wb1hV5upCbHFy/Qn3/M189Nl6FiutlGqnuTwEwxapdGbFegjAd062OA8Ye4yixIOnsPIWGsTGcIkHEQyRLN0=`,
192 | passthrough: `kLFV0ZkzYdTrYKIvFhQlkK9vccfQP0FxGpK10j563YK2zCGdhMij2aX/sblM`,
193 | payment_method: `paypal`,
194 | payment_tax: `0.48`,
195 | plan_name: `Example Plan Name`,
196 | quantity: `35`,
197 | receipt_url: `https://my.paddle.com/receipt/5/7dce0d96bae94f3-9bb70f9885`,
198 | sale_gross: `900.3`,
199 | status: `active`,
200 | subscription_id: `4`,
201 | subscription_payment_id: `4`,
202 | subscription_plan_id: `1`,
203 | unit_price: `4.99`,
204 | user_id: `6`,
205 | }
206 |
207 | export const createProductPayLinkApiResponse: RawPaddlePostProductGeneratePayLinkResponse = {
208 | url: 'https://checkout.paddle.com/checkout/custom/5686a6515f1eb5c9c679dc5494dd1b6b',
209 | }
210 |
211 | export const listSubscriptionsApiResponse: RawPaddlePostSubscriptionUsersResponse = [
212 | {
213 | subscription_id: 7908253,
214 | plan_id: 124890,
215 | user_id: 18124595,
216 | user_email: 'foo@bar.com',
217 | marketing_consent: false,
218 | update_url: `https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A`,
219 | cancel_url: `https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937`,
220 | state: 'paused',
221 | signup_date: '2020-08-08 23:15:48',
222 | last_payment: { amount: 2.99, currency: 'USD', date: '2020-08-08' },
223 | payment_information: { payment_method: 'paypal' },
224 | paused_at: '2020-09-01 23:15:48',
225 | paused_from: '2020-09-08 23:15:48',
226 | },
227 | {
228 | subscription_id: 7908253,
229 | plan_id: 124890,
230 | user_id: 18124595,
231 | user_email: 'foo@bar.com',
232 | marketing_consent: false,
233 | update_url: `https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A`,
234 | cancel_url: `https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937`,
235 | state: 'active',
236 | signup_date: '2020-08-08 23:15:48',
237 | last_payment: { amount: 2.99, currency: 'USD', date: '2020-08-08' },
238 | next_payment: { amount: 2.99, currency: 'USD', date: '2020-09-08' },
239 | payment_information: {
240 | payment_method: 'card',
241 | card_type: 'visa',
242 | last_four_digits: '1824',
243 | expiry_date: '08/2021',
244 | },
245 | quantity: 1,
246 | },
247 | {
248 | subscription_id: 7908253,
249 | plan_id: 124890,
250 | user_id: 18124595,
251 | user_email: 'foo@bar.com',
252 | marketing_consent: false,
253 | update_url: `https://checkout.paddle.com/subscription/update?user=18124595&subscription=7908253&hash=AEA72A29FABC23692043D8B6D55FD29C625F532552AA8AC6EB137E604FC13E1A`,
254 | cancel_url: `https://checkout.paddle.com/subscription/cancel?user=18124595&subscription=7908253&hash=9A4EE7D7645833AB6F90F81287DA92E139FE8DF6AF2090081357104138EC6937`,
255 | state: 'deleted',
256 | signup_date: '2020-08-08 23:15:48',
257 | last_payment: { amount: 2.99, currency: 'USD', date: '2020-08-08' },
258 | payment_information: {
259 | payment_method: 'card',
260 | card_type: 'visa',
261 | last_four_digits: '1824',
262 | expiry_date: '08/2021',
263 | },
264 | quantity: 1,
265 | },
266 | ]
267 |
268 | export const updateSubscriptionApiResponse: RawPaddlePostSubscriptionUsersUpdateResponse = {
269 | subscription_id: 1,
270 | user_id: 2,
271 | plan_id: 3,
272 | next_payment: {
273 | amount: 9.99,
274 | currency: 'EUR',
275 | date: '2020-03-04',
276 | },
277 | }
278 |
279 | export const cancelSubscriptionApiResponse: RawPaddlePostSubscriptionUsersCancelResponse = undefined
280 |
281 | export const createSubscriptionModifierApiResponse: RawPaddlePostSubscriptionModifiersCreateResponse =
282 | {
283 | subscription_id: 1,
284 | modifier_id: 2,
285 | }
286 |
--------------------------------------------------------------------------------
/tests/helpers/__snapshots__/fetch.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`helpers -> fetch can make a fetch request using a form data body 1`] = `
4 | {
5 | "response": {
6 | "foo": "bar",
7 | },
8 | "success": true,
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/tests/helpers/converters.spec.ts:
--------------------------------------------------------------------------------
1 | import { PaddleSdkCurrency, PaddleSdkSubscriptionStatus } from '../../src'
2 | import * as converters from '../../src/helpers/converters'
3 |
4 | describe('helpers -> converters', () => {
5 | test('can convert an API integer', () => {
6 | expect(converters.convertApiInteger('12')).toEqual(12)
7 | })
8 |
9 | test('can convert an API float', () => {
10 | expect(converters.convertApiFloat('12')).toEqual(12)
11 | expect(converters.convertApiFloat('12.99')).toEqual(12.99)
12 | })
13 |
14 | test('can convert an API boolean', () => {
15 | expect(converters.convertApiBoolean('1')).toEqual(true)
16 | expect(converters.convertApiBoolean('0')).toEqual(false)
17 | expect(converters.convertApiBoolean('true')).toEqual(true)
18 | expect(converters.convertApiBoolean('false')).toEqual(false)
19 | })
20 |
21 | test('can convert an SDK boolean', () => {
22 | expect(converters.convertSdkBoolean(true)).toEqual(1)
23 | expect(converters.convertSdkBoolean(false)).toEqual(0)
24 | })
25 |
26 | test('can convert an API date', () => {
27 | expect(converters.convertApiDate('2020-09-21', 'DATE')).toEqual(
28 | new Date('2020-09-21T00:00:00.000Z')
29 | )
30 | expect(converters.convertApiDate('2020-09-21 21:32:10', 'DATE_TIME')).toEqual(
31 | new Date('2020-09-21T21:32:10.000Z')
32 | )
33 | expect(converters.convertApiDate('09/2022', 'EXPIRY_DATE')).toEqual(
34 | new Date('2022-09-01T00:00:00.000Z')
35 | )
36 | })
37 |
38 | test('can convert an SDK date', () => {
39 | expect(converters.convertSdkDate(new Date('2020-09-21T00:00:00.000Z'), 'DATE')).toEqual(
40 | '2020-09-21'
41 | )
42 | expect(converters.convertSdkDate(new Date('2020-09-21T21:32:10.000Z'), 'DATE_TIME')).toEqual(
43 | '2020-09-21 21:32:10'
44 | )
45 | expect(converters.convertSdkDate(new Date('2022-09-01T00:00:00.000Z'), 'EXPIRY_DATE')).toEqual(
46 | '09/2022'
47 | )
48 | })
49 |
50 | test('can convert an API subscription status', () => {
51 | expect(converters.convertApiSubscriptionStatus('active')).toEqual('ACTIVE')
52 | expect(converters.convertApiSubscriptionStatus('trialing')).toEqual('TRIALING')
53 | expect(converters.convertApiSubscriptionStatus('past_due')).toEqual('PAST_DUE')
54 | expect(converters.convertApiSubscriptionStatus('paused')).toEqual('PAUSED')
55 | expect(converters.convertApiSubscriptionStatus('deleted')).toEqual('CANCELLED')
56 | })
57 |
58 | test('can convert an SDK subscription status', () => {
59 | expect(converters.convertSdkSubscriptionStatus(PaddleSdkSubscriptionStatus.ACTIVE)).toEqual(
60 | 'active'
61 | )
62 | expect(converters.convertSdkSubscriptionStatus(PaddleSdkSubscriptionStatus.TRIALING)).toEqual(
63 | 'trialing'
64 | )
65 | expect(converters.convertSdkSubscriptionStatus(PaddleSdkSubscriptionStatus.PAST_DUE)).toEqual(
66 | 'past_due'
67 | )
68 | expect(converters.convertSdkSubscriptionStatus(PaddleSdkSubscriptionStatus.PAUSED)).toEqual(
69 | 'paused'
70 | )
71 | expect(converters.convertSdkSubscriptionStatus(PaddleSdkSubscriptionStatus.CANCELLED)).toEqual(
72 | 'deleted'
73 | )
74 | })
75 |
76 | test('can convert an API paused reason', () => {
77 | expect(converters.convertApiPausedReason('delinquent')).toEqual('DELINQUENT')
78 | expect(converters.convertApiPausedReason('voluntary')).toEqual('VOLUNTARY')
79 | })
80 |
81 | test('can convert an API currency', () => {
82 | expect(converters.convertApiCurrency('EUR')).toEqual('EUR')
83 |
84 | // @ts-expect-error types have no overlap
85 | converters.convertApiCurrency('EUR') === 'NOOP'
86 | })
87 |
88 | test('can convert an API country', () => {
89 | expect(converters.convertApiCountry('GB')).toEqual('GB')
90 |
91 | // @ts-expect-error types have no overlap
92 | converters.convertApiCurrency('GB') === 'NOOP'
93 | })
94 |
95 | test('can convert an API payment method', () => {
96 | expect(converters.convertApiPaymentMethod('card')).toEqual('CARD')
97 | expect(converters.convertApiPaymentMethod('paypal')).toEqual('PAYPAL')
98 | expect(converters.convertApiPaymentMethod('apple-pay')).toEqual('APPLE_PAY')
99 | expect(converters.convertApiPaymentMethod('wire-transfer')).toEqual('WIRE_TRANSFER')
100 | expect(converters.convertApiPaymentMethod('free')).toEqual('FREE')
101 | })
102 |
103 | test('can convert an API card type', () => {
104 | expect(converters.convertApiCardBrand('visa')).toEqual('VISA')
105 | expect(converters.convertApiCardBrand('american_express')).toEqual('AMERICAN_EXPRESS')
106 | expect(converters.convertApiCardBrand('discover')).toEqual('DISCOVER')
107 | expect(converters.convertApiCardBrand('jcb')).toEqual('JCB')
108 | expect(converters.convertApiCardBrand('elo')).toEqual('ELO')
109 | expect(converters.convertApiCardBrand('master')).toEqual('MASTERCARD')
110 | expect(converters.convertApiCardBrand('mastercard')).toEqual('MASTERCARD')
111 | expect(converters.convertApiCardBrand('maestro')).toEqual('MAESTRO')
112 | expect(converters.convertApiCardBrand('diners_club')).toEqual('DINERS_CLUB')
113 | expect(converters.convertApiCardBrand('xxx')).toEqual('UNKNOWN')
114 | })
115 |
116 | test('can convert an refund type', () => {
117 | expect(converters.convertApiRefundType('full')).toEqual('FULL')
118 | expect(converters.convertApiRefundType('vat')).toEqual('VAT')
119 | expect(converters.convertApiRefundType('partial')).toEqual('PARTIAL')
120 | })
121 |
122 | test('can convert an SDK price list', () => {
123 | expect(
124 | converters.convertSdkPriceList([
125 | [PaddleSdkCurrency.EUR, 7.99],
126 | [PaddleSdkCurrency.USD, 9.99],
127 | ])
128 | ).toEqual(['EUR:7.99', 'USD:9.99'])
129 | })
130 | })
131 |
--------------------------------------------------------------------------------
/tests/helpers/fetch.spec.ts:
--------------------------------------------------------------------------------
1 | import FormData from 'form-data'
2 | import { fetch } from '../../src/helpers/fetch'
3 |
4 | const mockFetch = jest.fn()
5 | global.fetch = mockFetch
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | function mockNextNodeFetchCall(json: any) {
9 | mockFetch.mockImplementationOnce(async () => ({ json: async () => json }))
10 | }
11 |
12 | function getLastNodeFetchCall() {
13 | return mockFetch.mock.calls[0]
14 | }
15 |
16 | describe('helpers -> fetch', () => {
17 | beforeEach(() => {
18 | mockFetch.mockClear()
19 | })
20 |
21 | test('can make a fetch request using a form data body', async () => {
22 | mockNextNodeFetchCall({ success: true, response: { foo: 'bar' } })
23 |
24 | const response = await fetch('https://example.com', {
25 | method: 'POST',
26 | body: { baz: 42, ree: undefined },
27 | })
28 | expect(response).toMatchSnapshot()
29 |
30 | const request = getLastNodeFetchCall()
31 | expect(request[0]).toEqual('https://example.com')
32 | expect(request[1].method).toEqual('POST')
33 | expect(request[1].body).toBeInstanceOf(FormData)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/tests/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { PaddleSdk } from '../src/index'
2 | import { encryptMetadata, stringifyMetadata } from '../src/metadata'
3 | import * as FIXTURES from './fixtures'
4 |
5 | describe('PaddleSDK', () => {
6 | test('throws an error when initialized without a public key', async () => {
7 | expect(() => {
8 | // @ts-expect-error missing public key
9 | new PaddleSdk({
10 | vendorId: FIXTURES.vendorId,
11 | vendorAuthCode: FIXTURES.vendorAuthCode,
12 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
13 | })
14 | }).toThrowErrorMatchingSnapshot()
15 | })
16 |
17 | test('throws an error when initialized without a vendor id', async () => {
18 | expect(() => {
19 | // @ts-expect-error missing vendor id
20 | new PaddleSdk({
21 | publicKey: FIXTURES.publicKey,
22 | vendorAuthCode: FIXTURES.vendorAuthCode,
23 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
24 | })
25 | }).toThrowErrorMatchingSnapshot()
26 | })
27 |
28 | test('throws an error when initialized without a vendor auth code', async () => {
29 | expect(() => {
30 | // @ts-expect-error missing vendor auth code
31 | new PaddleSdk({
32 | publicKey: FIXTURES.publicKey,
33 | vendorId: FIXTURES.vendorId,
34 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
35 | })
36 | }).toThrowErrorMatchingSnapshot()
37 | })
38 |
39 | test('throws an error when initialized without a metadata codec', async () => {
40 | expect(() => {
41 | // @ts-expect-error missing encryption key
42 | new PaddleSdk({
43 | publicKey: FIXTURES.publicKey,
44 | vendorId: FIXTURES.vendorId,
45 | vendorAuthCode: FIXTURES.vendorAuthCode,
46 | })
47 | }).toThrowErrorMatchingSnapshot()
48 | })
49 |
50 | test('throws an error when initialized with a wrong metadata encryption key', async () => {
51 | expect(() => {
52 | new PaddleSdk({
53 | publicKey: FIXTURES.publicKey,
54 | vendorId: FIXTURES.vendorId,
55 | vendorAuthCode: FIXTURES.vendorAuthCode,
56 | metadataCodec: encryptMetadata(stringifyMetadata(), 'FooBar'),
57 | })
58 | }).toThrowErrorMatchingSnapshot()
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/tests/metadata.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | encryptMetadata,
3 | ignoreMetadata,
4 | passthroughMetadata,
5 | stringifyMetadata,
6 | } from '../src/metadata'
7 |
8 | describe('metadata codecs', () => {
9 | test('"ignore" codec strips metadata values', () => {
10 | const codec = ignoreMetadata()
11 | const metadata = null
12 | const encoded = codec.stringify(metadata)
13 | const decoded = codec.parse(encoded)
14 |
15 | expect(decoded).toEqual(metadata)
16 | })
17 |
18 | test('"passthrough" codec does not modify metadata values', () => {
19 | const codec = passthroughMetadata()
20 | const metadata = 'foo bar baz'
21 | const encoded = codec.stringify(metadata)
22 | const decoded = codec.parse(encoded)
23 |
24 | expect(decoded).toEqual(metadata)
25 | })
26 |
27 | test('"stringify" codec stringifies and parses metadata values', () => {
28 | const codec = stringifyMetadata()
29 | const metadata = { x: 0 }
30 | const encoded = codec.stringify(metadata)
31 | const decoded = codec.parse(encoded)
32 |
33 | expect(decoded).toEqual(metadata)
34 | })
35 |
36 | test('"stringify" codec throws error on parse failure', () => {
37 | const codec = stringifyMetadata()
38 |
39 | expect(() => codec.parse('garbage')).toThrowErrorMatchingSnapshot()
40 | })
41 |
42 | test('"encrypt" codec encrypts and decrypts metadata values', () => {
43 | const codec = encryptMetadata(stringifyMetadata(), '01234567890123456789012345678901')
44 | const metadata = { x: 0 }
45 | const encoded = codec.stringify(metadata)
46 | const decoded = codec.parse(encoded)
47 |
48 | expect(decoded).toEqual(metadata)
49 | })
50 |
51 | test('"encrypt" codec throws error on decryption failure', () => {
52 | const codec = encryptMetadata(passthroughMetadata(), '01234567890123456789012345678901')
53 |
54 | expect(() => codec.parse('garbage')).toThrowErrorMatchingSnapshot()
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/tests/parse-webhook-event.spec.ts:
--------------------------------------------------------------------------------
1 | import { PaddleSdk } from '../src/index'
2 | import { encryptMetadata, stringifyMetadata } from '../src/metadata'
3 | import * as FIXTURES from './fixtures'
4 |
5 | const paddleSdk = new PaddleSdk({
6 | publicKey: FIXTURES.publicKey,
7 | vendorId: FIXTURES.vendorId,
8 | vendorAuthCode: FIXTURES.vendorAuthCode,
9 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
10 | })
11 |
12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 | ;(paddleSdk as any).verifyWebhookEvent = () => true
14 |
15 | describe('parse webhook event', () => {
16 | test('parses a "payment succeeded" event correctly', () => {
17 | expect(paddleSdk.parseWebhookEvent(FIXTURES.paymentSucceededEvent)).toMatchSnapshot()
18 | })
19 |
20 | test('parses a "payment refunded" event correctly', () => {
21 | expect(paddleSdk.parseWebhookEvent(FIXTURES.paymentRefundedEvent)).toMatchSnapshot()
22 | })
23 |
24 | test('parses a "subscription created" event correctly', () => {
25 | expect(paddleSdk.parseWebhookEvent(FIXTURES.subscriptionCreatedEvent)).toMatchSnapshot()
26 | })
27 |
28 | test('parses a "subscription updated" event correctly', () => {
29 | expect(paddleSdk.parseWebhookEvent(FIXTURES.subscriptionUpdatedEvent)).toMatchSnapshot()
30 | })
31 |
32 | test('parses a "subscription updated (paused)" event correctly', () => {
33 | const fixture = {
34 | ...FIXTURES.subscriptionUpdatedEvent,
35 | paused_at: '2020-01-01 12:13:14',
36 | paused_from: '2020-02-01 12:13:14',
37 | paused_reason: 'delinquent',
38 | }
39 |
40 | expect(paddleSdk.parseWebhookEvent(fixture)).toMatchSnapshot()
41 | })
42 |
43 | test('parses a "subscription cancelled" event correctly', () => {
44 | expect(paddleSdk.parseWebhookEvent(FIXTURES.subscriptionCancelledEvent)).toMatchSnapshot()
45 | })
46 |
47 | test('parses a "subscription payment succeeded" event correctly', () => {
48 | expect(
49 | paddleSdk.parseWebhookEvent(FIXTURES.subscriptionPaymentSucceededEvent)
50 | ).toMatchSnapshot()
51 | })
52 |
53 | test('errors if the event type is not known', () => {
54 | const fixture = {
55 | ...FIXTURES.subscriptionCreatedEvent,
56 | alert_name: 'foo_bar',
57 | }
58 |
59 | expect(() => paddleSdk.parseWebhookEvent(fixture)).toThrowErrorMatchingSnapshot()
60 | })
61 |
62 | test('errors if the metadata can not be parsed', () => {
63 | const fixture = {
64 | ...FIXTURES.subscriptionCreatedEvent,
65 | passthrough: 'FooBar',
66 | }
67 |
68 | expect(() => paddleSdk.parseWebhookEvent(fixture)).toThrowErrorMatchingSnapshot()
69 | })
70 |
71 | test('errors if the webhook signature can not be validated', () => {
72 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
73 | ;(paddleSdk as any).verifyWebhookEvent = () => false
74 |
75 | expect(() =>
76 | paddleSdk.parseWebhookEvent(FIXTURES.subscriptionCreatedEvent)
77 | ).toThrowErrorMatchingSnapshot()
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/tests/verify-webhook-event.spec.ts:
--------------------------------------------------------------------------------
1 | import { PaddleSdk } from '../src/index'
2 | import { encryptMetadata, stringifyMetadata } from '../src/metadata'
3 | import * as FIXTURES from './fixtures'
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | function verify(body: any, publicKey?: string) {
7 | const paddleSdk = new PaddleSdk({
8 | publicKey: publicKey || FIXTURES.publicKey,
9 | vendorId: FIXTURES.vendorId,
10 | vendorAuthCode: FIXTURES.vendorAuthCode,
11 | metadataCodec: encryptMetadata(stringifyMetadata(), FIXTURES.metadataEncryptionKey),
12 | })
13 |
14 | return paddleSdk.verifyWebhookEvent(body)
15 | }
16 |
17 | describe('verify webhook event', () => {
18 | test('verifies valid signature', () => {
19 | expect(verify(FIXTURES.subscriptionCreatedEvent)).toEqual(true)
20 | })
21 |
22 | test('verifies valid signature for pre-parsed body', () => {
23 | const eventBodyParsed = {
24 | ...FIXTURES.subscriptionCreatedEvent,
25 | linked_subscriptions: [8, 9, 4],
26 | subscription_plan_id: 7,
27 | }
28 |
29 | expect(verify(eventBodyParsed)).toEqual(true)
30 | })
31 |
32 | test('rejects for wrong body type', () => {
33 | expect(verify(null)).toEqual(false)
34 | expect(verify('FooBarBaz')).toEqual(false)
35 | expect(verify(42)).toEqual(false)
36 | expect(verify({})).toEqual(false)
37 | expect(verify({ foo: 'bar' })).toEqual(false)
38 | expect(verify([])).toEqual(false)
39 | expect(verify(['Foo'])).toEqual(false)
40 | })
41 |
42 | test('rejects for empty body', () => {
43 | expect(verify({})).toEqual(false)
44 | })
45 |
46 | test('rejects for body with irrelevant properties', () => {
47 | expect(verify({ foo: 'bar' })).toEqual(false)
48 | })
49 |
50 | test('rejects for body with extra properties', () => {
51 | expect(verify({ ...FIXTURES.subscriptionCreatedEvent, foo: 'bar' })).toEqual(false)
52 | })
53 |
54 | test('rejects for invalid signature type', () => {
55 | expect(
56 | verify({
57 | ...FIXTURES.subscriptionCreatedEvent,
58 | p_signature: { foo: 'bar' },
59 | })
60 | ).toEqual(false)
61 | })
62 |
63 | test('rejects for invalid signature', () => {
64 | expect(verify({ ...FIXTURES.subscriptionCreatedEvent, p_signature: 'FooBar' })).toEqual(false)
65 | })
66 |
67 | test('rejects for malformed public key', () => {
68 | function replaceCharacterAt(string: string, index: number, replace: string) {
69 | return string.substring(0, index) + replace + string.substring(index + 1)
70 | }
71 |
72 | const malformedPublicKey = replaceCharacterAt(FIXTURES.publicKey, 165, 'A')
73 | expect(verify(FIXTURES.subscriptionCreatedEvent, malformedPublicKey)).toEqual(false)
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Output Options
4 | "target": "es2022",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "noEmitOnError": true,
8 | "newLine": "lf",
9 | "outDir": "dist/",
10 | "baseUrl": "./",
11 | "declaration": true,
12 | "sourceMap": true,
13 | "emitDecoratorMetadata": true,
14 | "experimentalDecorators": true,
15 | "incremental": true,
16 |
17 | // Type-Checking Options
18 | "lib": ["es2023"],
19 | "strict": true,
20 | "skipLibCheck": true,
21 | "strictPropertyInitialization": false,
22 | "useUnknownInCatchVariables": false,
23 |
24 | // Module Resolution Options
25 | "moduleResolution": "node",
26 | "allowSyntheticDefaultImports": true,
27 | "esModuleInterop": true,
28 | "forceConsistentCasingInFileNames": true,
29 | "resolveJsonModule": true,
30 | "allowJs": false
31 | },
32 | "include": ["generators/", "src/", "tests/"]
33 | }
34 |
--------------------------------------------------------------------------------