├── .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 |

9 | paddle-sdk 10 |

11 | 12 | 13 |

14 | An SDK to interface with the API and webhooks from Paddle 15 |

16 | 17 | 18 |

19 | 20 | Package Version 24 | 25 | 26 | 27 | Build Status 31 | 32 | 33 | 34 | Code Coverage 38 | 39 |

40 | 41 | 42 |

43 | Installation • 44 | Usage • 45 | Contributors • 46 | License 47 |

48 | 49 |
50 | 51 | ## Installation 52 | 53 | ```bash 54 | yarn add @devoxa/paddle-sdk 55 | ``` 56 | 57 | ## Usage 58 | 59 | > :warning: This package does **not** implement all available webhook events and API routes. It 60 | > should have the ones required for 90% of subscription based applications, but if you are missing 61 | > something feel free to add them via a pull request! 62 | 63 | ```ts 64 | import { PaddleSdk, stringifyMetadata } from '@devoxa/paddle-sdk' 65 | 66 | // The metadata you want to attach to subscriptions, to link them to your application entities 67 | type PaddleMetadata = { userId: number } 68 | 69 | const paddleSdk = new PaddleSdk({ 70 | baseUrl: 'https://vendors.paddle.com/api', // (Optional) The base URL of the paddle API (e.g. to use the sandbox) 71 | publicKey: '...', // Your public key from the paddle dashboard 72 | vendorId: 123, // Your vendor ID from the paddle dashboard 73 | vendorAuthCode: 'AAA', // Your vendor auth code from the paddle dashboard 74 | metadataCodec: stringifyMetadata(), // JSON stringify and parse additional order data 75 | }) 76 | ``` 77 | 78 | ### Available Methods 79 | 80 | ``` 81 | verifyWebhookEvent(body: any): boolean 82 | parseWebhookEvent(body: any): PaddleSdkSubscriptionCreatedEvent | PaddleSdkSubscriptionUpdatedEvent | ... 83 | async createProductPayLink(data: PaddleSdkCreateProductPayLinkRequest): Promise 84 | async listSubscriptions(data: PaddleSdkListSubscriptionsRequest): Promise 85 | async updateSubscription(data: PaddleSdkUpdateSubscriptionRequest): Promise 86 | async cancelSubscription(data: PaddleSdkCancelSubscriptionRequest): Promise 87 | async createSubscriptionModifier(data: PaddleSdkCreateSubscriptionModifierRequest): Promise 88 | ``` 89 | 90 | **A note on parsing webhooks:** 91 | 92 | If you are using `parseWebhookEvent` on raw events, only enable the following events in your 93 | dashboard to prevent `ImplementationMissing` errors being thrown. 94 | 95 | - [Payment Succeeded](https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-succeeded) 96 | - [Payment Refunded](https://developer.paddle.com/webhook-reference/one-off-purchase-alerts/payment-refunded) 97 | - [Subscription Created](https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-created) 98 | - [Subscription Updated](https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-updated) 99 | - [Subscription Cancelled](https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-cancelled) 100 | - [Subscription Payment Succeeded](https://developer.paddle.com/webhook-reference/subscription-alerts/subscription-payment-succeeded) 101 | 102 | ### Passthrough Data 103 | 104 | During the checkout stage Paddle allows passing additional data to your webhooks in the passthrough 105 | strings via either the client-side JavaScript API or the server-side Pay Link API. See the 106 | [official documentation](https://developer.paddle.com/guides/how-tos/checkout/pass-parameters) for 107 | more details. It's your responsibility to properly convert your data structures to/from such a 108 | string. This project offers you a few such converters in the form of metadata codecs which 109 | stringify/parse metadata values to/from passthrough strings: 110 | 111 | - `ignoreMetadata` is a codec which ignores metadata values. It stringifies any value to an empty 112 | string, and parses any string to a `null` value. 113 | - `passthroughMetadata` is a codec which assumes your metadata values are already strings and passes 114 | them unmodified. 115 | - `stringifyMetadata` is a codec which JSON stringifies/parses your metadata values to/from 116 | passthrough strings. 117 | - `encryptMetadata` is a wrapper which must be used to decorate any of the above codecs. It applies 118 | symmetric encryption to the passthrough strings generated by the wrapped codec to prevent users 119 | from tampering with your data. 120 | 121 | ## Contributors 122 | 123 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |

David Reeß

💻 📖 ⚠️

Jeff Hage

👀

Aliaksandr Radzivanovich

💻 📖 ⚠️
135 | 136 | 137 | 138 | 139 | 140 | 141 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) 142 | specification. Contributions of any kind welcome! 143 | 144 | ## License 145 | 146 | MIT 147 | -------------------------------------------------------------------------------- /dvx.json: -------------------------------------------------------------------------------- 1 | { 2 | "skipCheckRepositorySteps": ["npm-dependency-rules", "code-files-naming-consistency"] 3 | } 4 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Generating webhook alert types" 4 | ts-node generators/generate-webhook-alerts.ts 5 | 6 | echo "Generating API route types" 7 | ts-node generators/generate-api-routes.ts 8 | 9 | echo "Generating enum types" 10 | ts-node generators/generate-enums.ts 11 | 12 | echo "Formatting the generated code" 13 | yarn format 14 | -------------------------------------------------------------------------------- /generators/generate-api-routes.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | const INCLUDE_COMMENTS = true 4 | 5 | const DOCUMENTATION_URLS = [ 6 | 'https://developer.paddle.com/api-reference/product-api/pay-links/createpaylink', 7 | 'https://developer.paddle.com/api-reference/subscription-api/users/listusers', 8 | 'https://developer.paddle.com/api-reference/subscription-api/users/updateuser', 9 | 'https://developer.paddle.com/api-reference/subscription-api/users/canceluser', 10 | 'https://developer.paddle.com/api-reference/subscription-api/modifiers/createmodifier', 11 | ] 12 | 13 | interface RouteInfo { 14 | allProps: { 15 | page: { 16 | data: { 17 | blocks: Array<{ type: string; data: T; header?: { title: string } }> 18 | } 19 | } 20 | } 21 | } 22 | 23 | type RouteInfoRequestBlock = { 24 | children: [ 25 | { 26 | blocks: [ 27 | { 28 | data: JsonSchema 29 | }, 30 | ] 31 | }, 32 | ] 33 | } 34 | 35 | type RouteInfoResponseBlock = { 36 | children: [ 37 | { 38 | blocks: [ 39 | { 40 | data: { 41 | children: Array<{ 42 | title: string 43 | blocks: [ 44 | { 45 | data: { 46 | oneOf: Array<{ 47 | properties: { 48 | success: { 49 | default: boolean 50 | } 51 | response: 52 | | JsonSchema 53 | | { 54 | type: 'array' 55 | items: JsonSchema 56 | } 57 | } 58 | }> 59 | } 60 | }, 61 | ] 62 | }> 63 | } 64 | }, 65 | ] 66 | }, 67 | ] 68 | } 69 | 70 | type RouteInfoTestRequestBlock = { 71 | method: string 72 | url: string 73 | } 74 | 75 | type JsonSchema = { 76 | type: string 77 | properties: { [key: string]: JsonSchemaProperty } 78 | required?: Array 79 | } 80 | 81 | type JsonSchemaProperty = { 82 | type: string 83 | description?: string 84 | default?: any 85 | enum?: Array 86 | pattern?: string 87 | items?: JsonSchemaProperty 88 | properties?: { [key: string]: JsonSchemaProperty } 89 | oneOf?: Array 90 | } 91 | 92 | run() 93 | 94 | async function run() { 95 | let types = [] 96 | 97 | for (let url of DOCUMENTATION_URLS) { 98 | types.push(await buildApiRouteTypes(url)) 99 | console.log(`Built types from ${url}`) 100 | } 101 | 102 | let code = `// THIS FILE IS GENERATED AUTOMATICALLY. DO NOT EDIT. 103 | 104 | ${types.map((x) => x.sourceCode).join('\n\n')}` 105 | 106 | fs.writeFileSync(__dirname + '/../src/__generated__/api-routes.ts', code, 'utf-8') 107 | console.log('Written into /src/__generated__/api-routes.ts') 108 | } 109 | 110 | async function buildApiRouteTypes(url: string) { 111 | const routeInfo = await getRouteInfo(url) 112 | 113 | const requestEndpoint = getRequestEndpoint(routeInfo) 114 | const typeName = getTypeNameFromRequestEndpoint(requestEndpoint) 115 | 116 | const requestJsonSchema = getRequestJsonSchema(routeInfo) 117 | const responseJsonSchema = getResponseJsonSchema(routeInfo) 118 | 119 | const requestProperties = Object.entries(requestJsonSchema.properties) 120 | .filter(([propertyName]) => !['vendor_id', 'vendor_auth_code'].includes(propertyName)) 121 | .map(([propertyName, propertySchema]) => { 122 | const type = inferTypeFromPropertySchema(propertySchema) 123 | 124 | const nullable = requestJsonSchema.required?.includes(propertyName) ? '' : '?' 125 | const comment = propertySchema.description ? `/** ${propertySchema.description} */\n ` : '' 126 | return ` ${INCLUDE_COMMENTS ? comment : ''}${propertyName}${nullable}: ${type}` 127 | }) 128 | 129 | let responseType 130 | if (!responseJsonSchema) { 131 | responseType = 'void' 132 | } else { 133 | const responseIsArray = 'items' in responseJsonSchema 134 | const responsePropertiesRaw = 135 | 'items' in responseJsonSchema 136 | ? responseJsonSchema.items.properties 137 | : responseJsonSchema.properties 138 | 139 | const responseProperties = Object.entries(responsePropertiesRaw).map( 140 | ([propertyName, propertySchema]) => { 141 | const type = inferTypeFromPropertySchema(propertySchema) 142 | 143 | const pattern = propertySchema.pattern ? `\n@pattern ${propertySchema.pattern}` : '' 144 | const comment = propertySchema.description 145 | ? `/** ${propertySchema.description}${pattern} */\n ` 146 | : '' 147 | return ` ${INCLUDE_COMMENTS ? comment : ''}${propertyName}: ${type}` 148 | } 149 | ) 150 | 151 | responseType = `${responseIsArray ? 'Array<' : ''}{ 152 | ${responseProperties.join('\n')} 153 | }${responseIsArray ? '>' : ''}` 154 | } 155 | 156 | const sourceCode = `export const PADDLE_${toSnakeCase(typeName) 157 | .toUpperCase() 158 | .replace(/^(POST|GET)_/, '')} = { 159 | method: '${requestEndpoint.method.toUpperCase()}' as const, 160 | url: '${requestEndpoint.url}' as const, 161 | } 162 | 163 | export type RawPaddle${typeName}Request = { 164 | ${requestProperties.join('\n')} 165 | } 166 | 167 | export type RawPaddle${typeName}Response = ${responseType}` 168 | 169 | return { sourceCode } 170 | } 171 | 172 | async function getRouteInfo(url: string) { 173 | const response = await fetch(url) 174 | const text = await response.text() 175 | 176 | const line = text.split('\n').find((x) => x.trim().startsWith('window.__routeInfo =')) 177 | 178 | if (!line) { 179 | throw new Error('Could not find routeInfo line') 180 | } 181 | 182 | const jsonString = line.replace(/^.*window\.__routeInfo = /, '').replace(/;<.*$/, '') 183 | return JSON.parse(jsonString) 184 | } 185 | 186 | function getRequestEndpoint(routeInfo: RouteInfo) { 187 | const testRequestBlock = routeInfo.allProps.page.data.blocks.find((x) => x.type === 'http') 188 | 189 | if (!testRequestBlock) { 190 | throw new Error('Could not find text block for page') 191 | } 192 | 193 | return { method: testRequestBlock.data.method, url: testRequestBlock.data.url } 194 | } 195 | 196 | function getRequestJsonSchema(routeInfo: RouteInfo) { 197 | const accordionBlock = routeInfo.allProps.page.data.blocks.find((x) => x.type === 'accordion') 198 | 199 | if (!accordionBlock) { 200 | throw new Error('Could not find accordion block for page') 201 | } 202 | 203 | return accordionBlock.data.children[0].blocks[0].data 204 | } 205 | 206 | function getResponseJsonSchema(routeInfo: RouteInfo) { 207 | const tabsBlock = routeInfo.allProps.page.data.blocks.find((x) => x.type === 'tabs') 208 | 209 | if (!tabsBlock) { 210 | throw new Error('Could not find tabs block for page') 211 | } 212 | 213 | const schemaBlock = tabsBlock.data.children[0].blocks[0].data.children.find( 214 | (x) => x.title === 'Schema' 215 | ) 216 | 217 | if (!schemaBlock) { 218 | throw new Error('Could not find schema block for page') 219 | } 220 | 221 | const successResponseSchema = schemaBlock.blocks[0].data.oneOf.find( 222 | (x) => x.properties.success.default !== false 223 | ) 224 | 225 | if (!successResponseSchema) { 226 | throw new Error('Could not find success response block for page') 227 | } 228 | 229 | return successResponseSchema.properties.response 230 | } 231 | 232 | function getTypeNameFromRequestEndpoint(endpoint: { method: string; url: string }): string { 233 | const path = endpoint.url.replace(/^.*\/\d.\d\//, '').replace(/\//g, '_') 234 | return toPascalCase(`${endpoint.method}_${path}`) 235 | } 236 | 237 | function toPascalCase(snakeCase: string) { 238 | const camelCase = snakeCase.replace(/_(\w)/g, ($, $1) => $1.toUpperCase()) 239 | return `${camelCase.charAt(0).toUpperCase()}${camelCase.substr(1)}` 240 | } 241 | 242 | function toSnakeCase(pascalCase: string) { 243 | return pascalCase.replace(/\.?([A-Z]+)/g, ($, $1) => '_' + $1.toLowerCase()).replace(/^_/, '') 244 | } 245 | 246 | function inferTypeFromPropertySchema(propertySchema: JsonSchemaProperty): string { 247 | if (propertySchema.enum) { 248 | let enumValues = propertySchema.enum 249 | 250 | if (propertySchema.type === 'string') { 251 | enumValues = enumValues.map((x) => `"${x}"`) 252 | } 253 | 254 | return enumValues.join(' | ') 255 | } 256 | 257 | if (propertySchema.type === 'integer' || propertySchema.type === 'number') { 258 | return 'number' 259 | } 260 | 261 | if (propertySchema.type === 'string') { 262 | return 'string' 263 | } 264 | 265 | if (propertySchema.type === 'boolean') { 266 | return 'boolean' 267 | } 268 | 269 | if (propertySchema.type === 'object' && propertySchema.properties) { 270 | const objectProperties = Object.entries(propertySchema.properties).map(([key, property]) => { 271 | return ` ${key}: ${inferTypeFromPropertySchema(property)}` 272 | }) 273 | 274 | return '{\n' + objectProperties.join('\n') + '\n}' 275 | } 276 | 277 | if (propertySchema.type === 'object' && propertySchema.oneOf) { 278 | return propertySchema.oneOf 279 | .map((x) => inferTypeFromPropertySchema({ ...x, type: 'object' })) 280 | .join(' | ') 281 | } 282 | 283 | if (propertySchema.type === 'array' && propertySchema.items) { 284 | return `Array<${inferTypeFromPropertySchema(propertySchema.items)}>` 285 | } 286 | 287 | throw new Error(`Unknown property schema type ${propertySchema.type}`) 288 | } 289 | -------------------------------------------------------------------------------- /generators/generate-enums.ts: -------------------------------------------------------------------------------- 1 | import { matchAll } from '@devoxa/flocky' 2 | import fs from 'fs' 3 | 4 | const DOCUMENTATION_URLS = { 5 | RawPaddleEnumCurrencies: `https://developer.paddle.com/reference/platform-parameters/supported-currencies`, 6 | RawPaddleEnumCountries: `https://developer.paddle.com/reference/platform-parameters/supported-countries`, 7 | } 8 | 9 | run() 10 | 11 | async function run() { 12 | let types = [] 13 | 14 | for (let [name, url] of Object.entries(DOCUMENTATION_URLS)) { 15 | types.push(await buildType(name, url)) 16 | console.log(`Built types from ${url}`) 17 | } 18 | 19 | const code = `// THIS FILE IS GENERATED AUTOMATICALLY. DO NOT EDIT. 20 | 21 | ${types.map((x) => x.sourceCode).join('\n\n')}` 22 | 23 | fs.writeFileSync(__dirname + '/../src/__generated__/enums.ts', code, 'utf-8') 24 | console.log('Written into /src/__generated__/enums.ts') 25 | } 26 | 27 | async function buildType(name: string, url: string) { 28 | const enumFields = await getEnumFields(url) 29 | const sourceCode = 30 | `export enum ${name} {\n` + enumFields.map((x) => ` ${x} = "${x}",`).join('\n') + `\n}` 31 | 32 | return { sourceCode } 33 | } 34 | 35 | async function getEnumFields(url: string) { 36 | const response = await fetch(url) 37 | const text = await response.text() 38 | 39 | const matches = matchAll(/(.*?)<\/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 | --------------------------------------------------------------------------------