diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf21a23..7c0a2c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +# [5.0.0](https://github.com/Salable/node-sdk/compare/v4.11.0...v5.0.0) (2025-09-23) + + +### Bug Fixes + +* created more global stripe and salable data to stop the test overwrtting each other ([aba2bfe](https://github.com/Salable/node-sdk/commit/aba2bfe1b3c80f2840761290f504523685e67fef)) +* fixed imports ([1b354ba](https://github.com/Salable/node-sdk/commit/1b354ba060d7e5b470d9ef8963c1cad1a8adcbe6)) +* moved v2 schemas into a single file ([b6ff867](https://github.com/Salable/node-sdk/commit/b6ff867ab42105f81937f1c6725bfb27c23f00db)) +* removed clean up from tests ([e8d5288](https://github.com/Salable/node-sdk/commit/e8d528883860f96f9669c72bef668190ab6df02f)) +* updated EntitlementsCheck return type ([273b14d](https://github.com/Salable/node-sdk/commit/273b14dcb3269fe4ad987499804065f2253e0051)) + + +### Features + +* cursor pagination on products endpoint ([7e9af6f](https://github.com/Salable/node-sdk/commit/7e9af6f81721705882336e1b91226beed26f4c07)) +* entitlements method ([887e8da](https://github.com/Salable/node-sdk/commit/887e8da7cd3b38909b2f7b14fda56f7ae69cf4f5)) +* paginate features and plans endpoints ([37e3357](https://github.com/Salable/node-sdk/commit/37e335710ba5f4e21c472b3b1000bfa742642334)) +* v3 of salable api added ([9c050b5](https://github.com/Salable/node-sdk/commit/9c050b53253ff5d053482264270108195ae44e0a)) +* wip v3 removed Salable class for initSalable function ([a72d4e1](https://github.com/Salable/node-sdk/commit/a72d4e1c5dc3098e4b9246375b32afed3f5f38f1)) + + +### Tests + +* updated feature schema to include featureEnumOptions ([2b143a0](https://github.com/Salable/node-sdk/commit/2b143a030bd9f87e0c93dba8ceeb60f661a474f1)) + + +### BREAKING CHANGES + +* methods will use v3 of the API. New initSalable function to replace Salable class. + +# [5.0.0-beta.2](https://github.com/Salable/node-sdk/compare/v5.0.0-beta.1...v5.0.0-beta.2) (2025-09-18) + + +### Bug Fixes + +* updated EntitlementsCheck return type ([273b14d](https://github.com/Salable/node-sdk/commit/273b14dcb3269fe4ad987499804065f2253e0051)) + +# [5.0.0-beta.1](https://github.com/Salable/node-sdk/compare/v4.11.0...v5.0.0-beta.1) (2025-09-18) + + +### Bug Fixes + +* created more global stripe and salable data to stop the test overwrtting each other ([aba2bfe](https://github.com/Salable/node-sdk/commit/aba2bfe1b3c80f2840761290f504523685e67fef)) +* fixed imports ([1b354ba](https://github.com/Salable/node-sdk/commit/1b354ba060d7e5b470d9ef8963c1cad1a8adcbe6)) +* moved v2 schemas into a single file ([b6ff867](https://github.com/Salable/node-sdk/commit/b6ff867ab42105f81937f1c6725bfb27c23f00db)) +* removed clean up from tests ([e8d5288](https://github.com/Salable/node-sdk/commit/e8d528883860f96f9669c72bef668190ab6df02f)) + + +### Features + +* cursor pagination on products endpoint ([7e9af6f](https://github.com/Salable/node-sdk/commit/7e9af6f81721705882336e1b91226beed26f4c07)) +* entitlements method ([887e8da](https://github.com/Salable/node-sdk/commit/887e8da7cd3b38909b2f7b14fda56f7ae69cf4f5)) +* paginate features and plans endpoints ([37e3357](https://github.com/Salable/node-sdk/commit/37e335710ba5f4e21c472b3b1000bfa742642334)) +* v3 of salable api added ([9c050b5](https://github.com/Salable/node-sdk/commit/9c050b53253ff5d053482264270108195ae44e0a)) +* wip v3 removed Salable class for initSalable function ([a72d4e1](https://github.com/Salable/node-sdk/commit/a72d4e1c5dc3098e4b9246375b32afed3f5f38f1)) + + +### Tests + +* updated feature schema to include featureEnumOptions ([2b143a0](https://github.com/Salable/node-sdk/commit/2b143a030bd9f87e0c93dba8ceeb60f661a474f1)) + + +### BREAKING CHANGES + +* methods will use v3 of the API. New initSalable function to replace Salable class. + # [4.11.0](https://github.com/Salable/node-sdk/compare/v4.10.0...v4.11.0) (2025-07-08) diff --git a/README.md b/README.md index 16345442..35404b0f 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,16 @@ Let’s walk through setting up a project that uses the Salable API Class from t 1. Create a new Node.js project. 2. Inside of the project, run: `npm install @salable/node-sdk`. Adding packages results in update in lock file, [yarn.lock](https://yarnpkg.com/getting-started/qa/#should-lockfiles-be-committed-to-the-repository) or [package-lock.json](https://docs.npmjs.com/configuring-npm/package-lock-json). You **should** commit your lock file along with your code to avoid potential breaking changes. -## v4.0.0 Update +## v5.0.0 Update The SDK now supports Salable API version selection and developers can choose which version of the Salable API they want to interact with via the SDK -As such, the Salable API version is now a required argument when instantiating the SDK +As such, the Salable API version is now a required argument when instantiating the SDK ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('your_api_key', 'v2'); +const salable = initSalable('your_api_key', 'v3'); ``` -> **_NOTE:_** Support for `v1` of the Salable API has been deprecated, `v2` is currently the only supported version ### General Changes @@ -27,16 +26,16 @@ const salable = new Salable('your_api_key', 'v2'); - Types and method documentation are dynamic and automatically adjust to the version selected ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salableV1 = new Salable('your_api_key', 'v1'); // NOTE: 'v1' is not supported and used for example purposes -const salableV2 = new Salable('your_api_key', 'v2'); +const salableV2 = initSalable('your_api_key', 'v2'); +const salableV3 = initSalable('your_api_key', 'v3'); -// The "licenses.getUsage" method is supported in this version and will work -await salableV1.licenses.getUsage(): +// "licenses.check" method is supported in this version and will work +await salableV2.licenses.check(); -// This will error as "licenses.getUsage" has been deprecated in 'v2' -await salableV2.licenses.getUsage(): // Will error with: "Property 'getUsage' does not exist ..." +// This will error as all "licenses" methods has been deprecated in 'v3' +await salableV3.licenses.check(); // Will error with: "Property 'licenses' does not exist ..." ``` #### Pagination - All methods are now scope authorized and your API Key must contain the appropriate scopes to user certain methods diff --git a/__tests__/_setup/setup-test-envs.ts b/__tests__/_setup/setup-test-envs.ts index 07597b9b..176da719 100644 --- a/__tests__/_setup/setup-test-envs.ts +++ b/__tests__/_setup/setup-test-envs.ts @@ -1,5 +1,5 @@ -import createStripeData from '../../test-utils/stripe/create-stripe-test-data'; -import createTestData from '../../test-utils/scripts/create-test-data'; +import createStripeData from '../../test-utils/scripts/create-stripe-test-data'; +import createSalableTestData from '../../test-utils/scripts/create-salable-test-data'; import { config } from 'dotenv'; import { exec } from 'child_process'; import { promisify } from 'util'; @@ -20,7 +20,7 @@ const globalSetup = async () => { console.log('\n STRIPE ACCOUNT DATA CREATED'); process.env.stripEnvs = JSON.stringify(obj); - await createTestData(obj); + await createSalableTestData(obj); console.log('\n TEST DATA CREATED'); }; diff --git a/__tests__/test-mock-data/capabilities.ts b/__tests__/test-mock-data/capabilities.ts new file mode 100644 index 00000000..3e9c39ec --- /dev/null +++ b/__tests__/test-mock-data/capabilities.ts @@ -0,0 +1,8 @@ +import objectBuilder from './object-builder'; + +// deprecated +export const mockCapability = objectBuilder({ + name: 'test_capability', + status: 'ACTIVE', + description: 'Capability description', +}); diff --git a/__tests__/test-mock-data/coupons.ts b/__tests__/test-mock-data/coupons.ts new file mode 100644 index 00000000..ff986eff --- /dev/null +++ b/__tests__/test-mock-data/coupons.ts @@ -0,0 +1,17 @@ +import { CouponDuration, CouponStatus, DiscountType } from '@prisma/client'; +import objectBuilder from './object-builder'; + +export const mockCoupon = objectBuilder({ + status: 'ACTIVE' as CouponStatus, + createdAt: new Date(), + updatedAt: new Date(), + name: 'Percentage Coupon', + duration: 'ONCE' as CouponDuration, + discountType: 'PERCENTAGE' as DiscountType, + paymentIntegrationCouponId: 'test-payment-integration-id', + percentOff: 10, + expiresAt: null, + maxRedemptions: null, + isTest: false, + durationInMonths: 1, +}); diff --git a/__tests__/test-mock-data/currencies.ts b/__tests__/test-mock-data/currencies.ts new file mode 100644 index 00000000..b35b2ece --- /dev/null +++ b/__tests__/test-mock-data/currencies.ts @@ -0,0 +1,16 @@ +import objectBuilder from './object-builder'; + +export const mockCurrency = objectBuilder({ + shortName: 'XXX', + longName: 'Mock Currency', + symbol: '@', +}); + +export const mockProductCurrency = objectBuilder({ + defaultCurrency: true, +}); + +export const mockPlanCurrency = objectBuilder({ + price: 500, + paymentIntegrationPlanId: 'test-payment-integration-id', +}); diff --git a/__tests__/test-mock-data/event.ts b/__tests__/test-mock-data/event.ts new file mode 100644 index 00000000..a1cca570 --- /dev/null +++ b/__tests__/test-mock-data/event.ts @@ -0,0 +1,11 @@ +import objectBuilder from './object-builder'; +import { EventStatus } from '@prisma/client'; +import { EventType } from '../lib/constants'; + +export const mockSalableEvent = objectBuilder({ + type: EventType.CreateSeats, + organisation: 'xxxxx', + status: EventStatus.pending as EventStatus, + isTest: false, + retries: 0, +}); diff --git a/__tests__/test-mock-data/features.ts b/__tests__/test-mock-data/features.ts new file mode 100644 index 00000000..f811bd25 --- /dev/null +++ b/__tests__/test-mock-data/features.ts @@ -0,0 +1,23 @@ +import objectBuilder from './object-builder'; + +export const mockFeature = objectBuilder({ + name: 'Boolean Feature Name', + description: 'Feature description', + displayName: 'Boolean Feature Display Name', + variableName: 'boolean_feature', + status: 'ACTIVE', + visibility: 'public', + valueType: 'boolean', + defaultValue: 'false', + showUnlimited: false, + sortOrder: 0, +}); + +export const mockPlanFeature = objectBuilder({ + value: 'xxxxx', + isUnlimited: undefined as boolean | undefined, + isUsage: undefined as boolean | undefined, // deprecated + pricePerUnit: 10, // deprecated + minUsage: 1, // deprecated + maxUsage: 100, // deprecated +}); diff --git a/__tests__/test-mock-data/licenses.ts b/__tests__/test-mock-data/licenses.ts new file mode 100644 index 00000000..e5549057 --- /dev/null +++ b/__tests__/test-mock-data/licenses.ts @@ -0,0 +1,38 @@ +import { LicensesUsageRecordType, Prisma } from '@prisma/client'; +import objectBuilder from './object-builder'; + +// deprecated +export const mockLicenseCapability = objectBuilder({ + name: 'Export', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null as string | null, + productUuid: 'c32f26e4-21d9-4456-a1f0-7e76039af518', +}); + +export const mockLicenseUsageRecord = objectBuilder({ + unitCount: 0, + type: 'current' as LicensesUsageRecordType, + resetAt: null as Date | null, + recordedAt: null as Date | null, +}); + +export const mockLicense = objectBuilder({ + name: null as string | null, + email: null as string | null, + status: 'ACTIVE', + granteeId: '123456' as string | null, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined as undefined | { member: string; granteeId: string }, + capabilities: [ + mockLicenseCapability({ name: 'CapabilityOne' }), + mockLicenseCapability({ name: 'CapabilityTwo' }), + ] as Prisma.InputJsonObject[], // deprecated + startTime: undefined as undefined | Date, + endTime: new Date(), + cancelAtPeriodEnd: false, + isTest: false, +}); diff --git a/__tests__/test-mock-data/object-builder.ts b/__tests__/test-mock-data/object-builder.ts new file mode 100644 index 00000000..47d8e3ac --- /dev/null +++ b/__tests__/test-mock-data/object-builder.ts @@ -0,0 +1,32 @@ +type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; + +function isObject(obj?: unknown): obj is object { + return Boolean(obj) && (obj as object)?.constructor === Object; +} + +function merge(target: Record, source?: Record) { + const clone = { ...target } as Record; + if (!source) return clone; + for (const key of Object.keys(source)) { + if (isObject(source[key])) { + clone[key] = merge( + clone[key] as Record, + source[key] as Record + ); + } else { + clone[key] = source[key]; + } + } + + return clone; +} + +export default function objectBuilder(defaultParameters: T) { + return (overrideParameters?: DeepPartial | null): T => { + if (!overrideParameters) overrideParameters = {} as DeepPartial; + return merge( + defaultParameters as Record, + overrideParameters as Record + ) as T; + }; +} diff --git a/__tests__/test-mock-data/optional-type.ts b/__tests__/test-mock-data/optional-type.ts new file mode 100644 index 00000000..4a4634c2 --- /dev/null +++ b/__tests__/test-mock-data/optional-type.ts @@ -0,0 +1 @@ +export type Optional = Pick, K> & Omit; diff --git a/__tests__/test-mock-data/plans.ts b/__tests__/test-mock-data/plans.ts new file mode 100644 index 00000000..88799a31 --- /dev/null +++ b/__tests__/test-mock-data/plans.ts @@ -0,0 +1,27 @@ +import objectBuilder from './object-builder'; + +export const mockPlan = objectBuilder({ + name: 'Free Plan Name', // deprecated + description: 'Free Plan description', + displayName: 'Free Plan Display Name', + slug: 'example-slug', + status: 'ACTIVE', + trialDays: 0, // deprecated + evaluation: false, // deprecated + evalDays: 0, + organisation: 'xxxxx', + visibility: 'public', + licenseType: 'licensed', + interval: 'month', + perSeatAmount: 1, + maxSeatAmount: -1, + length: 1, + active: true, // deprecated + planType: 'Standard', // deprecated + pricingType: 'free', + environment: 'dev', // deprecated + paddlePlanId: null, // deprecated + isTest: false, + hasAcceptedTransaction: false, + archivedAt: null as Date | null, +}); diff --git a/__tests__/test-mock-data/pricing-table.ts b/__tests__/test-mock-data/pricing-table.ts new file mode 100644 index 00000000..699bd1ce --- /dev/null +++ b/__tests__/test-mock-data/pricing-table.ts @@ -0,0 +1,8 @@ +import objectBuilder from './object-builder'; + +export const mockPricingTable = objectBuilder({ + name: 'Sample Pricing Table', + status: 'ACTIVE', + productUuid: 'xxxxxx', + featuredPlanUuid: 'xxxxxx', +}); diff --git a/__tests__/test-mock-data/products.ts b/__tests__/test-mock-data/products.ts new file mode 100644 index 00000000..6f0e3d03 --- /dev/null +++ b/__tests__/test-mock-data/products.ts @@ -0,0 +1,29 @@ +import objectBuilder from './object-builder'; +import { PaymentIntegration, PaymentIntegrationStatus } from '@prisma/client'; + +export const mockProduct = objectBuilder({ + name: 'Sample Product', + description: 'This is a sample product for testing purposes', + logoUrl: 'https://example.com/logo.png', + displayName: 'Sample Product', + organisation: 'xxxxx', + slug: 'example-slug', + status: 'ACTIVE', + paid: false, + appType: 'CUSTOM', // deprecated + isTest: false, + archivedAt: null as null | Date, +}); + +export const mockOrganisationPaymentIntegration = objectBuilder({ + organisation: 'xxxxx', + integrationName: 'stripe_existing' as PaymentIntegration, + accountData: { // deprecated + key: 'xxxxx', + encryptedData: 'xoxox', + }, + isTest: false, + accountName: 'Account Name', + accountId: 'acc_1234', + status: PaymentIntegrationStatus.active, +}); diff --git a/__tests__/test-mock-data/sessions.ts b/__tests__/test-mock-data/sessions.ts new file mode 100644 index 00000000..dab45a2a --- /dev/null +++ b/__tests__/test-mock-data/sessions.ts @@ -0,0 +1,10 @@ +import objectBuilder from './object-builder'; + +export const mockSession = objectBuilder({ + organisationId: 'xxxxx', + expiresAt: new Date(Date.now() + 10800000), + value: 'xxxx', + scope: '', + isTest: true, + metadata: {}, +}); diff --git a/__tests__/test-mock-data/subscriptions.ts b/__tests__/test-mock-data/subscriptions.ts new file mode 100644 index 00000000..a1db02b7 --- /dev/null +++ b/__tests__/test-mock-data/subscriptions.ts @@ -0,0 +1,19 @@ +import objectBuilder from './object-builder'; +import { randomUUID } from 'crypto'; +import { PaymentIntegration } from '@prisma/client'; + +export const mockSubscription = objectBuilder({ + type: 'salable' as PaymentIntegration, + paymentIntegrationSubscriptionId: randomUUID(), + email: null as string | null, + owner: 'xxxxx', + organisation: 'xxxxx', + status: 'ACTIVE', + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + lineItemIds: ['xxxxx'] as string[] | undefined, + isTest: false, + quantity: 1, + cancelAtPeriodEnd: false, +}); diff --git a/commitlint.config.js b/commitlint.config.js index 422b1944..a93a3976 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1 +1,6 @@ -module.exports = { extends: ['@commitlint/config-conventional'] }; +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + "body-max-line-length": [0] + } +}; diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 2829c464..33457da3 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -4,6 +4,91 @@ sidebar_position: 2 # Changelog +## v5.0.0 + +### Breaking Changes + +The `Salable` class has been replaced with a new `initSalable` function. Using the new initialise function will enable +vendors to use `v2` or `v3` of the API within the same version of the SDK. + +**v4.0.0 implementation** +```typescript +const salable = new Salable('your-api-key', 'v2'); +``` +**v5.0.0 implementation** +```typescript +const salableV2 = initSalable('your-api-key', 'v2'); // v2 still supported +const salableV3 = initSalable('your-api-key', 'v3'); +``` + +### V3 Breaking changes + +#### Capabilities deprecated +Capabilities used to be stored on the License at the point of creation with no way of editing them. We found this to be +too rigid, for flexibility we have deprecated capabilities in favour of using the plan's feature values which are +editable in the Salable app. + +##### Deprecated methods that use capabilities +### Capabilities deprecated +Capabilities used to be stored on the License at the point of creation with no way of editing them. Instead, we have now +opted to use the plan's features which allow you to update a grantee’s access on-the-fly through the Salable dashboard. + +#### Deprecated capabilities deprecated +- `plans.capabilities` +- `products.capabilities` +- `licenses.check` + +#### Licenses deprecated +All license methods have been deprecated in favour of managing them through the subscription instead. This gives a +consistent implementation across all types of subscriptions. +- `licenses.create` moved to `subscriptions.create` - the `owner` value will be applied to the `purchaser` field of the license. +- `licenses.check` moved to `entitlements.check` +- `licenses.getAll` moved to `subscriptions.getSeats` +- `licenses.getOne` support removed - fetch the parent subscription instead using `subscriptions.getOne` +- `licenses.getForPurchaser` moved to `subscriptions.getAll` with the owner filter applied. +- `licenses.update` moved to `subscriptions.update` + - To update a seat's grantee use the `subscriptions.manageSeats` method. +- `licenses.updateMany` moved to `subscriptions.manageSeats` +- `licenses.getCount` moved to `subscriptions.getSeatCount` +- `licenses.cancel` moved to `subscriptions.cancel` - this will cancel all the subscription's child licenses. +- `licenses.cancelMany` moved to `subscriptions.cancel` - it is not possible to cancel many subscriptions in the same request. + +#### Other deprecated endpoints +- `products.getFeatures` moved to `features.getAll` with the `productUuid` filter applied. +- `products.getPlans` moved to `plans.getAll` with the `productUuid` filter applied. +- `subscriptions.addSeats` moved to `subscriptions.updateSeatCount` with `increment` set. +- `subscriptions.removeSeats` moved to `subscriptions.updateSeatCount` with `decerement` set. + +#### Affected responses +- `products.getAll` now uses cursor-based pagination in the response. + +### What's new? +#### New methods +- `features.getAll` - Retrieves all features for an organisation. The response uses cursor-based pagination. +- `plans.getAll` - Retrieves all plans for an organisation. The response uses cursor-based pagination. +- `subscriptions.updateSeatCount` - v2 of the API required two different endpoints to add and remove seats on a per-seat subscription. In v3 this has been aligned under one method `subscriptions.updateSeatCount`. +- `entitlements.check` - Check grantee access to specific features (replaces `licenses.check`). + +**v4.0.0 SDK with API v2** +```typescript +const salable = new Salable('your-api-key', 'v2'); +const check = await salable.licenses.check({ + productUuid: 'your-product-uuid', + granteeIds: ['your-grantee-id'], +}); +const hasAccess = check?.capabilities.find((c) => c.capability === 'your-boolean-feature'); +``` + +**v5.0.0 SDK with API v3** +```typescript +const salable = initSalable('your-api-key', 'v3'); +const check = await salable.entitlements.check({ + productUuid: 'your-product-uuid', + granteeIds: ['your-grantee-id'], +}); +const hasAccess = check.features.find((f) => f.feature === 'your-boolean-feature'); +``` + ## v4.0.0 ### Breaking Changes @@ -17,7 +102,7 @@ sidebar_position: 2 - `getOne` and `getForGranteeId` now offer an `expand` option to expand certain properties (e.g. `plan` etc) - `getForPurchaser` no longer offers `cancelLink` as an option - `getUsage` has been deprecated -- `create` and `createMany` are now seperate methods, `status` and `endTime` have been added as optional parameters +- `create` and `createMany` are now separate methods, `status` and `endTime` have been added as optional parameters - `update` method parameters have been changed to have an object as the second parameter, the `granteeId` property is where the grantee ID value can be assigned - `cancelMany` method parameter has been updated to be an object, the `uuids` property is where an array of license UUIDs to cancel can be assigned - `verifyLicenseCheck` has been renamed to `verify` diff --git a/docs/docs/entitlements/_category_.json b/docs/docs/entitlements/_category_.json new file mode 100644 index 00000000..522afcb0 --- /dev/null +++ b/docs/docs/entitlements/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Entitlements", + "position": 2, + "link": { + "type": "generated-index", + "description": "Contains methods for the Entitlements resource" + } +} diff --git a/docs/docs/entitlements/check.md b/docs/docs/entitlements/check.md new file mode 100644 index 00000000..4190e8a1 --- /dev/null +++ b/docs/docs/entitlements/check.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +--- + +# Check Entitlements + +Retrieves the features the grantee(s) have access to. + +## Code Sample + +```typescript +import { initSalable } from '@salable/node-sdk'; + +const salable = initSalable('{{API_KEY}}', 'v3'); + +const check = await salable.entitlements.check({ + productUuid: '{{PRODUCT_UUID}}', + granteeIds: ['userId_1', 'userId_2'] +}); +``` + +## Parameters + +##### productUuid (_required_) + +_Type:_ `string` + +Product `uuid` + +--- + +##### granteeIds (_required_) + +_Type:_ `string[]` + +A String array of the grantee Ids you wish to check against + +## Return Type + +For more information about this request see our API documentation on [Entitlements Check](https://docs.salable.app/api/v3#tag/Entitlements/operation/getEntitlementsCheck) diff --git a/docs/docs/events/get-one.md b/docs/docs/events/get-one.md index df753300..aa199368 100644 --- a/docs/docs/events/get-one.md +++ b/docs/docs/events/get-one.md @@ -9,9 +9,9 @@ Returns a single event ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const event = await salable.events.getOne('431b0c60-a145-4ae4-a7e6-391761b018ba'); ``` @@ -26,4 +26,4 @@ The UUID of the event ## Return Type -For more information about this request see our API documentation on [Event Object](https://docs.salable.app/api/v2#tag/Events/operation/getEventByUuid) +For more information about this request see our API documentation on [Event Object](https://docs.salable.app/api/v3#tag/Events/operation/getEventByUuid) diff --git a/docs/docs/features/_category_.json b/docs/docs/features/_category_.json new file mode 100644 index 00000000..da4a2a41 --- /dev/null +++ b/docs/docs/features/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Features", + "position": 9, + "link": { + "type": "generated-index", + "description": "Contains methods for the Features resource" + } +} diff --git a/docs/docs/features/get-all.md b/docs/docs/features/get-all.md new file mode 100644 index 00000000..dfc0fddb --- /dev/null +++ b/docs/docs/features/get-all.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +--- + +# Get All Features + +Returns a list of features with cursor based pagination. + +## Code Sample + +```typescript +import { initSalable } from '@salable/node-sdk'; + +const salable = initSalable('{{API_KEY}}', 'v3'); + +const features = await salable.features.getAll({ + productUuid: '431b0c60-a145-4ae4-a7e6-391761b018ba' +}); +``` + +## Parameters + +#### options (_required_) + +_Type:_ `GetAllFeaturesOptionsV3` + +| **Parameter** | **Type** | **Description** | **Required** | +|:--------------|:----------------|:----------------------------------------------------|:------------:| +| productUuid | string | The product to retrieve features for | ✅ | +| cursor | string | Cursor value, used for pagination | ❌ | +| take | number | The number of subscriptions to fetch. Default: `20` | ❌ | +| sort | `asc` \| `desc` | Default `asc` | ❌ | + +## Return Type + +For more information about this request see our API documentation on [get all features](https://docs.salable.app/api/v3#tag/Features/operation/getFeatures). diff --git a/docs/docs/licenses/_category_.json b/docs/docs/licenses/_category_.json deleted file mode 100644 index ecd7f061..00000000 --- a/docs/docs/licenses/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Licenses", - "position": 2, - "link": { - "type": "generated-index", - "description": "Contains methods for the License resource" - } -} diff --git a/docs/docs/licenses/cancel-many.md b/docs/docs/licenses/cancel-many.md deleted file mode 100644 index caddba38..00000000 --- a/docs/docs/licenses/cancel-many.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 12 ---- - -# Cancel many Licenses - -This method will cancel many ad hoc Licenses - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -await salable.licenses.cancelMany({uuids: ['c6b04b5b-3a5f-405d-af32-791912adfb53', 'ac4ff75d-714a-4eb3-8d3b-a34fe081c36a']}); -``` - -## Parameters - -##### licenseUuids (_required_) - -_Type:_ `string[]` - -`uuid` array of the Licenses to be canceled - -## Return Type - -For more information about this request see our API documentation on [cancel many Licenses](https://docs.salable.app/api/v2#tag/Licenses/operation/cancelLicenses) diff --git a/docs/docs/licenses/cancel.md b/docs/docs/licenses/cancel.md deleted file mode 100644 index 8b0c8dff..00000000 --- a/docs/docs/licenses/cancel.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 11 ---- - -# Cancel License - -This method will cancel an ad hoc License - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -await salable.licenses.cancel('8ea5c243-7052-4906-acc5-a84690e2cad9'); -``` - -## Parameters - -#### licenseUuid (_required_) - -_Type:_ `string` - -`uuid` of the License to be canceled - -## Return Type - -For more information about this request see our API documentation on [cancel License](https://docs.salable.app/api/v2#tag/Licenses/operation/cancelLicense) diff --git a/docs/docs/licenses/check.md b/docs/docs/licenses/check.md deleted file mode 100644 index f044f145..00000000 --- a/docs/docs/licenses/check.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 10 ---- - -# Check License - -Retrieves the capabilities the grantee(s) have access to. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const check = await salable.licenses.check({ - productUuid: 'product1', - granteeIds: ['grantee_1', 'grantee_2'], -}); -``` - -## Parameters - -#### checkLicenseParams (_required_) - -_Type:_ `CheckLicenseInput` - -| Option | Type | Description | Required | -| -------------------- | -------- | ---------------------------------- | -------- | -| grantproductUuideeId | string | The UUID of the product | ✅ | -| granteeIds | string[] | An array of grantee IDs | ✅ | -| grace | number | Optional grace period to filter by | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Check Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseCheck) diff --git a/docs/docs/licenses/create-many.md b/docs/docs/licenses/create-many.md deleted file mode 100644 index bc754d2a..00000000 --- a/docs/docs/licenses/create-many.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create Many Licenses - -This method creates many ad hoc licenses - -## Code Sample - -### Create Many - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const license = await salable.licenses.createMany([ - { - planUuid: 'fabdea7e-2a9a-4ed8-b3f6-20c029dcbacc', - member: 'orgId_1234', - granteeId: 'userId_1', - status: 'ACTIVE', - endTime: '2025-07-06T12:00:00.000Z', - }, - { - planUuid: '2e2170a9-3750-4176-aaf3-ff1ef12e8f66', - member: 'orgId_1234', - granteeId: 'userId_2', - status: 'ACTIVE', - endTime: '2025-07-06T12:00:00.000Z', - } -]); -``` - -## Parameters - -#### createManyAdHocLicenseParams (_required_) - -_Type:_ `CreateAdhocLicenseInput[]` - -| Option | Type | Description | Required | -| --------- | ------ | --------------------------------------------------------------------------------------------------------------------- | -------- | -| planUuid | string | The UUID of the plan associated with the license. The planUuid can be found on the Plan view in the Salable dashboard | ✅ | -| member | string | The ID of the member who will manage the license. | ✅ | -| granteeId | string | The grantee ID for the license. | ❌ | -| status | string | The status of the created license, e.g. "ACTIVE" "TRIALING" | ❌ | -| endTime | string | Provide a custom end time for the license; this will override the plan's default interval. | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/create.md b/docs/docs/licenses/create.md deleted file mode 100644 index 1fae3875..00000000 --- a/docs/docs/licenses/create.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create License - -This method creates an ad hoc license - -## Code Sample - -### Create one - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const license = await salable.licenses.create({ - planUuid: '60f4f073-f6df-43cf-b394-2d373802863d', - member: 'orgId_1234', - granteeId: 'userId_1', - status: 'ACTIVE', - endTime: '2025-07-06T12:00:00.000Z', -}); -``` - -## Parameters - -#### createAdHocLicenseParams (_required_) - -_Type:_ `CreateAdhocLicenseInput` - -| Option | Type | Description | Required | -| --------- | ------ | --------------------------------------------------------------------------------------------------------------------- | -------- | -| planUuid | string | The UUID of the plan associated with the license. The planUuid can be found on the Plan view in the Salable dashboard | ✅ | -| member | string | The ID of the member who will manage the license. | ✅ | -| granteeId | string | The grantee ID for the license. | ❌ | -| status | string | The status of the created license, e.g. "ACTIVE" "TRIALING" | ❌ | -| endTime | string | Provide a custom end time for the license; this will override the plan's default interval. | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/get-all.md b/docs/docs/licenses/get-all.md deleted file mode 100644 index 85a08abd..00000000 --- a/docs/docs/licenses/get-all.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Get All Licenses - -Returns a list of all the licenses created by your Salable organization - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const licenses = await salable.licenses.getAll(); -``` - -## Parameters - -#### options - -_Type:_ `GetLicenseOptions` - -| Option | Type | Description | Required | -| ---------------- |--------| ----------------------------------------------------------- | -------- | -| status | string | The status of the created license, e.g. "ACTIVE" "TRIALING" | ❌ | -| cursor | string | Cursor value, used for pagination | ❌ | -| take | number | The amount of licenses to fetch | ❌ | -| subscriptionUuid | string | The UUID of the subscription to filter by | ❌ | -| granteeId | string | The grantee ID to filter by | ❌ | -| planUuid | string | The UUID of the plan to filter by | ❌ | -| productUuid | string | The UUID of the product to filter by | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/get-count.md b/docs/docs/licenses/get-count.md deleted file mode 100644 index ec1cbfa7..00000000 --- a/docs/docs/licenses/get-count.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Licenses Count - -This method returns aggregate count number of Licenses. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const licenseCount = await salable.licenses.getCount({subscriptionUuid: '9eeabc1b-cffd-488c-b242-e1fc80c5fc0c', status: 'ACTIVE'}); -``` - -## Parameters - -#### options - -_Type:_ `GetLicenseCountOptions` - -| Option | Type | Description | Required | -| ---------------- | ------ | ------------------------ | -------- | -| subscriptionUuid | string | Filter by subscription | ❌ | -| status | string | Filter by license status | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License count](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicensesCount) diff --git a/docs/docs/licenses/get-for-granteeId.md b/docs/docs/licenses/get-for-granteeId.md deleted file mode 100644 index 09c9bf00..00000000 --- a/docs/docs/licenses/get-for-granteeId.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Get Licenses for a Grantee ID - -Returns licenses for a grantee ID - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const licenses = await salable.licenses.getForGranteeId('da88805a-2802-4062-87d7-2b83ddf8e0ca', { expand: 'plan' }); -``` - -## Parameters - -#### granteeId (_required_) - -_Type:_ `string` - -The grantee ID of the licenses - ---- - -#### options - -_Type:_ `{ expand: string[] }` - -| Option | Type | Description | Required | -| ------ | ------ | --------------------------------------------------------------- | -------- | -| expand | string | Specify which properties to expand. e.g. `{ expand: ['plan'] }` | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/get-for-purchaser.md b/docs/docs/licenses/get-for-purchaser.md deleted file mode 100644 index ba064f75..00000000 --- a/docs/docs/licenses/get-for-purchaser.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Get Licenses for a Purchaser - -Returns licenses for a purchaser on a product - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const licenses = await salable.licenses.getForPurchaser({purchaser: 'purchaser_1', productUuid: 'e7682a81-dd25-4e09-9f64-eebd00194b38', status: 'ACTIVE'}); -``` - -## Parameters - -#### getForPurchaserOptions (_required_) - -_Type:_ `GetPurchasersLicensesOptions` - -| Option | Type | Description | Required | -| ----------- | ------ | ------------------------------------------ | -------- | -| purchaser | string | The purchaser of the licenses to fetch for | ✅ | -| productUuid | string | The UUID of the product | ✅ | -| status | string | Filter by license status | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/get-one.md b/docs/docs/licenses/get-one.md deleted file mode 100644 index 44c1ae3b..00000000 --- a/docs/docs/licenses/get-one.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Get One License - -Returns a single license - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const license = await salable.licenses.getOne('dba43177-43a7-4639-9dba-7b0ff9fcee0a', { expand: 'plan' }); -``` - -## Parameters - -#### licenseUuid (_required_) - -_Type:_ `string` - -The UUID of the license - ---- - -#### options - -_Type:_ `{ expand: string[] }` - -| Option | Type | Description | Required | -| ------ | -------- | -------------------------------------------------------------- | -------- | -| expand | string[] | Specify which properties to expand. e.g. `{ expand: ['plan' }` | ❌ | - -## Return Type - -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/update-many.md b/docs/docs/licenses/update-many.md deleted file mode 100644 index e7c01bfe..00000000 --- a/docs/docs/licenses/update-many.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_position: 9 ---- - -# Update Many Licenses - -This method updates many Licenses with the values passed into the body of the request. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const updatedLicenses = await salable.licenses.updateMany([ - { granteeId: 'userId_1', uuid: '4886d8c4-fbb0-4a68-bf28-2a640269b0f9' }, - { granteeId: 'userId_2', uuid: '65157cf5-cad6-4528-ac13-e5e26733f730' }, -]); -``` - -## Parameters - -### updateManyLicensesParams(_required_) - -_Type:_ `UpdateManyLicenseInput[]` - -| Option | Type | Description | Required | -| --------- | ------ | --------------------------------- | -------- | -| granteeId | string | The new grantee ID value | ✅ | -| uuid | string | The UUID of the license to update | ✅ | - - -## Return Type - -For more information about this request see our API documentation on [licenses object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/licenses/update.md b/docs/docs/licenses/update.md deleted file mode 100644 index c9a7ad25..00000000 --- a/docs/docs/licenses/update.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 8 ---- - -# Update License - -This method updates specific Licenses with the values passed into the body of the request. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const updatedLicense = await salable.licenses.update('e38f0e83-b82d-4f95-a374-6663061456c3', { granteeId: 'updated_grantee_id' }); -``` - -## Parameters - -#### licenseUuid (_required_) - -_Type:_ `string` - -The `uuid` of the license to be updated - ---- - -#### updateLicenseParams (_required_) - -_Type:_ `{ granteeId: string }` - -| Option | Type | Description | Required | -| --------- | -------------- | ---------------------------------------------------------------------------------- | -------- | -| granteeId | string or null | The new grantee ID for the license | ✅ | -| endTime | string | Custom DateTime string for the license which overrides the plan's default interval | ❌ | - -## Return Type - -For more information about this request see our API documentation on [license object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseByUuid) diff --git a/docs/docs/overview.md b/docs/docs/overview.md index a3089e54..dde77637 100644 --- a/docs/docs/overview.md +++ b/docs/docs/overview.md @@ -6,14 +6,14 @@ sidebar_position: 1 Salable is designed to be a flexible tool to allow you to integrate your app with your chosen payment provider easily. The advantage of using Salable is that you can more easily make changes to your pricing structures, and you can offer more options to your customers. Instead of having to go to many places to get the flexibility you need, you can do it all through Salable. -Our Node SDK exposes HTTP endpoints that accept requests with JSON arguments and return JSON responses. Authentication is done via the API key passed to the `Salable` class. +Our Node SDK exposes HTTP endpoints that accept requests with JSON arguments and return JSON responses. Authentication is done via the API key passed to the `initSalable` function. -Specific versions of the Salable API can also be specified as the second argument of the `Salable` constructor function. +Specific versions of the Salable API can also be specified as the second argument of the `initSalable` function. ```ts -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); ``` > NOTE: If you'd like to use test mode, make sure to use an API key generated in test mode (prefixed with `test_`). diff --git a/docs/docs/plans/get-all.md b/docs/docs/plans/get-all.md new file mode 100644 index 00000000..f6071892 --- /dev/null +++ b/docs/docs/plans/get-all.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 1 +--- + +# Get All Plans + +Returns a list of plans with cursor based pagination. + +## Code Sample + +```typescript +import { initSalable } from '@salable/node-sdk'; + +const salable = initSalable('{{API_KEY}}', 'v3'); + +const plans = await salable.plans.getAll(); +``` + +## Parameters + +#### options + +_Type:_ `GetAllPlansOptionsV3` + +| **Parameter** | **Type** | **Description** | **Required** | +|:--------------|:----------------|:------------------------------------------------------------|:------------:| +| cursor | string | Cursor value, used for pagination | ❌ | +| take | number | The number of subscriptions to fetch. Default: `20` | ❌ | +| sort | `asc` \| `desc` | Default `asc` - sorted by `slug` | ❌ | +| archived | boolean | Default response returns both archived and unarchived plans | ❌ | +| productUuid | string | Filter plans by product | ❌ | + +## Return Type + +For more information about this request see our API documentation on [get all plans](https://docs.salable.app/api/v3#tag/Plans/operation/getPlans). diff --git a/docs/docs/plans/get-capabilities.md b/docs/docs/plans/get-capabilities.md deleted file mode 100644 index 7dade305..00000000 --- a/docs/docs/plans/get-capabilities.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Get Capabilities - -Returns a list of all the Capabilities associated with a Plan - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const plan = await salable.plans.getCapabilities('2141fada-4b65-477d-b369-afb24dea94e6'); -``` - -## Parameters - -##### planUuid (_required_) - -_Type:_ `string` - -The `uuid` of the Plan to return the Features from - -## Return Type - -For more information about this request see our API documentation on [Plan Capability Object](https://docs.salable.app/api/v2#tag/Plans/operation/getPlanCapabilities) diff --git a/docs/docs/plans/get-checkout-link.md b/docs/docs/plans/get-checkout-link.md index b14f2f63..91e65f3f 100644 --- a/docs/docs/plans/get-checkout-link.md +++ b/docs/docs/plans/get-checkout-link.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # Get Checkout Link @@ -11,30 +11,30 @@ Returns the checkout link for a plan. This endpoint will only work for paid Plan #### Required parameters ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const checkoutLink = await salable.plans.getCheckoutLink('1de11022-ef14-4e22-94e6-c5b0652e497f', { cancelUrl: 'https://example.com/cancel', successUrl: 'https://example.com/success', granteeId: 'userId-1', - member: 'orgId_1', + owner: 'orgId_1', }); ``` #### Customer details ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}'); +const salable = initSalable('{{API_KEY}}', 'v3'); const checkoutLink = await salable.plans.getCheckoutLink('15914694-5ff1-40d7-8ccb-7acc00586508', { cancelUrl: 'https://example.com/cancel', successUrl: 'https://example.com/success', granteeId: 'userId-1', - member: 'orgId_1', + owner: 'orgId_1', customerEmail: 'person@company.com', }); ``` @@ -58,8 +58,7 @@ Query parameters to be passed in to the checkout config | successUrl | string | The URL to send users if they have successfully completed a purchase | ✅ | | cancelUrl | string | The URL to send users to if the transaction fails. | ✅ | | granteeId | string | Value to use as granteeId on Plan | ✅ | -| member | string | The purchaser of the license | ✅ | -| owner | string | The ID of the entity who will own the subscription. Default is the value given to member. | ❌ | +| owner | string | The ID of the entity who will own the subscription. Default is the value given to member. | ✅ | | promoCode | string | Enables the promo code field in Stripe checkout. Cannot be used with promoCode. | ❌ | | currency | string | Shortname of the currency to be used in the checkout. The currency must be added to the plan's product in Salable. If not specified, it defaults to the currency selected on the product. | ❌ | | quantity | string | Only applicable for per seat plans. Set the amount of seats the customer pays for in the checkout. | ❌ | @@ -68,4 +67,4 @@ Query parameters to be passed in to the checkout config ## Return Type -For more information about this request see our API documentation on [Plan checkout link](https://docs.salable.app/api/v2#tag/Plans/operation/getPlanCheckoutLink) +For more information about this request see our API documentation on [Plan checkout link](https://docs.salable.app/api/v3#tag/Plans/operation/getPlanCheckoutLink) diff --git a/docs/docs/plans/get-currencies.md b/docs/docs/plans/get-currencies.md deleted file mode 100644 index 73b98076..00000000 --- a/docs/docs/plans/get-currencies.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Currencies - -Returns a list of all the Currencies associated with a Plan - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const plan = await salable.plans.getCurrencies('8494c276-ad2d-4341-bba0-f0fd416b7cec'); -``` - -## Parameters - -##### planUuid (_required_) - -_Type:_ `string` - -The `uuid` of the Plan to return the Currencies from - -## Return Type - -For more information about this request see our API documentation on [Plan Currency Object](https://docs.salable.app/api/v2#tag/Plans/operation/getPlanCurrencies) diff --git a/docs/docs/plans/get-features.md b/docs/docs/plans/get-features.md deleted file mode 100644 index e62925be..00000000 --- a/docs/docs/plans/get-features.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Get Features - -Returns a list of all the Features associated with a Plan - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const plan = await salable.plans.getFeatures('003c917a-4c3a-4e67-8d36-adeb22281681'); -``` - -## Parameters - -##### planUuid (_required_) - -_Type:_ `string` - -The `uuid` of the Plan to return the Features from - -## Return Type - -For more information about this request see our API documentation on [Plan Feature Object](https://docs.salable.app/api/v2#tag/Plans/operation/getPlanFeatures) diff --git a/docs/docs/plans/get-one.md b/docs/docs/plans/get-one.md index c92467f2..8fbae70f 100644 --- a/docs/docs/plans/get-one.md +++ b/docs/docs/plans/get-one.md @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 2 --- # Get One Plan @@ -9,11 +9,11 @@ Returns the details of a single plan. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); -const plan = await salable.plans.getOne('f965551b-5070-48df-b3aa-944c7ff876e0', { expand: ['product'] }); +const plan = await salable.plans.getOne('f965551b-5070-48df-b3aa-944c7ff876e0', { expand: ['product', 'features', 'currencies'] }); ``` ## Parameters @@ -37,4 +37,4 @@ _Type:_ `{ expand: string[] }` ## Return Type -For more information about this request see our API documentation on [plan object](https://docs.salable.app/api/v2#tag/Plans/operation/getPlanByUuid) +For more information about this request see our API documentation on [plan object](https://docs.salable.app/api/v3#tag/Plans/operation/getPlanByUuid) diff --git a/docs/docs/pricing-tables/get-one.md b/docs/docs/pricing-tables/get-one.md index be73d0f3..45f3f02f 100644 --- a/docs/docs/pricing-tables/get-one.md +++ b/docs/docs/pricing-tables/get-one.md @@ -11,9 +11,9 @@ Returns all necessary data on a display a pricing table. #### Required parameters ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const pricingTable = await salable.pricingTables.getOne('0c0ee2b7-2f3b-436b-8b4e-b21d0ddbf2a9', { granteeId: 'grantee_1', @@ -42,4 +42,4 @@ _Type:_ `{ granteeId: String, currency: String }` ## Return Type -For more information about this request see our API documentation on [Pricing Table](https://docs.salable.app/api/v2#tag/Pricing-Tables/operation/getPricingTableByUuid) +For more information about this request see our API documentation on [Pricing Table](https://docs.salable.app/api/v3#tag/Pricing-Tables/operation/getPricingTableByUuid) diff --git a/docs/docs/products/get-all.md b/docs/docs/products/get-all.md index e171fe52..2e774406 100644 --- a/docs/docs/products/get-all.md +++ b/docs/docs/products/get-all.md @@ -4,18 +4,18 @@ sidebar_position: 2 # Get All Products -Returns a list of all the products created by your Salable organization +Returns a list of all the products created by your Salable organization. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}'); +const salable = initSalable('{{API_KEY}}', 'v3'); const products = await salable.products.getAll(); ``` ## Return Type -For more information about this request see our API documentation on [Product Object](https://docs.salable.app/api/v2#tag/Products/operation/getProducts) +For more information about this request see our API documentation on [Product Object](https://docs.salable.app/api/v3#tag/Products/operation/getProducts) diff --git a/docs/docs/products/get-capabilities.md b/docs/docs/products/get-capabilities.md deleted file mode 100644 index 6e704854..00000000 --- a/docs/docs/products/get-capabilities.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Get Capabilities - -Returns a list of all the capabilities associated with a product - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}'); - -const currencies = await salable.products.getCapabilities('cc5fcd03-cfd1-471e-819d-2193746f93dd'); -``` - -## Parameters - -#### productUuid (_required_) - -_Type:_ `string` - -The UUID of the Product - -## Return Type - -For more information about this request see our API documentation on [Product Capability Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductCapabilities) diff --git a/docs/docs/products/get-currencies.md b/docs/docs/products/get-currencies.md deleted file mode 100644 index 0e7e057e..00000000 --- a/docs/docs/products/get-currencies.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Get Currencies for a product - -Returns a list of all the currencies associated with a product - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}'); - -const currencies = await salable.products.getCurrencies('1df1f535-4b5c-4948-ac71-71c5e4d3f919'); -``` - -## Parameters - -#### productUuid (_required_) - -_Type:_ `string` - -The UUID of the Product - -## Return Type - -For more information about this request see our API documentation on [Product Currency Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductCurrencies) diff --git a/docs/docs/products/get-features.md b/docs/docs/products/get-features.md deleted file mode 100644 index 502cdf85..00000000 --- a/docs/docs/products/get-features.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Features for a product - -Returns a list of all the features associated with a product - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}'); - -const features = await salable.products.getFeatures('0d300ac7-0fc1-44de-8ee0-5089683b22c2'); -``` - -## Parameters - -#### productUuid (_required_) - -_Type:_ `string` - -The UUID of the Product - -## Return Type - -For more information about this request see our API documentation on [Product Feature Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductFeatures) diff --git a/docs/docs/products/get-one.md b/docs/docs/products/get-one.md index 33fb0c9f..a7009009 100644 --- a/docs/docs/products/get-one.md +++ b/docs/docs/products/get-one.md @@ -9,9 +9,9 @@ Returns the details of a single product. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const product = await salable.products.getOne('3fe29048-28bf-461c-8498-c42c3572359c'); ``` @@ -36,4 +36,4 @@ _Type:_ `GetProductOptions` ## Return Type -For more information about this request see our API documentation on [Product Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductByUuid) +For more information about this request see our API documentation on [Product Object](https://docs.salable.app/api/v3#tag/Products/operation/getProductByUuid) diff --git a/docs/docs/products/get-plans.md b/docs/docs/products/get-plans.md deleted file mode 100644 index c5adfe0b..00000000 --- a/docs/docs/products/get-plans.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Get Plans for a Product - -Returns a list of all the plans associated with a product - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}'); - -const plans = await salable.products.getPlans('054788af-6f2c-4e7a-bc32-3747b6b0d2e6'); -``` - -## Parameters - -#### productUuid (_required_) - -_Type:_ `string` - -The UUID of the Product - -## Return Type - -For more information about this request see our API documentation on [Plan Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductPlans) diff --git a/docs/docs/products/get-pricing-table.md b/docs/docs/products/get-pricing-table.md index 9ed0a99a..60c0c418 100644 --- a/docs/docs/products/get-pricing-table.md +++ b/docs/docs/products/get-pricing-table.md @@ -11,12 +11,12 @@ Returns all necessary data on a Product to be able to display a pricing table. E #### Required parameters ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}'); +const salable = initSalable('{{API_KEY}}'); const pricingTable = await salable.products.getPricingTable('7827727d-6fa9-46e6-b865-172ccda6f5a4', { - granteeId: 'granteeid@email.com', + granteeId: 'userId_1', }); ``` @@ -43,4 +43,4 @@ Below is the list of properties than can be used in the `queryParams` argument. ## Return Type -For more information about this request see our API documentation on [Product Pricing Table Object](https://docs.salable.app/api/v2#tag/Products/operation/getProductPricingTable) +For more information about this request see our API documentation on [Product Pricing Table Object](https://docs.salable.app/api/v3#tag/Products/operation/getProductPricingTable) diff --git a/docs/docs/sessions/create.md b/docs/docs/sessions/create.md index ef8acc37..fd2d4bc5 100644 --- a/docs/docs/sessions/create.md +++ b/docs/docs/sessions/create.md @@ -9,9 +9,9 @@ This methods creates a new session to use with the Salable web components ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const event = await salable.sessions.create({ scope: SessionScope.PricingTable, @@ -34,4 +34,4 @@ _Type:_ `CreateSessionInput` ## Return Type -For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v2#tag/Sessions/operation/createSession) +For more information about this request see our API documentation on [License Object](https://docs.salable.app/api/v3#tag/Sessions/operation/createSession) diff --git a/docs/docs/subscriptions/add-coupon.md b/docs/docs/subscriptions/add-coupon.md index 2b5af49d..7f9a78fe 100644 --- a/docs/docs/subscriptions/add-coupon.md +++ b/docs/docs/subscriptions/add-coupon.md @@ -9,9 +9,9 @@ Adds the specified coupon to the subscription. Adding coupons do not trigger imm ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.addCoupon('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { couponUuid: '4c064ace-57c4-4618-bd79-a0e8029f9904' }); ``` diff --git a/docs/docs/subscriptions/add-seats.md b/docs/docs/subscriptions/add-seats.md deleted file mode 100644 index c176a7e8..00000000 --- a/docs/docs/subscriptions/add-seats.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -sidebar_position: 12 ---- - -# Increment Subscription Seats - -Adds seats to a Subscription. Initially the seats will be unassigned. To assign granteeIds to the seats use the [update many](../licenses/update-many.md) method. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -await salable.subscriptions.addSeats('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { increment: 2 }); -``` - -## Parameters - -#### subscriptionUuid (_required_) - -_Type:_ `string` - -The UUID of the Subscription - -#### Options (_required_) - -_Type:_ `{ increment: number, proration: string }` - -| Option | Type | Description | Required | -| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -| increment | number | The number of seats to be created | ✅ | -| proration | string | `create_prorations`: Will cause proration invoice items to be created when applicable (default). `none`: Disable creating prorations in this request. `always_invoice`: Always invoice immediately for prorations. | ❌ | - -## Return Type - -For more information about this request see our API documentation on [Subscription Seat Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/incrementSubscriptionSeats) diff --git a/docs/docs/subscriptions/cancel.md b/docs/docs/subscriptions/cancel.md index 9b442f43..5267ca0c 100644 --- a/docs/docs/subscriptions/cancel.md +++ b/docs/docs/subscriptions/cancel.md @@ -9,9 +9,9 @@ Cancels a Subscription with options for when it terminates. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.cancel('ce8cc0cb-a180-4d90-985b-0890d5ac6cbb', { when: 'end' }); ``` diff --git a/docs/docs/subscriptions/change-plan.md b/docs/docs/subscriptions/change-plan.md index 56fd3497..43db9d85 100644 --- a/docs/docs/subscriptions/change-plan.md +++ b/docs/docs/subscriptions/change-plan.md @@ -9,9 +9,9 @@ Move a Subscription to a new Plan. Proration behaviour can optionally be set. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const changeSubscriptionPlan = await salable.subscriptions.changePlan('e9e8c539-f2ef-451d-a072-bde07d066a03', { planUuid: 'ce361df2-4555-4259-9349-84e046225d3d', @@ -37,4 +37,4 @@ _Type:_ `SubscriptionsChangePlanOptions` ## Return Type -For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/changeSubscriptionsPlan) +For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/changeSubscriptionsPlan) diff --git a/docs/docs/subscriptions/create.md b/docs/docs/subscriptions/create.md index 49cfecc6..00d9435c 100644 --- a/docs/docs/subscriptions/create.md +++ b/docs/docs/subscriptions/create.md @@ -9,9 +9,9 @@ Create a subscription with no payment integration. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.create({ planUuid: '41192f3a-fcfd-46e2-83db-0fd6a288ad5f', @@ -37,4 +37,4 @@ _Type:_ `CreateSubscriptionInput` ## Return Type -For more information about this request see our API documentation on [Subscription Create](https://docs.salable.app/api/v2#tag/Subscriptions/operation/createSubscripion) \ No newline at end of file +For more information about this request see our API documentation on [Subscription Create](https://docs.salable.app/api/v3#tag/Subscriptions/operation/createSubscripion) \ No newline at end of file diff --git a/docs/docs/subscriptions/get-all.md b/docs/docs/subscriptions/get-all.md index 95087618..04337c63 100644 --- a/docs/docs/subscriptions/get-all.md +++ b/docs/docs/subscriptions/get-all.md @@ -9,9 +9,9 @@ Returns a list of all the subscriptions created by your Salable organization. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getAll(); ``` @@ -36,4 +36,4 @@ _Type:_ `GetSubscriptionOptions` ## Return Type -For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptions) +For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptions) diff --git a/docs/docs/subscriptions/get-cancel-subscription-link.md b/docs/docs/subscriptions/get-cancel-subscription-link.md index 30f1401d..9b620d87 100644 --- a/docs/docs/subscriptions/get-cancel-subscription-link.md +++ b/docs/docs/subscriptions/get-cancel-subscription-link.md @@ -9,9 +9,9 @@ Returns a link to cancel a specific subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getCancelSubscriptionLink('ecc6868e-3ba5-4f10-b955-5dd46beb9602'); ``` @@ -26,4 +26,4 @@ The UUID of the subscription ## Return Type -For more information about this request see our API documentation on [Cancel Subscription Link Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionCancelLink) +For more information about this request see our API documentation on [Cancel Subscription Link Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCancelLink) diff --git a/docs/docs/subscriptions/get-count.md b/docs/docs/subscriptions/get-count.md index 6ede841c..43a2bc62 100644 --- a/docs/docs/subscriptions/get-count.md +++ b/docs/docs/subscriptions/get-count.md @@ -9,9 +9,9 @@ This method returns the aggregate number of seats. The response is broken down b ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const seatCount = await salable.subscriptions.getSeatCount('ef946d3d-f2fa-46f2-96d3-d67162540493'); ``` @@ -26,4 +26,4 @@ _Type:_ `string` ## Return Type -For more information about this request, see our API documentation on [subscription seat count](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionSeatCount). +For more information about this request, see our API documentation on [subscription seat count](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionSeatCount). diff --git a/docs/docs/subscriptions/get-customer-portal-link.md b/docs/docs/subscriptions/get-customer-portal-link.md index c3affaf4..9e4e158f 100644 --- a/docs/docs/subscriptions/get-customer-portal-link.md +++ b/docs/docs/subscriptions/get-customer-portal-link.md @@ -9,9 +9,9 @@ Returns the customer portal link for a subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getOne('a2188e78-2490-408e-93f6-35f829d05b49'); ``` @@ -26,4 +26,4 @@ The UUID of the subscription to be returned ## Return Type -For more information about this request see our API documentation on [Subscription Portal Link Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionCustomerPortalLink) +For more information about this request see our API documentation on [Subscription Portal Link Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCustomerPortalLink) diff --git a/docs/docs/subscriptions/get-invoices.md b/docs/docs/subscriptions/get-invoices.md index 9ad3f0b7..73546d5f 100644 --- a/docs/docs/subscriptions/get-invoices.md +++ b/docs/docs/subscriptions/get-invoices.md @@ -9,9 +9,9 @@ Returns a list of invoices for a subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getInvoices('5fa0fbfa-5fbf-4fee-b286-ed1cb25379f9'); ``` @@ -35,4 +35,4 @@ _Type:_ `GetAllInvoicesOptions` ## Return Type -For more information about this request see our API documentation on [Subscription Invoice Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionInvoices) +For more information about this request see our API documentation on [Subscription Invoice Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionInvoices) diff --git a/docs/docs/subscriptions/get-one.md b/docs/docs/subscriptions/get-one.md index 3793d54c..e846446a 100644 --- a/docs/docs/subscriptions/get-one.md +++ b/docs/docs/subscriptions/get-one.md @@ -9,9 +9,9 @@ Returns the details of a single subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getOne('2694ae7b-8b0e-4954-b7eb-ceceb583a79b'); ``` @@ -36,4 +36,4 @@ _Type:_ `GetSubscriptionOptions` ## Return Type -For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionByUuid) +For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionByUuid) diff --git a/docs/docs/subscriptions/get-payment-link.md b/docs/docs/subscriptions/get-payment-link.md index 68cb2fa8..e27fc524 100644 --- a/docs/docs/subscriptions/get-payment-link.md +++ b/docs/docs/subscriptions/get-payment-link.md @@ -9,9 +9,9 @@ Returns the update payment link for a specific subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getPortalLink('4264d425-697c-4b65-b189-0e747050bfff'); ``` @@ -26,4 +26,4 @@ The UUID of the subscription ## Return Type -For more information about this request see our API documentation on [Subscription Payment Link Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionUpdatePaymentLink) +For more information about this request see our API documentation on [Subscription Payment Link Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionUpdatePaymentLink) diff --git a/docs/docs/subscriptions/get-payment-method.md b/docs/docs/subscriptions/get-payment-method.md index 636b6e60..1af6fed2 100644 --- a/docs/docs/subscriptions/get-payment-method.md +++ b/docs/docs/subscriptions/get-payment-method.md @@ -9,9 +9,9 @@ Returns the payment method used to pay for a subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getPaymentMethod('07b3b494-a8f0-44f7-b051-add30c8c6002'); ``` @@ -26,4 +26,4 @@ The UUID of the subscription ## Return Type -For more information about this request see our API documentation on [Subscription Payment Method Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionPaymentMethod) +For more information about this request see our API documentation on [Subscription Payment Method Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionPaymentMethod) diff --git a/docs/docs/subscriptions/get-seats.md b/docs/docs/subscriptions/get-seats.md index 44102c07..06dc521f 100644 --- a/docs/docs/subscriptions/get-seats.md +++ b/docs/docs/subscriptions/get-seats.md @@ -9,9 +9,9 @@ Returns a list of seats on a subscription. Seats with the status `CANCELED` are ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.getSeats('0dfc9ce9-4dfd-4b20-bfe6-57eacbe45389'); ``` @@ -30,4 +30,4 @@ _Type:_ `GetSubscriptionSeatsOptions` ## Return Type -For more information about this request, see our API documentation on [get subscription seats](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionsSeats). +For more information about this request, see our API documentation on [get subscription seats](https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionsSeats). diff --git a/docs/docs/subscriptions/get-user-plans.md b/docs/docs/subscriptions/get-user-plans.md deleted file mode 100644 index 16b7271b..00000000 --- a/docs/docs/subscriptions/get-user-plans.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Get Switchable Plans for a Subscribed User - -Returns the details of a single subscription. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -const subscription = await salable.subscriptions.getSwitchablePlans('e0517f96-1ac0-4631-a52b-56ace9d1168c'); -``` - -## Parameters - -#### subscriptionUuid (_required_) - -_Type:_ `string` - -The UUID of the subscription - -## Return Type - -For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionUpdatablePlans) diff --git a/docs/docs/subscriptions/manage-seats.md b/docs/docs/subscriptions/manage-seats.md index eefafb8b..1f604e24 100644 --- a/docs/docs/subscriptions/manage-seats.md +++ b/docs/docs/subscriptions/manage-seats.md @@ -9,9 +9,9 @@ Assign, unassign and replace grantees on seats. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.manageSeats('17830730-3214-4dda-8306-9bb8ae0e3a11', [ { @@ -50,4 +50,4 @@ _Type:_ `ManageSeatOptions[]` ## Return Type -For more information about this request, see our API documentation on [subscription manage seats](https://docs.salable.app/api/v2#tag/Subscriptions/operation/manageSubscriptionSeats) \ No newline at end of file +For more information about this request, see our API documentation on [subscription manage seats](https://docs.salable.app/api/v3#tag/Subscriptions/operation/manageSubscriptionSeats) \ No newline at end of file diff --git a/docs/docs/subscriptions/reactivate.md b/docs/docs/subscriptions/reactivate.md index 478d0e9a..5f2a3360 100644 --- a/docs/docs/subscriptions/reactivate.md +++ b/docs/docs/subscriptions/reactivate.md @@ -9,9 +9,9 @@ This method reactivates a subscription scheduled for cancellation before the bil ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const subscription = await salable.subscriptions.reactivateSubscription('9237877c-baae-46d0-b482-cb0147179e30'); ``` diff --git a/docs/docs/subscriptions/remove-coupon.md b/docs/docs/subscriptions/remove-coupon.md index cf80d46a..28642e59 100644 --- a/docs/docs/subscriptions/remove-coupon.md +++ b/docs/docs/subscriptions/remove-coupon.md @@ -9,9 +9,9 @@ Removes the specified coupon from the subscription. Removing coupons do not trig ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.removeCoupon('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { couponUuid: '4c064ace-57c4-4618-bd79-a0e8029f9904' }); ``` diff --git a/docs/docs/subscriptions/remove-seats.md b/docs/docs/subscriptions/remove-seats.md deleted file mode 100644 index 1cd9fcbe..00000000 --- a/docs/docs/subscriptions/remove-seats.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -sidebar_position: 13 ---- - -# Remove Subscription Seats - -Remove seats from a Subscription. Seats can only be removed if they are unassigned. To unassign seats use the [update many](../licenses/update-many.md) method to set the `granteeId` of each seat to `null`. - -## Code Sample - -```typescript -import { Salable } from '@salable/node-sdk'; - -const salable = new Salable('{{API_KEY}}', 'v2'); - -await salable.subscriptions.removeSeats('17830730-3214-4dda-8306-9bb8ae0e3a11', { decrement: 1 }); -``` - -## Parameters - -#### subscriptionUuid (_required_) - -_Type:_ `string` - -The UUID of the Subscription - -#### Options (_required_) - -_Type:_ `RemoveSubscriptionSeatsOption` - -| Option | Type | Description | Required | -| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -| decrement | number | The number of seats to be created | ✅ | -| proration | string | `create_prorations`: Will cause proration invoice items to be created when applicable (default). `none`: Disable creating prorations in this request. `always_invoice`: Always invoice immediately for prorations. | ❌ | - -## Return Type - -For more information about this request see our API documentation on [Subscription Seat Object](https://docs.salable.app/api/v2#tag/Subscriptions/operation/decrementSubscriptionSeats) diff --git a/docs/docs/subscriptions/update-seat-count.md b/docs/docs/subscriptions/update-seat-count.md new file mode 100644 index 00000000..c46d9d24 --- /dev/null +++ b/docs/docs/subscriptions/update-seat-count.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 12 +--- + +# Update Subscription Seat Count + + +## Add seats + +Increase a subscription's seat count. If the subscription's plan has a max seat limit you will not be able to exceed this. All created seats will be unassigned, to assign them use the [./subscriptions/manage-seats.md](manage seats) method. + +### Code Sample + +```typescript +import { initSalable } from '@salable/node-sdk'; + +const salable = initSalable('{{API_KEY}}', 'v3'); + +await salable.subscriptions.updateSeatCount('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { increment: 2 }); +``` + +### Parameters + +#### subscriptionUuid (_required_) + +_Type:_ `string` + +The UUID of the Subscription + +#### Options (_required_) + +_Type:_ `{ increment: number, proration?: string }` + +| Option | Type | Description | Required | +| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | +| increment | number | The number of seats to be created | ✅ | +| proration | string | `create_prorations`: Will cause proration invoice items to be created when applicable (default). `none`: Disable creating prorations in this request. `always_invoice`: Always invoice immediately for prorations. | ❌ | + +### Return Type + +For more information about this request see our API documentation on [Subscription Seat Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/updateSubscriptionSeatCount) + +## Remove seats + +Decrease a subscription's seat count. If the subscription's plan has a minimum seat limit you will not be able to go below this. Only unassigned seats can be removed, to unassign seats use the [./subscriptions/manage-seats.md](manage seats) method. + +### Code Sample + +```typescript +import { initSalable } from '@salable/node-sdk'; + +const salable = initSalable('{{API_KEY}}', 'v3'); + +await salable.subscriptions.updateSeatCount('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { decrement: 2 }); +``` + +### Parameters + +#### subscriptionUuid (_required_) + +_Type:_ `string` + +The UUID of the Subscription + +#### Options (_required_) + +_Type:_ `{ decrement: number, proration?: string }` + +| Option | Type | Description | Required | +|-----------| ------ |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | +| decrement | number | The number of seats that will be removed | ✅ | +| proration | string | `create_prorations`: Will cause proration invoice items to be created when applicable (default). `none`: Disable creating prorations in this request. `always_invoice`: Always invoice immediately for prorations. | ❌ | + +### Return Type + +For more information about this request see our API documentation on [Subscription Seat Object](https://docs.salable.app/api/v3#tag/Subscriptions/operation/updateSubscriptionSeatCount) diff --git a/docs/docs/subscriptions/update.md b/docs/docs/subscriptions/update.md index 0bdb044b..23741c9e 100644 --- a/docs/docs/subscriptions/update.md +++ b/docs/docs/subscriptions/update.md @@ -9,9 +9,9 @@ Update properties on a subscription. ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.subscriptions.update('17830730-3214-4dda-8306-9bb8ae0e3a11', { owner: 'orgId_2' }); ``` @@ -34,4 +34,4 @@ _Type:_ `UpdateSubscriptionInput` ## Return Type -For more information about this request see our API documentation on [Subscription update](https://docs.salable.app/api/v2#tag/Subscriptions/operation/updateSubscriptionByUuid) +For more information about this request see our API documentation on [Subscription update](https://docs.salable.app/api/v3#tag/Subscriptions/operation/updateSubscriptionByUuid) diff --git a/docs/docs/usage/get-record-for-plan.md b/docs/docs/usage/get-record-for-plan.md index 8282c643..93e4a5f6 100644 --- a/docs/docs/usage/get-record-for-plan.md +++ b/docs/docs/usage/get-record-for-plan.md @@ -9,9 +9,9 @@ Returns the currency usage record for a metered license ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const records = await salable.usage.getCurrentUsageRecord({ granteeId: 'grantee_1', @@ -32,4 +32,4 @@ _Type:_ `CurrentUsageOptions` ## Return Type -For more information about this request see our API documentation on [Usage Record Object](https://docs.salable.app/api/v2#tag/Usage/operation/getCurrentLicenseUsage) +For more information about this request see our API documentation on [Usage Record Object](https://docs.salable.app/api/v3#tag/Usage/operation/getCurrentLicenseUsage) diff --git a/docs/docs/usage/get-records.md b/docs/docs/usage/get-records.md index 1cecb30e..ca75ee13 100644 --- a/docs/docs/usage/get-records.md +++ b/docs/docs/usage/get-records.md @@ -9,9 +9,9 @@ Returns a list of all the usage records for grantee's metered licenses ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); const records = await salable.usage.getAllUsageRecords({ granteeId: 'grantee_1' @@ -37,4 +37,4 @@ _Type:_ `GetLicenseOptions` ## Return Type -For more information about this request see our API documentation on [Usage Record Object](https://docs.salable.app/api/v2#tag/Usage/operation/getLicenseUsage) +For more information about this request see our API documentation on [Usage Record Object](https://docs.salable.app/api/v3#tag/Usage/operation/getLicenseUsage) diff --git a/docs/docs/usage/update.md b/docs/docs/usage/update.md index 1dbe301d..24b445d1 100644 --- a/docs/docs/usage/update.md +++ b/docs/docs/usage/update.md @@ -9,9 +9,9 @@ Increments usage count on a License ## Code Sample ```typescript -import { Salable } from '@salable/node-sdk'; +import { initSalable } from '@salable/node-sdk'; -const salable = new Salable('{{API_KEY}}', 'v2'); +const salable = initSalable('{{API_KEY}}', 'v3'); await salable.usage.updateLicenseUsage({ granteeId: 'grantee_1', diff --git a/package-lock.json b/package-lock.json index ff4b15d3..9d12dcfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@salable/node-sdk", - "version": "4.8.0", + "version": "4.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@salable/node-sdk", - "version": "4.8.0", + "version": "4.11.0", "license": "MIT", "devDependencies": { "@aws-sdk/client-kms": "^3.682.0", diff --git a/package.json b/package.json index 03f902a5..d962e495 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salable/node-sdk", - "version": "4.11.0", + "version": "5.0.0", "description": "Node.js SDK to interact with Salable APIs", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fdaf7dfe..130abd63 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,6 @@ generator client { model Product { uuid String @id @default(uuid()) - name String description String? @db.Text logoUrl String? @db.Text displayName String @@ -22,33 +21,34 @@ model Product { organisationPaymentIntegration OrganisationPaymentIntegration? @relation(fields: [organisationPaymentIntegrationUuid], references: [uuid], onUpdate: NoAction) organisationPaymentIntegrationUuid String @default("free") paymentIntegrationProductId String? - appType String @default("custom") + successUrl String? @db.Text + cancelUrl String? @db.Text + updatedAt DateTime @default(now()) @updatedAt + isTest Boolean @default(false) + archivedAt DateTime? plans Plan[] - capabilities Capability[] features Feature[] pricingTables PricingTable[] licenses License[] subscriptions Subscription[] currencies CurrenciesOnProduct[] - updatedAt DateTime @default(now()) @updatedAt - archivedAt DateTime? - isTest Boolean @default(false) coupons Coupon[] + name String /// @deprecated + appType String @default("custom") /// @deprecated + capabilities Capability[] /// @deprecated + @@index([organisation]) @@index([organisationPaymentIntegrationUuid]) } model Plan { uuid String @id @default(uuid()) - name String description String? @db.Text displayName String slug String @default("") status String isTest Boolean @default(false) - trialDays Int? - evaluation Boolean @default(false) evalDays Int @default(0) organisation String visibility String @@ -57,30 +57,35 @@ model Plan { maxSeatAmount Int @default(-1) interval String length Int - active Boolean - planType String pricingType String @default("free") - environment String - paddlePlanId Int? + archivedAt DateTime? + updatedAt DateTime @default(now()) @updatedAt + hasAcceptedTransaction Boolean @default(false) product Product @relation(fields: [productUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) productUuid String featuredOnPricingTable PricingTable? currencies CurrenciesOnPlans[] features FeaturesOnPlans[] - usage LicensesUsageOnPlans[] pricingTables PlansOnPricingTables[] - capabilities CapabilitiesOnPlans[] licenses License[] subscription Subscription[] - salablePlan Boolean @default(false) - updatedAt DateTime @default(now()) @updatedAt - flags FlagsOnPlans[] - hasAcceptedTransaction Boolean @default(false) coupons CouponsOnPlans[] licensesUsage LicensesUsage[] - archivedAt DateTime? + + planType String /// @deprecated + name String /// @deprecated + trialDays Int? /// @deprecated use `evalDays` instead + evaluation Boolean @default(false) /// @deprecated + active Boolean /// @deprecated use `status` instead + environment String /// @deprecated + paddlePlanId Int? /// @deprecated + salablePlan Boolean @default(false) /// @deprecated + usage LicensesUsageOnPlans[] /// @deprecated + capabilities CapabilitiesOnPlans[] /// @deprecated + flags FlagsOnPlans[] /// @deprecated } +/// @deprecated model Capability { uuid String @id @default(uuid()) name String @@ -96,7 +101,6 @@ model Capability { model Feature { uuid String @id @default(uuid()) - name String description String? @db.Text displayName String variableName String? @default("") @@ -105,14 +109,16 @@ model Feature { valueType String @default("numerical") defaultValue String @default("0") showUnlimited Boolean @default(false) + updatedAt DateTime @default(now()) @updatedAt + sortOrder Int @default(0) product Product? @relation(fields: [productUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) productUuid String? plans FeaturesOnPlans[] featureEnumOptions FeatureEnumOption[] pricingTables FeaturesOnPricingTables[] - updatedAt DateTime @default(now()) @updatedAt - licenses FeaturesOnLicenses[] - sortOrder Int @default(0) + + name String /// @deprecated + licenses FeaturesOnLicenses[] /// @deprecated @@index([productUuid]) } @@ -137,12 +143,13 @@ model FeaturesOnPlans { enumValue FeatureEnumOption? @relation(fields: [enumValueUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) enumValueUuid String? isUnlimited Boolean @default(false) - isUsage Boolean @default(false) - pricePerUnit Float? - minUsage Int? - maxUsage Int? updatedAt DateTime @default(now()) @updatedAt + isUsage Boolean @default(false) /// @deprecated + pricePerUnit Float? /// @deprecated + minUsage Int? /// @deprecated + maxUsage Int? /// @deprecated + @@id([planUuid, featureUuid]) @@index([planUuid]) @@index([featureUuid]) @@ -164,21 +171,22 @@ model FeaturesOnPricingTables { model PricingTable { uuid String @id @default(uuid()) - name String status String @default("ACTIVE") - title String? - text String? @db.Text - theme String @default("light") featureOrder String @default("default") product Product @relation(fields: [productUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) productUuid String plans PlansOnPricingTables[] features FeaturesOnPricingTables[] - customTheme Json? featuredPlan Plan? @relation(fields: [featuredPlanUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) featuredPlanUuid String? @unique updatedAt DateTime @default(now()) @updatedAt + name String /// @deprecated + title String? /// @deprecated + text String? @db.Text /// @deprecated + theme String @default("light") /// @deprecated + customTheme Json? /// @deprecated + @@index([productUuid]) } @@ -236,30 +244,31 @@ model Session { } model License { - uuid String @id @default(uuid()) - name String? - email String? - subscription Subscription? @relation(fields: [subscriptionUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) - subscriptionUuid String? - status String - granteeId String? - paymentService String - purchaser String - type String - product Product @relation(fields: [productUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) - productUuid String - plan Plan @relation(fields: [planUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) - planUuid String - capabilities Json - metadata Json? - startTime DateTime @default(now()) - endTime DateTime - updatedAt DateTime @default(now()) @updatedAt + uuid String @id @default(uuid()) + subscription Subscription? @relation(fields: [subscriptionUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) + subscriptionUuid String? + status String + granteeId String? + paymentService String + purchaser String + type String + product Product @relation(fields: [productUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) + productUuid String + plan Plan @relation(fields: [planUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) + planUuid String + startTime DateTime @default(now()) + endTime DateTime + updatedAt DateTime @default(now()) @updatedAt + usage LicensesUsageOnPlans[] + usageRecords LicensesUsage[] + isTest Boolean @default(false) + + name String? /// @deprecated + email String? /// @deprecated Use subscription `email` instead + cancelAtPeriodEnd Boolean @default(false) /// @deprecated Use subscription `cancelAtPeriodEnd` instead. features FeaturesOnLicenses[] /// @deprecated Use `usage` instead. - usage LicensesUsageOnPlans[] - usageRecords LicensesUsage[] - isTest Boolean @default(false) - cancelAtPeriodEnd Boolean @default(false) + capabilities Json /// @deprecated + metadata Json? /// @deprecated @@index([status, paymentService]) @@index([productUuid]) @@ -369,13 +378,14 @@ model OrganisationPaymentIntegration { products Product[] webhooks Webhook[] accountName String - accountData Json status PaymentIntegrationStatus? accountId String? updatedAt DateTime @default(now()) @updatedAt isTest Boolean @default(false) newPaymentEnabled Boolean @default(false) + accountData Json /// @deprecated + @@index([accountId]) } @@ -434,6 +444,7 @@ model LicensesUsageOnPlans { @@index([planUuid]) } +/// @deprecated model FeaturesOnLicenses { license License @relation(fields: [licenseUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) licenseUuid String @@ -768,4 +779,4 @@ model WebhookEventAttempt { scheduledAt DateTime? organisation String sentCount Int @default(0) -} \ No newline at end of file +} diff --git a/src/constants.ts b/src/constants.ts index daacdc01..f5d49c47 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,31 +11,11 @@ export const RESOURCE_NAMES = { PRICING_TABLES: 'pricing-tables', PRODUCTS: 'products', USAGE: 'usage', + FEATURES: 'features', + ENTITLEMENTS: 'entitlements', RBAC: { PERMISSIONS: 'rbac/permissions', ROLES: 'rbac/roles', USERS: 'rbac/users', }, }; - -export const allowedPlanCheckoutParams = [ - 'successUrl', - 'cancelUrl', - 'granteeId', - 'member', - 'customerCountry', - 'customerEmail', - 'customerPostcode', - 'couponCode', - 'promoCode', - 'allowPromoCode', - 'marketingConsent', - 'vatCity', - 'vatCompanyName', - 'vatCountry', - 'vatNumber', - 'vatPostcode', - 'vatState', - 'vatStreet', - 'customMessage', -]; diff --git a/src/entitlements/index.ts b/src/entitlements/index.ts new file mode 100644 index 00000000..6593d783 --- /dev/null +++ b/src/entitlements/index.ts @@ -0,0 +1,20 @@ +import { TVersion, Version, EntitlementCheck } from '../types'; + +export type EntitlementVersions = { + [Version.V3]: { + /** + * Check entitlements + * + * @param {string[]} granteeIds - The UUIDs of the grantee to be checked + * @param {string} productUuid - The UUID of the product to be checked + * + * @returns { Promise} + */ + check: (options: { + granteeIds: string[], + productUuid: string + }) => Promise; + }; +}; + +export type EntitlementVersionedMethods = V extends keyof EntitlementVersions ? EntitlementVersions[V] : never; \ No newline at end of file diff --git a/src/entitlements/v3/entitlements-v3.test.ts b/src/entitlements/v3/entitlements-v3.test.ts new file mode 100644 index 00000000..8bfb4e31 --- /dev/null +++ b/src/entitlements/v3/entitlements-v3.test.ts @@ -0,0 +1,202 @@ +import { addMonths } from 'date-fns'; +import { initSalable } from '../..'; +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; + +const productUuid = randomUUID(); +const granteeId = randomUUID(); +const featureEnumUuid = randomUUID(); + +describe('Entitlements v3', () => { + const salable = initSalable(testUuids.devApiKeyV3, 'v3') + beforeAll(async () => { + await generateTestData() + }) + it('check: return correct features', async () => { + const entitlements = await salable.entitlements.check({ + granteeIds: [granteeId], + productUuid, + }) + expect(entitlements).toEqual({ + signature: expect.any(String), + features: expect.arrayContaining([ + { + expiry: expect.any(String), + feature: 'boolean', + }, + { + expiry: expect.any(String), + feature: 'plan_display_name', + }, + { + expiry: expect.any(String), + feature: 'text_options:access', + }, + { + expiry: expect.any(String), + feature: 'numerical:1', + }, + { + expiry: expect.any(String), + feature: 'unlimited_numerical:100', + } + ]) + }); + }) +}); + +const generateTestData = async () => { + const product = await prismaClient.product.create({ + data: { + uuid: productUuid, + name: 'Sample Product', + description: 'This is a sample product for testing purposes', + logoUrl: 'https://example.com/logo.png', + displayName: 'Sample Product', + organisation: testUuids.organisationId, + status: 'ACTIVE', + paid: false, + appType: 'CUSTOM', + features: { + createMany: { + data: [ + { + name: 'boolean', + displayName: 'Boolean', + sortOrder: 0, + variableName: 'boolean', + defaultValue: 'true', + visibility: 'public', + showUnlimited: false, + status: 'ACTIVE', + valueType: 'boolean', + }, + { + uuid: featureEnumUuid, + name: 'text_options', + displayName: 'Text options', + sortOrder: 1, + variableName: 'text_options', + valueType: 'enum', + defaultValue: 'Access', + visibility: 'public', + showUnlimited: false, + status: 'ACTIVE', + }, + { + name: 'numerical', + displayName: 'Numerical', + sortOrder: 2, + variableName: 'numerical', + valueType: 'numerical', + defaultValue: '50', + visibility: 'public', + showUnlimited: false, + status: 'ACTIVE', + }, + { + name: 'unlimited_numerical', + displayName: 'Numerical unlimited', + sortOrder: 3, + variableName: 'unlimited_numerical', + valueType: 'numerical', + defaultValue: 'unlimited', + visibility: 'public', + showUnlimited: false, + status: 'ACTIVE', + }, + ] + }, + }, + }, + include: { features: true }, + }); + const enumOption = await prismaClient.featureEnumOption.create({ + data: { + featureUuid: featureEnumUuid, + name: 'Access', + } + }) + const plan = await prismaClient.plan.create({ + data: { + organisation: testUuids.organisationId, + pricingType: 'free', + licenseType: 'licensed', + perSeatAmount: 1, + name: '', + description: '', + slug: 'plan_display_name', + displayName: 'Plan Display Name', + product: { connect: { uuid: product.uuid } }, + status: 'ACTIVE', + trialDays: 0, + evaluation: false, + evalDays: 0, + interval: 'month', + length: 1, + active: true, + planType: 'Standard', + environment: 'dev', + paddlePlanId: null, + maxSeatAmount: -1, + visibility: 'public', + features: { + createMany: { + data: product.features.map((f) => { + const getValue = (name: string) => { + switch (name) { + case 'boolean' : + return 'true'; + case 'text_options': + return 'Access'; + case 'numerical': + return '1'; + case 'unlimited_numerical': + return '100' + default: + throw new Error('Value not found') + } + } + return { + value: getValue(f.name), + featureUuid: f.uuid, + ...(f.name === 'text_options' && { enumValueUuid: enumOption.uuid }) + }; + }), + }, + }, + }, + }); + await prismaClient.subscription.create({ + data: { + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + productUuid, + planUuid: plan.uuid, + createdAt: new Date(), + owner: randomUUID(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + purchaser: 'tester@testing.com', + metadata: undefined, + paymentService: 'ad-hoc', + granteeId, + type: 'licensed', + planUuid: plan.uuid, + productUuid, + capabilities: [], + endTime: addMonths(new Date(), 1) + } + } + } + }) +}; diff --git a/src/entitlements/v3/index.ts b/src/entitlements/v3/index.ts new file mode 100644 index 00000000..569a8ac4 --- /dev/null +++ b/src/entitlements/v3/index.ts @@ -0,0 +1,10 @@ +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import { EntitlementVersions } from '..'; +import getUrl from '../../utils/get-url'; +import { ApiRequest } from '../../types'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.ENTITLEMENTS}`; + +export const v3EntitlementMethods = (request: ApiRequest): EntitlementVersions['v3'] => ({ + check: (options) => request(getUrl(`${baseUrl}/check`, options), { method: 'GET' }), +}); \ No newline at end of file diff --git a/src/events/index.ts b/src/events/index.ts index de92847b..021fae8a 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,5 +1,4 @@ -import { ApiRequest, TVersion, Version, Event } from '../types'; -import { v2EventMethods } from './v2'; +import { TVersion, Version, Event } from '../types'; export type EventVersions = { [Version.V2]: { @@ -12,15 +11,7 @@ export type EventVersions = { */ getOne: (uuid: string) => Promise; }; + [Version.V3]: EventVersions['v2']; }; -export type EventVersionedMethods = V extends keyof EventVersions ? EventVersions[V] : never; - -export const eventsInit = (version: V, request: ApiRequest): EventVersionedMethods => { - switch (version) { - case Version.V2: - return v2EventMethods(request) as EventVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; +export type EventVersionedMethods = V extends keyof EventVersions ? EventVersions[V] : never; \ No newline at end of file diff --git a/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index b0247765..8a239bb1 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,40 +1,39 @@ -import Salable from '../..'; -import { Event, EventTypeEnum, Version } from '../../types'; +import { initSalable, TVersion, Version, VersionedMethodsReturn } from '../..'; import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; -import { v4 as uuidv4 } from 'uuid'; -import { EventStatus } from '@prisma/client'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; +import { EventSchema } from '../../schemas/v2/schemas-v2'; -const version = Version.V2; - -const eventUuid = uuidv4(); - -describe('Events V2 Tests', () => { - const salable = new Salable(testUuids.devApiKeyV2, version); +const eventUuid = randomUUID(); +describe('Events Tests for v2, v3', () => { + const salableVersions = {} as Record> + const versions: {version: TVersion; scopes: string[]}[] = [ + { version: Version.V2, scopes: ['events:read'] }, + { version: Version.V3, scopes: ['events:read'] } + ]; beforeAll(async () => { await generateTestData(); + for (const {version, scopes} of versions) { + const value = randomUUID() + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key', + organisation: testUuids.organisationId, + value, + scopes: JSON.stringify(scopes), + status: 'ACTIVE', + }, + }); + salableVersions[version] = initSalable(value, version); + } }); - - it('getOne: Should successfully fetch the specified event', async () => { - const data = await salable.events.getOne(eventUuid); - expect(data).toEqual(eventSchema); + it.each(versions)('getOne: Should successfully fetch the specified event', async ({ version }) => { + const data = await salableVersions[version].events.getOne(eventUuid); + expect(data).toEqual(EventSchema); }); }); -const eventSchema: Event = { - uuid: expect.any(String), - type: expect.toBeOneOf(Object.values(EventTypeEnum)) as EventTypeEnum, - organisation: expect.any(String), - status: expect.toBeOneOf(Object.values(EventStatus)) as EventStatus, - isTest: expect.any(Boolean), - retries: expect.any(Number), - errorMessage: expect.toBeOneOf([expect.any(String), null]), - errorCode: expect.toBeOneOf([expect.any(String), null]), - createdAt: expect.any(String), - updatedAt: expect.any(String), -}; - const generateTestData = async () => { await prismaClient.event.create({ data: { diff --git a/src/events/v2/index.ts b/src/events/v2/index.ts index 3027f70a..0f8fe687 100644 --- a/src/events/v2/index.ts +++ b/src/events/v2/index.ts @@ -7,4 +7,4 @@ const baseUrl = `${SALABLE_BASE_URL}/events`; export const v2EventMethods = (request: ApiRequest): EventVersions['v2'] => ({ getOne: (uuid) => request(getUrl(`${baseUrl}/${uuid}`), { method: 'GET' }), -}); +}); \ No newline at end of file diff --git a/src/features/index.ts b/src/features/index.ts new file mode 100644 index 00000000..2d9651f8 --- /dev/null +++ b/src/features/index.ts @@ -0,0 +1,16 @@ +import { GetAllFeaturesOptionsV3, GetAllFeaturesV3, TVersion, Version } from '../types'; + +export type FeatureVersions = { + [Version.V3]: { + /** + * Get all features + * + * @param GetAllFeaturesOptionsV3 + * + * @returns { Promise} + */ + getAll: (options: GetAllFeaturesOptionsV3) => Promise; + }; +}; + +export type FeatureVersionedMethods = V extends keyof FeatureVersions ? FeatureVersions[V] : never; \ No newline at end of file diff --git a/src/features/v3/features-v3.test.ts b/src/features/v3/features-v3.test.ts new file mode 100644 index 00000000..0f2e166a --- /dev/null +++ b/src/features/v3/features-v3.test.ts @@ -0,0 +1,175 @@ +import { initSalable } from '../..'; +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { Feature, FeatureEnumOption, Product } from '@prisma/client'; +import { FeatureSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { EnumValueSchema } from '../../schemas/v2/schemas-v2'; +import { mockFeature } from '../../../__tests__/test-mock-data/features'; +import { mockProduct } from '../../../__tests__/test-mock-data/products'; + +describe('Features v3', () => { + const salable = initSalable(testUuids.devApiKeyV3, 'v3') + let product: Product; + let booleanFeature: Feature; + let enumFeature: Feature; + let numberFeature: Feature; + beforeAll(async () => { + const data = await generateTestData(testUuids.organisationId); + product = data.product; + booleanFeature = data.booleanFeature; + enumFeature = data.enumFeature; + numberFeature = data.numberFeature; + }) + it('getAll: return correct features', async () => { + const features = await salable.features.getAll({ + productUuid: product.uuid, + }) + expect(features).toEqual( + { + first: booleanFeature.uuid, + last: numberFeature.uuid, + data: [ + { + ...FeatureSchemaV3, + uuid: booleanFeature.uuid, + sortOrder: 0, + featureEnumOptions: [], + }, + { + ...FeatureSchemaV3, + uuid: enumFeature.uuid, + sortOrder: 1, + featureEnumOptions: expect.arrayContaining([ + { ...EnumValueSchema, name: 'Option 0' }, + { ...EnumValueSchema, name: 'Option 1' }, + { ...EnumValueSchema, name: 'Option 2' }, + ]) as FeatureEnumOption[], + }, + { + ...FeatureSchemaV3, + uuid: numberFeature.uuid, + sortOrder: 2, + featureEnumOptions: [], + }, + ], + }, + ); + }) + it('getAll: return correct features with sort desc', async () => { + const features = await salable.features.getAll({ + productUuid: product.uuid, + sort: 'desc' + }) + expect(features).toEqual({ + first: numberFeature.uuid, + last: booleanFeature.uuid, + data: [ + { + ...FeatureSchemaV3, + uuid: numberFeature.uuid, + sortOrder: 2, + featureEnumOptions: [], + }, + { + ...FeatureSchemaV3, + uuid: enumFeature.uuid, + sortOrder: 1, + featureEnumOptions: expect.arrayContaining([ + { ...EnumValueSchema, name: 'Option 0' }, + { ...EnumValueSchema, name: 'Option 1' }, + { ...EnumValueSchema, name: 'Option 2' }, + ]) as FeatureEnumOption[], + }, + { + ...FeatureSchemaV3, + uuid: booleanFeature.uuid, + sortOrder: 0, + featureEnumOptions: [], + }, + ], + }); + }); + it('getAll: returns two features with take applied', async () => { + const features = await salable.features.getAll({ + productUuid: product.uuid, + take: 2 + }) + expect(features).toEqual({ + first: booleanFeature.uuid, + last: enumFeature.uuid, + data: [ + { ...FeatureSchemaV3, uuid: booleanFeature.uuid, featureEnumOptions: [] }, + { + ...FeatureSchemaV3, + uuid: enumFeature.uuid, + featureEnumOptions: expect.arrayContaining([ + { ...EnumValueSchema, name: 'Option 0' }, + { ...EnumValueSchema, name: 'Option 1' }, + { ...EnumValueSchema, name: 'Option 2' }, + ]) as FeatureEnumOption[], + }, + ], + }); + }) + it('getAll: returns two features with take and cursor applied', async () => { + const features = await salable.features.getAll({ + productUuid: product.uuid, + take: 2, + cursor: booleanFeature.uuid, + }) + expect(features).toEqual({ + first: enumFeature.uuid, + last: numberFeature.uuid, + data: [ + { + ...FeatureSchemaV3, + uuid: enumFeature.uuid, + featureEnumOptions: expect.arrayContaining([ + { ...EnumValueSchema, name: 'Option 0' }, + { ...EnumValueSchema, name: 'Option 1' }, + { ...EnumValueSchema, name: 'Option 2' }, + ]) as FeatureEnumOption[], + }, + { ...FeatureSchemaV3, uuid: numberFeature.uuid, featureEnumOptions: [] }, + ], + }); + }) +}); + +async function generateTestData(organisation: string) { + const product = await prismaClient.product.create({ + data: mockProduct({organisation}), + }); + const booleanFeature = await prismaClient.feature.create({ + data: { + ...mockFeature({ displayName: 'Boolean', valueType: 'boolean', sortOrder: 0 }), + productUuid: product.uuid, + }, + }); + const enumFeature = await prismaClient.feature.create({ + data: { + ...mockFeature({ displayName: 'Boolean', valueType: 'enum', sortOrder: 1 }), + productUuid: product.uuid, + featureEnumOptions: { + createMany: { + data: Array.from({ length: 3 }, (_, i) => ({ + name: 'Option ' + i, + })), + }, + }, + }, + include: { featureEnumOptions: true }, + }); + const numberFeature = await prismaClient.feature.create({ + data: { + ...mockFeature({ displayName: 'Boolean', valueType: 'number', sortOrder: 2 }), + productUuid: product.uuid, + }, + }); + return { + product, + booleanFeature, + enumFeature, + numberFeature, + }; +} diff --git a/src/features/v3/index.ts b/src/features/v3/index.ts new file mode 100644 index 00000000..71cb0637 --- /dev/null +++ b/src/features/v3/index.ts @@ -0,0 +1,10 @@ +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; +import { ApiRequest } from '../../types'; +import { FeatureVersions } from '../index'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.FEATURES}`; + +export const v3FeatureMethods = (request: ApiRequest): FeatureVersions['v3'] => ({ + getAll: (options) => request(getUrl(`${baseUrl}`, options), { method: 'GET' }), +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 11f30f06..f5d2409d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,35 @@ import { ErrorCodes, ResponseError, SalableParseError, SalableRequestError, SalableResponseError, SalableUnknownError, SalableValidationError, ValidationError } from './exceptions/salable-error'; -import { licensesInit, LicenseVersionedMethods } from './licenses'; -import { subscriptionsInit, SubscriptionVersionedMethods } from '../src/subscriptions'; -import { plansInit, PlanVersionedMethods } from '../src/plans'; -import { productsInit, ProductVersionedMethods } from '../src/products'; -import { pricingTablesInit, PricingTableVersionedMethods } from '../src/pricing-tables'; -import { UsageVersionedMethods, usageInit } from './usage'; -import { eventsInit, EventVersionedMethods } from './events'; -import { sessionsInit, SessionVersionedMethods } from './sessions'; +import { LicenseVersionedMethods } from './licenses'; +import { SubscriptionVersionedMethods } from './subscriptions'; +import { PlanVersionedMethods } from './plans'; +import { ProductVersionedMethods } from './products'; +import { PricingTableVersionedMethods } from './pricing-tables'; +import { UsageVersionedMethods } from './usage'; +import { EventVersionedMethods } from './events'; +import { SessionVersionedMethods } from './sessions'; +import { v2EventMethods } from './events/v2'; +import { v2LicenseMethods } from './licenses/v2'; +import { v2PlanMethods } from './plans/v2'; +import { v2PricingTableMethods } from './pricing-tables/v2'; +import { v2ProductMethods } from './products/v2'; +import { v2SessionMethods } from './sessions/v2'; +import { v2SubscriptionMethods } from './subscriptions/v2'; +import { v2UsageMethods } from './usage/v2'; +import { v3PlanMethods } from './plans/v3'; +import { v3PricingTableMethods } from './pricing-tables/v3'; +import { v3ProductMethods } from './products/v3'; +import { v3SubscriptionMethods } from './subscriptions/v3'; +import { EntitlementVersionedMethods } from './entitlements'; +import { v3EntitlementMethods } from './entitlements/v3'; +import { v3FeatureMethods } from './features/v3'; +import { FeatureVersionedMethods } from './features'; export { ErrorCodes, SalableParseError, SalableRequestError, SalableResponseError, SalableUnknownError, SalableValidationError } from './exceptions/salable-error'; export type { ResponseError, ValidationError } from './exceptions/salable-error'; export const Version = { V2: 'v2', + V3: 'v3', } as const; export type TVersion = (typeof Version)[keyof typeof Version]; @@ -54,26 +71,75 @@ export const initRequest: ApiFetch = } }; -export default class Salable { - products: ProductVersionedMethods; - plans: PlanVersionedMethods; - pricingTables: PricingTableVersionedMethods; - subscriptions: SubscriptionVersionedMethods; - licenses: LicenseVersionedMethods; - usage: UsageVersionedMethods; - events: EventVersionedMethods; - sessions: SessionVersionedMethods - constructor(apiKey: string, version: V) { - const request = initRequest(apiKey, version); +type MethodsV2 = { + version: 'v2' + licenses: LicenseVersionedMethods<'v2'> + events: EventVersionedMethods<'v2'> + subscriptions: SubscriptionVersionedMethods<'v2'> + plans: PlanVersionedMethods<'v2'> + pricingTables: PricingTableVersionedMethods<'v2'> + products: ProductVersionedMethods<'v2'> + sessions: SessionVersionedMethods<'v2'> + usage: UsageVersionedMethods<'v2'> +} + +type MethodsV3 = { + version: 'v3' + events: EventVersionedMethods<'v3'> + subscriptions: SubscriptionVersionedMethods<'v3'> + plans: PlanVersionedMethods<'v3'> + pricingTables: PricingTableVersionedMethods<'v3'> + products: ProductVersionedMethods<'v3'> + sessions: SessionVersionedMethods<'v3'> + usage: UsageVersionedMethods<'v3'> + entitlements: EntitlementVersionedMethods<'v3'> + features: FeatureVersionedMethods<'v3'> +} - this.products = productsInit(version, request); - this.plans = plansInit(version, request); - this.pricingTables = pricingTablesInit(version, request); - this.subscriptions = subscriptionsInit(version, request); - this.licenses = licensesInit(version, request); - this.usage = usageInit(version, request); - this.events = eventsInit(version, request); - this.sessions = sessionsInit(version, request); +export type VersionedMethodsReturn = + V extends 'v2' ? MethodsV2 : + V extends 'v3' ? MethodsV3 : + never; + +function versionedMethods( + request: ApiRequest, + version: V +): VersionedMethodsReturn { + switch (version) { + case 'v2': + return { + version: 'v2', + events: v2EventMethods(request), + licenses: v2LicenseMethods(request), + subscriptions: v2SubscriptionMethods(request), + plans: v2PlanMethods(request), + pricingTables: v2PricingTableMethods(request), + products: v2ProductMethods(request), + sessions: v2SessionMethods(request), + usage: v2UsageMethods(request), + } as VersionedMethodsReturn; + case 'v3': + return { + version: 'v3', + events: v2EventMethods(request), + subscriptions: v3SubscriptionMethods(request), + plans: v3PlanMethods(request), + pricingTables: v3PricingTableMethods(request), + products: v3ProductMethods(request), + sessions: v2SessionMethods(request), + usage: v2UsageMethods(request), + entitlements: v3EntitlementMethods(request), + features: v3FeatureMethods(request), + } as VersionedMethodsReturn; + default: + throw new Error('Unknown version ' + version); } } + +export function initSalable(apiKey: string, version: V) { + const request = initRequest(apiKey, version); + const versioned = versionedMethods(request, version); + if (!versioned) throw new Error('Unknown Version ' + version); + return versioned; +} \ No newline at end of file diff --git a/src/licenses/index.ts b/src/licenses/index.ts index 648dfd6f..417e995b 100644 --- a/src/licenses/index.ts +++ b/src/licenses/index.ts @@ -1,5 +1,4 @@ -import { CheckLicenseInput, CheckLicensesCapabilitiesResponse, CreateAdhocLicenseInput, PaginatedLicenses, GetLicenseOptions, License, GetLicenseCountResponse, UpdateManyLicenseInput, GetLicenseCountOptions, GetPurchasersLicensesOptions, ApiRequest, TVersion, Version } from '../types'; -import { v2LicenseMethods } from './v2'; +import { CheckLicenseInput, CheckLicensesCapabilitiesResponse, CreateAdhocLicenseInput, PaginatedLicenses, GetLicenseOptions, License, GetLicenseCountResponse, UpdateManyLicenseInput, GetLicenseCountOptions, GetPurchasersLicensesOptions, TVersion, Version } from '../types'; export type LicenseVersions = { [Version.V2]: { @@ -124,13 +123,4 @@ export type LicenseVersions = { }; }; -export type LicenseVersionedMethods = V extends keyof LicenseVersions ? LicenseVersions[V] : never; - -export const licensesInit = (version: V, request: ApiRequest): LicenseVersionedMethods => { - switch (version) { - case Version.V2: - return v2LicenseMethods(request) as LicenseVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; +export type LicenseVersionedMethods = V extends keyof LicenseVersions ? LicenseVersions[V] : never; \ No newline at end of file diff --git a/src/licenses/v2/licenses-v2.test.ts b/src/licenses/v2/licenses-v2.test.ts index d0018c70..75798eb9 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -1,55 +1,49 @@ -import Salable from '../..'; -import { Capability, License, PaginatedLicenses, Plan, Version } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import getEndTime from '../../../test-utils/helpers/get-end-time'; -import { v4 as uuidv4 } from 'uuid'; - -const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); - -const version = Version.V2; - -const licenseUuid = uuidv4(); -const licenseTwoUuid = uuidv4(); -const licenseThreeUuid = uuidv4(); -const activeLicenseUuid = uuidv4(); -const noSubLicenseUuid = uuidv4(); -const noSubLicenseTwoUuid = uuidv4(); -const noSubLicenseThreeUuid = uuidv4(); -const subscriptionUuid = uuidv4(); -const testPurchaser = 'tester@testing.com'; -const testGrantee = '123456'; -const owner = 'subscription-owner' +import { initSalable } from '../../index'; +import { randomUUID } from 'crypto'; +import { addMonths } from 'date-fns'; +import { LicenseSchema, PaginatedLicensesSchema, PlanSchema } from '../../schemas/v2/schemas-v2'; + +const licenseUuid = randomUUID(); +const licenseTwoUuid = randomUUID(); +const licenseThreeUuid = randomUUID(); +const activeLicenseUuid = randomUUID(); +const noSubLicenseUuid = randomUUID(); +const noSubLicenseTwoUuid = randomUUID(); +const noSubLicenseThreeUuid = randomUUID(); +const subscriptionUuid = randomUUID(); +const testPurchaser = randomUUID(); +const testGrantee = randomUUID(); +const owner = randomUUID(); +const organisation = randomUUID(); describe('Licenses V2 Tests', () => { - const salable = new Salable(testUuids.devApiKeyV2, version); + const salable = initSalable(testUuids.devApiKeyV2, 'v2'); beforeAll(async () => { await generateTestData(); }); - afterAll(async () => { - await deleteTestData(); - }); - it('getOne: Should successfully fetch the specified license', async () => { const data = await salable.licenses.getOne(licenseUuid); - expect(data).toEqual(licenseSchema); + expect(data).toEqual(LicenseSchema); expect(data).not.toHaveProperty('plan'); }); it('getOne (w/ search params): Should successfully fetch the specified license', async () => { const dataWithSearchParams = await salable.licenses.getOne(licenseUuid, { expand: ['plan'] }); - expect(dataWithSearchParams).toEqual({ ...licenseSchema, plan: planSchema }); - expect(dataWithSearchParams).toHaveProperty('plan', planSchema); + expect(dataWithSearchParams).toEqual({ ...LicenseSchema, plan: PlanSchema }); + expect(dataWithSearchParams).toHaveProperty('plan', PlanSchema); }); it('getAll: Should successfully fetch licenses', async () => { const data = await salable.licenses.getAll(); - expect(data).toEqual(paginatedLicensesSchema); + expect(data).toEqual(PaginatedLicensesSchema); }); it('getAll (w/ search params): Should successfully fetch licenses', async () => { @@ -62,7 +56,7 @@ describe('Licenses V2 Tests', () => { expect(dataWithSearchParams).toEqual({ first: expect.any(String), last: expect.any(String), - data: expect.arrayContaining([licenseSchema]), + data: expect.arrayContaining([LicenseSchema]), }); expect(dataWithSearchParams.data.length).toEqual(3); for (const license of dataWithSearchParams.data) { @@ -96,7 +90,7 @@ describe('Licenses V2 Tests', () => { it('getForPurchaser: Should successfully fetch a purchasers licenses', async () => { const data = await salable.licenses.getForPurchaser({ purchaser: testPurchaser, productUuid: testUuids.productUuid }); - expect(data).toEqual(expect.arrayContaining([expect.objectContaining(licenseSchema)])); + expect(data).toEqual(expect.arrayContaining([expect.objectContaining(LicenseSchema)])); }); it('getForPurchaser (w/ search params): Should successfully fetch a purchasers licenses', async () => { @@ -109,7 +103,7 @@ describe('Licenses V2 Tests', () => { expect(dataWithSearchParams).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, + ...LicenseSchema, status: 'ACTIVE', purchaser: testPurchaser, productUuid: testUuids.productUuid, @@ -121,7 +115,7 @@ describe('Licenses V2 Tests', () => { it('getForGranteeId: Should successfully fetch a grantees licenses', async () => { const data = await salable.licenses.getForGranteeId(testGrantee); - expect(data).toEqual(expect.arrayContaining([expect.objectContaining(licenseSchema)])); + expect(data).toEqual(expect.arrayContaining([expect.objectContaining(LicenseSchema)])); }); it('getForGranteeId (w/ search params): Should successfully fetch a grantees licenses', async () => { @@ -130,8 +124,8 @@ describe('Licenses V2 Tests', () => { expect(dataWithSearchParams).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, - plan: planSchema, + ...LicenseSchema, + plan: PlanSchema, }), ]), ); @@ -146,7 +140,7 @@ describe('Licenses V2 Tests', () => { endTime: '2025-07-06T12:00:00.000Z', }); - expect(data).toEqual(expect.objectContaining(licenseSchema)); + expect(data).toEqual(expect.objectContaining(LicenseSchema)); }); it('createMany: Should successfully create multiple licenses', async () => { @@ -168,7 +162,7 @@ describe('Licenses V2 Tests', () => { ]); expect(data.length).toEqual(2); - expect(data).toEqual(expect.arrayContaining([expect.objectContaining(licenseSchema)])); + expect(data).toEqual(expect.arrayContaining([expect.objectContaining(LicenseSchema)])); }); it('update: Should successfully update a license', async () => { @@ -180,7 +174,7 @@ describe('Licenses V2 Tests', () => { it('updateMany: Should successfully update multiple licenses', async () => { const data = await salable.licenses.updateMany([ { - uuid: noSubLicenseTwoUuid, + uuid: noSubLicenseUuid, granteeId: 'updated-grantee-id', }, { @@ -193,7 +187,7 @@ describe('Licenses V2 Tests', () => { expect(data).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, + ...LicenseSchema, granteeId: 'updated-grantee-id', }), ]), @@ -241,348 +235,383 @@ describe('Licenses V2 Tests', () => { }); }); -const licenseCapabilitySchema: Capability = { - uuid: expect.any(String), - productUuid: expect.any(String), - name: expect.any(String), - status: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - updatedAt: expect.any(String), -}; - -const licenseSchema: License = { - uuid: expect.any(String), - name: expect.toBeOneOf([expect.any(String), null]), - email: expect.toBeOneOf([expect.any(String), null]), - subscriptionUuid: expect.toBeOneOf([expect.any(String), null]), - status: expect.toBeOneOf(['ACTIVE', 'CANCELED', 'EVALUATION', 'SCHEDULED', 'TRIALING', 'INACTIVE']), - granteeId: expect.toBeOneOf([expect.any(String), null]), - paymentService: expect.toBeOneOf(['ad-hoc', 'salable', 'stripe_existing']), - purchaser: expect.any(String), - type: expect.toBeOneOf(['licensed', 'metered', 'perSeat', 'customId', 'user']), - productUuid: expect.any(String), - planUuid: expect.any(String), - capabilities: expect.arrayContaining([licenseCapabilitySchema]), - metadata: expect.toBeOneOf([expect.anything(), null]), - startTime: expect.any(String), - endTime: expect.any(String), - updatedAt: expect.any(String), - isTest: expect.any(Boolean), - cancelAtPeriodEnd: expect.any(Boolean), -}; - -const paginatedLicensesSchema: PaginatedLicenses = { - first: expect.toBeOneOf([expect.any(String), null]), - last: expect.toBeOneOf([expect.any(String), null]), - data: expect.arrayContaining([licenseSchema]), -}; - -const planSchema: Plan = { - uuid: expect.any(String), - name: expect.any(String), - slug: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.any(String), - status: expect.any(String), - trialDays: expect.toBeOneOf([expect.any(Number), null]), - evaluation: expect.any(Boolean), - evalDays: expect.any(Number), - perSeatAmount: expect.any(Number), - maxSeatAmount: expect.any(Number), - organisation: expect.any(String), - visibility: expect.any(String), - licenseType: expect.any(String), - hasAcceptedTransaction: expect.any(Boolean), - interval: expect.any(String), - length: expect.any(Number), - active: expect.any(Boolean), - planType: expect.any(String), - pricingType: expect.any(String), - environment: expect.any(String), - isTest: expect.any(Boolean), - paddlePlanId: expect.toBeOneOf([expect.any(String), null]), - productUuid: expect.any(String), - salablePlan: expect.any(Boolean), - type: expect.toBeOneOf([expect.any(String), undefined]), - updatedAt: expect.any(String), - archivedAt: expect.toBeOneOf([expect.any(String), null]), - features: expect.toBeOneOf([expect.anything(), undefined]), -}; - -const deleteTestData = async () => { - await prismaClient.license.deleteMany({ where: { OR: [{ uuid: licenseUuid }, { uuid: licenseTwoUuid }, { uuid: licenseThreeUuid }, { uuid: activeLicenseUuid }, { uuid: noSubLicenseUuid }, { uuid: noSubLicenseTwoUuid }, { uuid: noSubLicenseThreeUuid }] } }); - await prismaClient.subscription.deleteMany({ where: { OR: [{ uuid: subscriptionUuid }] } }); -}; - const generateTestData = async () => { - await prismaClient.license.create({ + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: licenseUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: getEndTime(1, 'years'), }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - - await prismaClient.license.create({ + } + } + }) + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseTwoUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: licenseTwoUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: getEndTime(1, 'years'), }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - - await prismaClient.license.create({ + } + } + }) + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseThreeUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: licenseThreeUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: getEndTime(1, 'years'), }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - - await prismaClient.license.create({ + } + } + }) + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: 'active-grantee-id', - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: activeLicenseUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: 'active-grantee-id', + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: activeLicenseUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), }, - ], - endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), - }, - }); - - await prismaClient.license.create({ + } + } + }) + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: 'no-sub-license', - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: noSubLicenseUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: 'no-sub-license', + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: noSubLicenseUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), }, - ], - endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), - }, - }); + } + } + }) - await prismaClient.license.create({ + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: 'no-sub-license', - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: noSubLicenseTwoUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, + owner: testPurchaser, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: 'no-sub-license', + paymentService: 'ad-hoc', + purchaser: testPurchaser, + type: 'user', + uuid: noSubLicenseTwoUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), }, - ], - endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), - }, - }); + } + } + }) - await prismaClient.license.create({ + await prismaClient.subscription.create({ data: { - name: null, - email: null, + lineItemIds: [], + paymentIntegrationSubscriptionId: randomUUID(), + email: 'tester@testing.com', + type: 'none', status: 'ACTIVE', - granteeId: 'no-sub-license', - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: noSubLicenseThreeUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, + owner, + organisation, product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: uuidv4(), - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, + granteeId: 'no-sub-license', + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'user', + uuid: noSubLicenseThreeUuid, + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: randomUUID(), + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: addMonths(new Date(), 1), }, - ], - endTime: new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000), - }, - }); + } + } + }) await prismaClient.subscription.create({ data: { - lineItemIds: [stripeEnvs.basicSubscriptionLineItemId], - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionId, uuid: subscriptionUuid, + lineItemIds: [], + paymentIntegrationSubscriptionId: subscriptionUuid, email: 'tester@testing.com', - type: 'salable', + type: 'none', status: 'ACTIVE', owner, - organisation: testUuids.organisationId, + organisation, license: { connect: [{ uuid: licenseUuid }, { uuid: licenseTwoUuid }, { uuid: licenseThreeUuid }] }, product: { connect: { uuid: testUuids.productUuid } }, plan: { connect: { uuid: testUuids.paidPlanUuid } }, createdAt: new Date(), updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), + expiryDate: addMonths(new Date(), 1), }, }); }; diff --git a/src/plans/index.ts b/src/plans/index.ts index feebf195..b608140c 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -1,5 +1,18 @@ -import { Plan, PlanCheckout, PlanFeature, PlanCapability, PlanCurrency, ApiRequest, TVersion, Version, GetPlanOptions, GetPlanCheckoutOptions } from '../types'; -import { v2PlanMethods } from './v2'; +import { + Plan, + PlanCheckout, + PlanFeature, + PlanCapability, + PlanCurrency, + TVersion, + Version, + GetPlanOptions, + GetPlanCheckoutOptions, + GetPlanOptionsV3, + GetPlanCheckoutOptionsV3, + GetAllPlansOptionsV3, + GetAllPlansV3, +} from '../types'; export type PlanVersions = { [Version.V2]: { @@ -65,15 +78,46 @@ export type PlanVersions = { */ getCurrencies: (planUuid: string) => Promise; }; -}; + [Version.V3]: { + /** + * Get all plans + * + * @param GetAllPlansOptionsV3 + * + * @returns { Promise} + */ + getAll: (options?: GetAllPlansOptionsV3) => Promise; + /** + * Retrieves all plans for an organisation with cursor based pagination. The response does not contain any relational data. If you want to expand the relational data, you can do so with the `expand` parameter. + ** + * Docs - https://docs.salable.app/api/v3#tag/Plans/operation/getPlanByUuid + * + * @returns {Promise} + */ + getOne: ( + planUuid: string, + options?: GetPlanOptionsV3 + ) => Promise; -export type PlanVersionedMethods = V extends keyof PlanVersions ? PlanVersions[V] : never; + /** -export const plansInit = (version: V, request: ApiRequest): PlanVersionedMethods => { - switch (version) { - case Version.V2: - return v2PlanMethods(request) as PlanVersionedMethods; - default: - throw new Error('Unsupported version'); - } + /** + * Retrieves a checkout link for a specific plan. The checkout link can be used by customers to purchase the plan. + * + * @param {string} - planUuid The UUID of the plan + * @param {GetPlanCheckoutOptions} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Plans/operation/getPlanCheckoutLink + * + * @returns {Promise<{checkoutUrl: string;}>} + */ + getCheckoutLink: ( + planUuid: string, + options: GetPlanCheckoutOptionsV3 + ) => Promise; + }; }; + +export type PlanVersionedMethods = V extends keyof PlanVersions ? PlanVersions[V] : never; diff --git a/src/plans/v2/plan-v2.test.ts b/src/plans/v2/plan-v2.test.ts index 5b84e1c7..646ae831 100644 --- a/src/plans/v2/plan-v2.test.ts +++ b/src/plans/v2/plan-v2.test.ts @@ -1,24 +1,22 @@ -import Salable from '../..'; -import { Plan, PlanCapability, PlanCheckout, PlanCurrency, PlanFeature, Version } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { initSalable } from '../../index'; +import { PlanCapabilitySchema, PlanCheckoutLinkSchema, PlanCurrencySchema, PlanFeatureSchema, PlanSchema } from '../../schemas/v2/schemas-v2'; describe('Plans V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V2; - - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v2'); const planUuid = testUuids.paidPlanUuid; it('getOne: should successfully fetch all products', async () => { const data = await salable.plans.getOne(planUuid); - expect(data).toEqual(planSchema); + expect(data).toEqual(PlanSchema); }); it('getOne (w / search params): should successfully fetch a plan', async () => { const data = await salable.plans.getOne(planUuid, { expand: ['capabilities', 'capabilities.capability', 'features', 'features.feature', 'features.enumValue', 'currencies', 'currencies.currency'] }); - expect(data).toEqual(planSchema); + expect(data).toEqual(PlanSchema); }); it('getCheckoutLink (w / required params): should successfully fetch checkout link for plan', async () => { @@ -67,75 +65,3 @@ describe('Plans V2 Tests', () => { expect(data).toEqual(expect.arrayContaining([PlanCurrencySchema])); }); }); - -const planSchema: Plan = { - uuid: expect.any(String), - name: expect.any(String), - slug: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.any(String), - status: expect.any(String), - trialDays: expect.toBeOneOf([expect.any(Number), null]), - evaluation: expect.any(Boolean), - evalDays: expect.any(Number), - perSeatAmount: expect.any(Number), - maxSeatAmount: expect.any(Number), - organisation: expect.any(String), - visibility: expect.any(String), - licenseType: expect.any(String), - hasAcceptedTransaction: expect.any(Boolean), - interval: expect.any(String), - length: expect.any(Number), - active: expect.any(Boolean), - planType: expect.any(String), - pricingType: expect.any(String), - environment: expect.any(String), - isTest: expect.any(Boolean), - paddlePlanId: expect.toBeOneOf([expect.any(String), null]), - productUuid: expect.any(String), - salablePlan: expect.any(Boolean), - type: expect.toBeOneOf([expect.any(String), undefined]), - updatedAt: expect.any(String), - archivedAt: expect.toBeOneOf([expect.any(String), null]), - features: expect.toBeOneOf([expect.anything(), undefined]), -}; - -const PlanCheckoutLinkSchema: PlanCheckout = { - checkoutUrl: expect.any(String), -}; - -const PlanFeatureSchema: PlanFeature = { - enumValue: expect.toBeOneOf([expect.anything(), null]), - enumValueUuid: expect.any(String), - feature: expect.toBeOneOf([expect.anything()]), - featureUuid: expect.any(String), - isUnlimited: expect.any(Boolean), - isUsage: expect.any(Boolean), - maxUsage: expect.any(Number), - minUsage: expect.any(Number), - planUuid: expect.any(String), - pricePerUnit: expect.any(Number), - updatedAt: expect.any(String), - value: expect.any(String), -}; - -const PlanCapabilitySchema: PlanCapability = { - planUuid: expect.any(String), - capabilityUuid: expect.any(String), - updatedAt: expect.any(String), - capability: expect.toBeOneOf([expect.anything()]), -}; - -const PlanCurrencySchema: PlanCurrency = { - planUuid: expect.any(String), - currencyUuid: expect.any(String), - price: expect.any(Number), - paymentIntegrationPlanId: expect.any(String), - hasAcceptedTransaction: expect.any(Boolean), - currency: { - uuid: expect.any(String), - shortName: expect.any(String), - longName: expect.any(String), - symbol: expect.any(String), - }, -}; diff --git a/src/plans/v3/index.ts b/src/plans/v3/index.ts new file mode 100644 index 00000000..a34cd557 --- /dev/null +++ b/src/plans/v3/index.ts @@ -0,0 +1,12 @@ +import { ApiRequest } from '../../types'; +import { PlanVersions } from '..'; +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.PLANS}`; + +export const v3PlanMethods = (request: ApiRequest): PlanVersions['v3'] => ({ + getAll: (options) => request(getUrl(`${baseUrl}`, options), { method: 'GET' }), + getOne: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}`, options), { method: 'GET' }), + getCheckoutLink: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}/checkout-link`, options), { method: 'GET' }), +}); diff --git a/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts new file mode 100644 index 00000000..8d797b78 --- /dev/null +++ b/src/plans/v3/plan-v3.test.ts @@ -0,0 +1,306 @@ +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { + PlanFeatureSchemaV3, + OrganisationPaymentIntegrationSchemaV3, PlanCurrencySchema, + PlanSchemaV3, + ProductSchemaV3 +} from '../../schemas/v3/schemas-v3'; +import { initSalable, VersionedMethodsReturn } from '../../index'; +import { PlanCheckoutLinkSchema } from '../../schemas/v2/schemas-v2'; +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { mockProduct } from '../../../__tests__/test-mock-data/products'; +import { mockPlan } from '../../../__tests__/test-mock-data/plans'; +import { Product, Plan } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +describe('Plans V3 Tests', () => { + const apiKey = testUuids.devApiKeyV3; + const salable = initSalable(apiKey, 'v3'); + const planUuid = testUuids.paidPlanUuid; + + describe('getAll for organisation', () => { + const organisation = randomUUID(); + let data: { + product: Product; + differentProduct: Product; + planOne: Plan; + planTwo: Plan; + planThree: Plan; + differentProductPlan: Plan; + } + let salableGetAllPlans: VersionedMethodsReturn<'v3'> + beforeAll(async () => { + data = await generateTestData(organisation) + const value = randomUUID() + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key', + organisation, + value, + scopes: JSON.stringify(['plans:read']), + status: 'ACTIVE', + }, + }); + salableGetAllPlans = initSalable(value, 'v3'); + }) + it('getAll: return correct plans', async () => { + const plans = await salableGetAllPlans.plans.getAll() + expect(plans).toEqual( + { + first: data.planOne.uuid, + last: data.planThree.uuid, + data: [ + { + ...PlanSchemaV3, + uuid: data.planOne.uuid, + slug: 'a-slug', + productUuid: data.product.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.differentProductPlan.uuid, + slug: 'aa-slug', + productUuid: data.differentProduct.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.planTwo.uuid, + slug: 'b-slug', + productUuid: data.product.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.planThree.uuid, + slug: 'c-slug', + productUuid: data.product.uuid, + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + + it('getAll: return correct plans with sort desc', async () => { + const plans = await salableGetAllPlans.plans.getAll({ + sort: 'desc' + }) + expect(plans).toEqual( + { + first: data.planThree.uuid, + last: data.planOne.uuid, + data: [ + { + ...PlanSchemaV3, + uuid: data.planThree.uuid, + slug: 'c-slug', + productUuid: data.product.uuid, + archivedAt: expect.any(String) as string, + }, + { + ...PlanSchemaV3, + uuid: data.planTwo.uuid, + slug: 'b-slug', + productUuid: data.product.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.differentProductPlan.uuid, + slug: 'aa-slug', + productUuid: data.differentProduct.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.planOne.uuid, + slug: 'a-slug', + productUuid: data.product.uuid, + archivedAt: null, + }, + ], + } + ); + }) + it('getAll: return correct plans with take set', async () => { + const plans = await salableGetAllPlans.plans.getAll({ + take: 2 + }) + expect(plans).toEqual( + { + first: data.planOne.uuid, + last: data.differentProductPlan.uuid, + data: [ + { + ...PlanSchemaV3, + uuid: data.planOne.uuid, + slug: 'a-slug', + productUuid: data.product.uuid, + status: 'ACTIVE', + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.differentProductPlan.uuid, + slug: 'aa-slug', + productUuid: data.differentProduct.uuid, + status: 'ACTIVE', + archivedAt: null, + }, + ], + } + ); + }) + it('getAll: return correct plans with productUuid, take and cursor set', async () => { + const plans = await salableGetAllPlans.plans.getAll({ + productUuid: data.product.uuid, + take: 2, + cursor: data.planOne.uuid, + }) + expect(plans).toEqual( + { + first: data.planTwo.uuid, + last: data.planThree.uuid, + data: [ + { + ...PlanSchemaV3, + uuid: data.planTwo.uuid, + slug: 'b-slug', + productUuid: data.product.uuid, + archivedAt: null, + }, + { + ...PlanSchemaV3, + uuid: data.planThree.uuid, + slug: 'c-slug', + productUuid: data.product.uuid, + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + it('getAll: return correct plans with archived set', async () => { + const plans = await salableGetAllPlans.plans.getAll({ + archived: true + }) + expect(plans).toEqual( + { + first: data.planThree.uuid, + last: data.planThree.uuid, + data: [ + { + ...PlanSchemaV3, + uuid: data.planThree.uuid, + slug: 'c-slug', + productUuid: data.product.uuid, + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + }) + + it('getOne: should successfully fetch one plan', async () => { + const data = await salable.plans.getOne(planUuid); + expect(data).toEqual(PlanSchemaV3); + }); + + it('getOne (w / search params): should successfully fetch a plan', async () => { + const data = await salable.plans.getOne(planUuid, { expand: ['features', 'product', 'currencies'] }); + expect(data).toEqual({ + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + product: { + ...ProductSchemaV3, + organisationPaymentIntegration: OrganisationPaymentIntegrationSchemaV3, + }, + currencies: expect.arrayContaining([PlanCurrencySchema]) + }); + }); + + it('getCheckoutLink (w / required params): should successfully fetch checkout link for plan', async () => { + const data = await salable.plans.getCheckoutLink(planUuid, { + successUrl: 'https://www.salable.app', + cancelUrl: 'https://www.salable.app', + granteeId: 'granteeid@example.com', + owner: 'owner-id', + }); + + expect(data).toEqual(PlanCheckoutLinkSchema); + }); + + it('getCheckoutLink (w / optional params): should successfully fetch checkout link for plan', async () => { + const data = await salable.plans.getCheckoutLink(planUuid, { + successUrl: 'https://www.salable.app', + cancelUrl: 'https://www.salable.app', + granteeId: 'granteeid@example.com', + owner: 'member-id', + allowPromoCode: true, + customerEmail: 'customer@email.com', + currency: 'GBP', + automaticTax: '', + changeQuantity: '1', + requirePaymentMethod: false, + }); + + expect(data).toEqual(PlanCheckoutLinkSchema); + }); +}); + +async function generateTestData(organisation: string) { + const product = await prismaClient.product.create({ + data: mockProduct({ organisation }), + }); + const differentProduct = await prismaClient.product.create({ + data: mockProduct({ organisation }), + }); + const planOne = await prismaClient.plan.create({ + data: { + ...mockPlan({ + organisation, + slug: 'a-slug', + }), + productUuid: product.uuid, + }, + }); + const planTwo = await prismaClient.plan.create({ + data: { + ...mockPlan({ + organisation, + slug: 'b-slug', + }), + productUuid: product.uuid, + }, + }); + const planThree = await prismaClient.plan.create({ + data: { + ...mockPlan({ + organisation, + slug: 'c-slug', + archivedAt: new Date(), + }), + productUuid: product.uuid, + }, + }); + const differentProductPlan = await prismaClient.plan.create({ + data: { + ...mockPlan({ + organisation, + slug: 'aa-slug', + }), + productUuid: differentProduct.uuid, + }, + }); + return { + product, + differentProduct, + planOne, + planTwo, + planThree, + differentProductPlan, + }; +} \ No newline at end of file diff --git a/src/pricing-tables/index.ts b/src/pricing-tables/index.ts index cf0bbaa8..5e007d84 100644 --- a/src/pricing-tables/index.ts +++ b/src/pricing-tables/index.ts @@ -1,5 +1,4 @@ -import { PricingTable, ApiRequest, TVersion, Version } from '../types'; -import { v2PricingTableMethods } from './v2'; +import { PricingTable, TVersion, Version, PricingTableV3 } from '../types'; export type PricingTableVersions = { [Version.V2]: { @@ -13,15 +12,17 @@ export type PricingTableVersions = { */ getOne: (pricingTableUuid: string, options?: { granteeId?: string; currency?: string }) => Promise; }; + [Version.V3]: { + /** + * Retrieves a pricing table by its UUID. This returns all necessary data on a Pricing Table to be able to display it. + * + * @param {string} pricingTableUuid - The UUID for the pricingTable + * @param {{ granteeId?: string; currency?: string;}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Pricing-Tables/operation/getPricingTableByUuid + * + * @returns {Promise} + */ + getOne: (pricingTableUuid: string, options: { owner: string; currency?: string }) => Promise; + }; }; -export type PricingTableVersionedMethods = V extends keyof PricingTableVersions ? PricingTableVersions[V] : never; - -export const pricingTablesInit = (version: V, request: ApiRequest): PricingTableVersionedMethods => { - switch (version) { - case Version.V2: - return v2PricingTableMethods(request) as PricingTableVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; +export type PricingTableVersionedMethods = V extends keyof PricingTableVersions ? PricingTableVersions[V] : never; \ No newline at end of file diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index d2128748..37a9cbae 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -1,51 +1,31 @@ -import Salable from '../..'; -import { PricingTable, Version } from '../../types'; +import { initSalable } from '../..'; import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; +import { PricingTableSchema } from '../../schemas/v2/schemas-v2'; -const pricingTableUuid = 'aec06de8-3a3e-46eb-bd09-f1094c1b1b8d'; describe('Pricing Table V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V2; - - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v2'); + const pricingTableUuid = randomUUID(); beforeAll(async() => { - await generateTestData() + await generateTestData(pricingTableUuid) }) - it('getAll: should successfully fetch all products', async () => { + it('getAll: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); - - expect(data).toEqual(expect.objectContaining(pricingTableSchema)); + expect(data).toEqual(expect.objectContaining(PricingTableSchema)); }); }); -const pricingTableSchema: PricingTable = { - customTheme: expect.toBeOneOf([expect.any(String), null]), - productUuid: expect.any(String), - featuredPlanUuid: expect.toBeOneOf([expect.any(String), null]), - name: expect.any(String), - status: expect.any(String), - theme: expect.any(String), - text: expect.toBeOneOf([expect.any(String), null]), - title: expect.toBeOneOf([expect.any(String), null]), - updatedAt: expect.any(String), - uuid: expect.any(String), - featureOrder: expect.any(String), - features: expect.toBeOneOf([expect.anything(), undefined]), - product: expect.toBeOneOf([expect.anything(), undefined]), - plans: expect.toBeOneOf([expect.anything(), undefined]), -}; - -const generateTestData = async () => { +const generateTestData = async (pricingTableUuid: string) => { - const product = await prismaClient.product.findFirst({ + const product = await prismaClient.product.findUnique({ where: { uuid: testUuids.productUuid }, - select: { + include: { features: true, - uuid: true, plans: true, }, }); diff --git a/src/pricing-tables/v3/index.ts b/src/pricing-tables/v3/index.ts new file mode 100644 index 00000000..0482a0e4 --- /dev/null +++ b/src/pricing-tables/v3/index.ts @@ -0,0 +1,10 @@ +import { ApiRequest } from '../../types'; +import { PricingTableVersions } from '..'; +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.PRICING_TABLES}`; + +export const v3PricingTableMethods = (request: ApiRequest): PricingTableVersions['v3'] => ({ + getOne: (productUuid, options) => request(getUrl(`${baseUrl}/${productUuid}`, options), { method: 'GET' }), +}); diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts new file mode 100644 index 00000000..cebee3f1 --- /dev/null +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -0,0 +1,75 @@ +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { PricingTableSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { initSalable } from '../../index'; +import { randomUUID } from 'crypto'; + +describe('Pricing Table V3 Tests', () => { + const apiKey = testUuids.devApiKeyV3; + const salable = initSalable(apiKey, 'v3'); + const pricingTableUuid = randomUUID(); + + beforeAll(async() => { + await generateTestData(pricingTableUuid) + }) + + it('getOne: should successfully fetch all pricing tables', async () => { + const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); + expect(data).toEqual(expect.objectContaining(PricingTableSchemaV3)); + }); +}); + + +const generateTestData = async (pricingTableUuid: string) => { + + const product = await prismaClient.product.findUnique({ + where: { uuid: testUuids.productUuid }, + include: { + features: true, + plans: true, + }, + }); + + if (!product) return null; + + await prismaClient.pricingTable.create({ + data: { + uuid: pricingTableUuid, + name: 'xxxxx', + product: { connect: { uuid: product.uuid } }, + features: { + create: product.features.map((f) => ({ + feature: { connect: { uuid: f.uuid } }, + sortOrder: f.sortOrder, + })), + }, + featuredPlan: { connect: { uuid: testUuids.paidPlanUuid } }, + plans: { + create: [{ planUuid: testUuids.paidPlanUuid, sortOrder: 0 }], + }, + }, + include: { + features: { + include: { feature: { include: { featureEnumOptions: true } } }, + }, + plans: { + include: { + plan: { + include: { + features: { include: { feature: true, enumValue: true } }, + capabilities: { include: { capability: true } }, + currencies: { include: { currency: true } }, + }, + }, + }, + }, + product: { + include: { + currencies: true, + organisationPaymentIntegration: true, + features: { include: { featureEnumOptions: true } }, + }, + }, + }, + }); +} diff --git a/src/products/index.ts b/src/products/index.ts index fa5a5084..fc576aae 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -1,5 +1,4 @@ -import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, ApiRequest, TVersion, Version } from '../types'; -import { v2ProductMethods } from './v2'; +import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, TVersion, Version, ProductV3, ProductPricingTableV3, GetAllProductsV3, GetAllProductsOptionsV3 } from '../types'; export type ProductVersions = { [Version.V2]: { @@ -8,14 +7,14 @@ export type ProductVersions = { * * Docs - https://docs.salable.app/api/v2#tag/Products/operation/getProducts * - * @returns {Promise} All products present on the account + * @returns {Promise} All products for an organisation */ getAll: () => Promise; /** * Retrieves a specific product by its UUID. By default, the response does not contain any relational data. If you want to expand the relational data, you can do so with the `expand` query parameter. * - * @param {string} productUuid - The UUID for the pricingTable + * @param {string} productUuid - The UUID for the product * @param {{ expand: string[]}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductByUuid * * @returns {Promise} @@ -25,7 +24,7 @@ export type ProductVersions = { /** * Retrieves all the plans associated with a specific product. By default, the response does not contain any relational data. If you want to expand the relational data, you can do so with the expand query parameter. * - * @param {string} productUuid - The UUID for the pricingTable + * @param {string} productUuid - The UUID for the product * @param {{ granteeId?: string; currency?: string }} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductPricingTable * * @returns {Promise} @@ -76,15 +75,35 @@ export type ProductVersions = { */ getCurrencies(productUuid: string): Promise; }; -}; + [Version.V3]: { + /** + * Retrieves a paginated list of all products + * + * Docs - https://docs.salable.app/api/v3#tag/Products/operation/getProducts + * + * @returns {Promise} All products for an organisation + */ + getAll: (options?: GetAllProductsOptionsV3) => Promise; -export type ProductVersionedMethods = V extends keyof ProductVersions ? ProductVersions[V] : never; + /** + * Retrieves a specific product by its UUID. By default, the response does not contain any relational data. If you want to expand the relational data, you can do so with the `expand` query parameter. + * + * @param {string} productUuid - The UUID for the product + * @param {{ expand: string[]}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Products/operation/getProductByUuid + * + * @returns {Promise} + */ + getOne: (productUuid: string, options?: { expand: ('organisationPaymentIntegration')[] }) => Promise; -export const productsInit = (version: V, request: ApiRequest): ProductVersionedMethods => { - switch (version) { - case Version.V2: - return v2ProductMethods(request) as ProductVersionedMethods; - default: - throw new Error('Unsupported version'); - } + /** + * Retrieves all non archived plans with their features and currencies to display in a pricing table. The plans are sorted by price. + * + * @param {string} productUuid - The UUID for the product + * + * @returns {Promise} + */ + getPricingTable: (productUuid: string, options: { owner: string }) => Promise; + }; }; + +export type ProductVersionedMethods = V extends keyof ProductVersions ? ProductVersions[V] : never; diff --git a/src/products/v2/product-v2.test.ts b/src/products/v2/product-v2.test.ts index 7e8b2e32..d0a43cdf 100644 --- a/src/products/v2/product-v2.test.ts +++ b/src/products/v2/product-v2.test.ts @@ -1,12 +1,10 @@ -import Salable from '../..'; -import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, Version } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { initSalable } from '../../index'; +import { ProductCapabilitySchema, ProductCurrencySchema, ProductFeatureSchema, ProductPlanSchema, ProductPricingTableSchema, ProductSchema } from '../../schemas/v2/schemas-v2'; describe('Products V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V2; - - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v2'); const productUuid = testUuids.productUuid; @@ -75,95 +73,3 @@ describe('Products V2 Tests', () => { expect(data).toEqual(expect.arrayContaining([ProductCurrencySchema])); }); }); - -const ProductSchema: Product = { - uuid: expect.any(String), - name: expect.any(String), - slug: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - logoUrl: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.toBeOneOf([expect.any(String), null]), - organisation: expect.any(String), - status: expect.any(String), - paid: expect.any(Boolean), - isTest: expect.any(Boolean), - organisationPaymentIntegrationUuid: expect.any(String), - paymentIntegrationProductId: expect.toBeOneOf([expect.any(String), null]), - appType: expect.any(String), - updatedAt: expect.any(String), - archivedAt: expect.toBeOneOf([expect.any(String), null]), -}; - -const ProductPricingTableSchema: ProductPricingTable = { - ...ProductSchema, - features: expect.toBeOneOf([expect.anything(), undefined]), - currencies: expect.toBeOneOf([expect.anything(), undefined]), - plans: expect.toBeOneOf([expect.anything(), undefined]), - status: expect.any(String), - updatedAt: expect.any(String), - uuid: expect.any(String), -}; -const ProductPlanSchema: Plan = { - uuid: expect.any(String), - name: expect.any(String), - slug: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.any(String), - status: expect.any(String), - trialDays: expect.toBeOneOf([expect.any(Number), null]), - evaluation: expect.any(Boolean), - evalDays: expect.any(Number), - perSeatAmount: expect.any(Number), - maxSeatAmount: expect.any(Number), - organisation: expect.any(String), - visibility: expect.any(String), - licenseType: expect.any(String), - hasAcceptedTransaction: expect.any(Boolean), - interval: expect.any(String), - length: expect.any(Number), - active: expect.any(Boolean), - planType: expect.any(String), - pricingType: expect.any(String), - environment: expect.any(String), - isTest: expect.any(Boolean), - paddlePlanId: expect.toBeOneOf([expect.any(String), null]), - productUuid: expect.any(String), - salablePlan: expect.any(Boolean), - type: expect.toBeOneOf([expect.any(String), undefined]), - updatedAt: expect.any(String), - features: expect.toBeOneOf([expect.anything(), undefined]), - archivedAt: expect.toBeOneOf([expect.any(String), null]), -}; - -const ProductFeatureSchema: ProductFeature = { - defaultValue: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.any(String), - featureEnumOptions: expect.toBeOneOf([expect.anything()]), - name: expect.any(String), - productUuid: expect.any(String), - showUnlimited: expect.any(Boolean), - sortOrder: expect.any(Number), - status: expect.any(String), - updatedAt: expect.any(String), - uuid: expect.any(String), - valueType: expect.any(String), - variableName: expect.any(String), - visibility: expect.any(String), -}; - -const ProductCapabilitySchema: ProductCapability = { - uuid: expect.any(String), - name: expect.any(String), - description: expect.any(String), - status: expect.any(String), - productUuid: expect.any(String), - updatedAt: expect.any(String), -}; - -const ProductCurrencySchema: ProductCurrency = { - productUuid: expect.any(String), - currencyUuid: expect.any(String), - defaultCurrency: expect.any(Boolean), - currency: expect.toBeOneOf([expect.anything()]), -}; diff --git a/src/products/v3/index.ts b/src/products/v3/index.ts new file mode 100644 index 00000000..6b2ef975 --- /dev/null +++ b/src/products/v3/index.ts @@ -0,0 +1,12 @@ +import { ApiRequest } from '../../types'; +import { ProductVersions } from '..'; +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.PRODUCTS}`; + +export const v3ProductMethods = (request: ApiRequest): ProductVersions['v3'] => ({ + getAll: (options) => request(getUrl(baseUrl, options), { method: 'GET' }), + getOne: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}`, options), { method: 'GET' }), + getPricingTable: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}/pricing-table`, options), { method: 'GET' }), +}); diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts new file mode 100644 index 00000000..d86411f4 --- /dev/null +++ b/src/products/v3/product-v3.test.ts @@ -0,0 +1,205 @@ +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { OrganisationPaymentIntegrationSchemaV3, ProductSchemaV3, ProductPricingTableSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { initSalable, VersionedMethodsReturn } from '../../index'; +import { randomUUID } from 'crypto'; +import { Product } from '@prisma/client'; +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { mockProduct } from '../../../__tests__/test-mock-data/products'; + +describe('Products V3 Tests', () => { + const apiKey = testUuids.devApiKeyV2; + const salable = initSalable(apiKey, 'v3'); + const productUuid = testUuids.productUuid; + + describe('getAll products for organisation', () => { + const organisation = randomUUID(); + let data: { + productOne: Product + productTwo: Product + productThree: Product + differentOrganisationProduct: Product + } + let differentOrgSalable: VersionedMethodsReturn<'v3'> + beforeAll(async () => { + data = await generateTestData(organisation) + const value = randomUUID() + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key', + organisation, + value, + scopes: JSON.stringify(['products:read']), + status: 'ACTIVE', + }, + }); + differentOrgSalable = initSalable(value, 'v3'); + }) + it('getAll: return correct products', async () => { + const plans = await differentOrgSalable.products.getAll() + expect(plans).toEqual( + { + first: data.productOne.uuid, + last: data.productThree.uuid, + data: [ + { + ...ProductSchemaV3, + uuid: data.productOne.uuid, + slug: 'a-slug', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.differentOrganisationProduct.uuid, + slug: 'aa-slug', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.productTwo.uuid, + slug: 'b-slug', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.productThree.uuid, + slug: 'c-slug', + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + + it('getAll: return correct products with sort desc', async () => { + const plans = await differentOrgSalable.products.getAll({ + sort: 'desc' + }) + expect(plans).toEqual( + { + first: data.productThree.uuid, + last: data.productOne.uuid, + data: [ + { + ...ProductSchemaV3, + uuid: data.productThree.uuid, + slug: 'c-slug', + archivedAt: expect.any(String) as string, + }, + { + ...ProductSchemaV3, + uuid: data.productTwo.uuid, + slug: 'b-slug', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.differentOrganisationProduct.uuid, + slug: 'aa-slug', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.productOne.uuid, + slug: 'a-slug', + archivedAt: null, + }, + ], + } + ); + }) + it('getAll: return correct products with take set', async () => { + const plans = await differentOrgSalable.products.getAll({ + take: 2 + }) + expect(plans).toEqual( + { + first: data.productOne.uuid, + last: data.differentOrganisationProduct.uuid, + data: [ + { + ...ProductSchemaV3, + uuid: data.productOne.uuid, + slug: 'a-slug', + status: 'ACTIVE', + archivedAt: null, + }, + { + ...ProductSchemaV3, + uuid: data.differentOrganisationProduct.uuid, + slug: 'aa-slug', + status: 'ACTIVE', + archivedAt: null, + }, + ], + } + ); + }) + it('getAll: return correct products with archived set', async () => { + const plans = await differentOrgSalable.products.getAll({ + archived: true + }) + expect(plans).toEqual( + { + first: data.productThree.uuid, + last: data.productThree.uuid, + data: [ + { + ...ProductSchemaV3, + uuid: data.productThree.uuid, + slug: 'c-slug', + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + }) + + it('getOne: should successfully fetch a product', async () => { + const data = await salable.products.getOne(productUuid); + expect(data).toEqual(ProductSchemaV3); + }); + + it('getOne (w / search params): should successfully fetch a product', async () => { + const data = await salable.products.getOne(productUuid, { + expand: ['organisationPaymentIntegration'], + }); + expect(data).toEqual({ + ...ProductSchemaV3, + organisationPaymentIntegration: OrganisationPaymentIntegrationSchemaV3 + }); + }); + + it('getPricingTable: should successfully fetch a product pricing table', async () => { + const data = await salable.products.getPricingTable(productUuid, {owner: 'xxxxx'}); + expect(data).toEqual(ProductPricingTableSchemaV3); + }); + + it('getPricingTable (w / search params): should successfully fetch a product pricing table', async () => { + const data = await salable.products.getPricingTable(productUuid, { + owner: 'xxxxx', + }); + expect(data).toEqual(ProductPricingTableSchemaV3); + }); +}); + +async function generateTestData(organisation: string) { + const productOne = await prismaClient.product.create({ + data: mockProduct({ organisation, slug: 'a-slug' }), + }); + const productTwo = await prismaClient.product.create({ + data: mockProduct({ organisation, slug: 'b-slug' }), + }); + const productThree = await prismaClient.product.create({ + data: mockProduct({ organisation, slug: 'c-slug', archivedAt: new Date() }), + }); + const differentOrganisationProduct = await prismaClient.product.create({ + data: mockProduct({ organisation, slug: 'aa-slug' }), + }); + return { + productOne, + productTwo, + productThree, + differentOrganisationProduct + }; +} diff --git a/src/schemas/v2/schemas-v2.ts b/src/schemas/v2/schemas-v2.ts new file mode 100644 index 00000000..d26b21f7 --- /dev/null +++ b/src/schemas/v2/schemas-v2.ts @@ -0,0 +1,452 @@ +import { + EnumValue, + Event, + EventTypeEnum, + Invoice, + PaginatedLicenses, + PaginatedSubscription, + PaginatedSubscriptionInvoice, + PaginatedUsageRecords, + Plan, + PlanCapability, + PlanCheckout, + PlanCurrency, + PlanFeature, + PricingTable, + Product, + ProductCapability, + ProductCurrency, + ProductFeature, + ProductPricingTable, + Session, + Subscription, + UsageRecord, +} from '../../types'; +import { Capability, EventStatus, License } from '@prisma/client'; + +export const EventSchema: Event = { + uuid: expect.any(String), + type: expect.toBeOneOf(Object.values(EventTypeEnum)) as EventTypeEnum, + organisation: expect.any(String), + status: expect.toBeOneOf(Object.values(EventStatus)) as EventStatus, + isTest: expect.any(Boolean), + retries: expect.any(Number), + errorMessage: expect.toBeOneOf([expect.any(String), null]), + errorCode: expect.toBeOneOf([expect.any(String), null]), + createdAt: expect.any(String), + updatedAt: expect.any(String), +}; + +export const LicenseCapabilitySchema: Capability = { + uuid: expect.any(String), + productUuid: expect.any(String), + name: expect.any(String), + status: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + updatedAt: expect.any(String), +}; + +export const LicenseSchema: License = { + uuid: expect.any(String), + name: expect.toBeOneOf([expect.any(String), null]), + email: expect.toBeOneOf([expect.any(String), null]), + subscriptionUuid: expect.toBeOneOf([expect.any(String), null]), + status: expect.toBeOneOf(['ACTIVE', 'CANCELED', 'EVALUATION', 'SCHEDULED', 'TRIALING', 'INACTIVE']), + granteeId: expect.toBeOneOf([expect.any(String), null]), + paymentService: expect.toBeOneOf(['ad-hoc', 'salable', 'stripe_existing']), + purchaser: expect.any(String), + type: expect.toBeOneOf(['licensed', 'metered', 'perSeat', 'customId', 'user']), + productUuid: expect.any(String), + planUuid: expect.any(String), + capabilities: expect.arrayContaining([LicenseCapabilitySchema]), + metadata: expect.toBeOneOf([expect.anything(), null]), + startTime: expect.any(String), + endTime: expect.any(String), + updatedAt: expect.any(String), + isTest: expect.any(Boolean), + cancelAtPeriodEnd: expect.any(Boolean), +}; + +export const PaginatedLicensesSchema: PaginatedLicenses = { + first: expect.toBeOneOf([expect.any(String), null]), + last: expect.toBeOneOf([expect.any(String), null]), + data: expect.arrayContaining([LicenseSchema]), +}; + +export const PlanSchema: Plan = { + uuid: expect.any(String), + name: expect.any(String), + slug: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.any(String), + status: expect.any(String), + trialDays: expect.toBeOneOf([expect.any(Number), null]), + evaluation: expect.any(Boolean), + evalDays: expect.any(Number), + perSeatAmount: expect.any(Number), + maxSeatAmount: expect.any(Number), + organisation: expect.any(String), + visibility: expect.any(String), + licenseType: expect.any(String), + hasAcceptedTransaction: expect.any(Boolean), + interval: expect.any(String), + length: expect.any(Number), + active: expect.any(Boolean), + planType: expect.any(String), + pricingType: expect.any(String), + environment: expect.any(String), + isTest: expect.any(Boolean), + paddlePlanId: expect.toBeOneOf([expect.any(String), null]), + productUuid: expect.any(String), + salablePlan: expect.any(Boolean), + type: expect.toBeOneOf([expect.any(String), undefined]), + updatedAt: expect.any(String), + features: expect.toBeOneOf([expect.anything(), undefined]), + currencies: expect.toBeOneOf([expect.anything(), undefined]), + archivedAt: expect.toBeOneOf([expect.any(String), null]), +}; + +export const PlanCheckoutLinkSchema: PlanCheckout = { + checkoutUrl: expect.any(String), +}; + +export const PlanFeatureSchema: PlanFeature = { + enumValue: expect.toBeOneOf([expect.anything(), null]), + enumValueUuid: expect.any(String), + feature: expect.toBeOneOf([expect.anything()]), + featureUuid: expect.any(String), + isUnlimited: expect.any(Boolean), + isUsage: expect.any(Boolean), + maxUsage: expect.any(Number), + minUsage: expect.any(Number), + planUuid: expect.any(String), + pricePerUnit: expect.any(Number), + updatedAt: expect.any(String), + value: expect.any(String), +}; + +export const PlanCapabilitySchema: PlanCapability = { + planUuid: expect.any(String), + capabilityUuid: expect.any(String), + updatedAt: expect.any(String), + capability: expect.toBeOneOf([expect.anything()]), +}; + +export const PlanCurrencySchema: PlanCurrency = { + planUuid: expect.any(String), + currencyUuid: expect.any(String), + price: expect.any(Number), + paymentIntegrationPlanId: expect.any(String), + hasAcceptedTransaction: expect.any(Boolean), + currency: { + uuid: expect.any(String), + shortName: expect.any(String), + longName: expect.any(String), + symbol: expect.any(String), + }, +}; + +export const PricingTableSchema: PricingTable = { + customTheme: expect.toBeOneOf([expect.any(String), null]), + productUuid: expect.any(String), + featuredPlanUuid: expect.toBeOneOf([expect.any(String), null]), + name: expect.any(String), + status: expect.any(String), + theme: expect.any(String), + text: expect.toBeOneOf([expect.any(String), null]), + title: expect.toBeOneOf([expect.any(String), null]), + updatedAt: expect.any(String), + uuid: expect.any(String), + featureOrder: expect.any(String), + features: expect.toBeOneOf([expect.anything(), undefined]), + product: expect.toBeOneOf([expect.anything(), undefined]), + plans: expect.toBeOneOf([expect.anything(), undefined]), +}; + +export const ProductSchema: Product = { + uuid: expect.any(String), + name: expect.any(String), + slug: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + logoUrl: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.toBeOneOf([expect.any(String), null]), + organisation: expect.any(String), + status: expect.any(String), + paid: expect.any(Boolean), + isTest: expect.any(Boolean), + organisationPaymentIntegrationUuid: expect.any(String), + paymentIntegrationProductId: expect.toBeOneOf([expect.any(String), null]), + appType: expect.any(String), + updatedAt: expect.any(String), + archivedAt: expect.toBeOneOf([expect.any(String), null]), +}; + +export const ProductPricingTableSchema: ProductPricingTable = { + ...ProductSchema, + features: expect.toBeOneOf([expect.anything(), undefined]), + currencies: expect.toBeOneOf([expect.anything(), undefined]), + plans: expect.toBeOneOf([expect.anything(), undefined]), + status: expect.any(String), + updatedAt: expect.any(String), + uuid: expect.any(String), +}; +export const ProductPlanSchema: Plan = { + uuid: expect.any(String), + name: expect.any(String), + slug: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.any(String), + status: expect.any(String), + trialDays: expect.toBeOneOf([expect.any(Number), null]), + evaluation: expect.any(Boolean), + evalDays: expect.any(Number), + perSeatAmount: expect.any(Number), + maxSeatAmount: expect.any(Number), + organisation: expect.any(String), + visibility: expect.any(String), + licenseType: expect.any(String), + hasAcceptedTransaction: expect.any(Boolean), + interval: expect.any(String), + length: expect.any(Number), + active: expect.any(Boolean), + planType: expect.any(String), + pricingType: expect.any(String), + environment: expect.any(String), + isTest: expect.any(Boolean), + paddlePlanId: expect.toBeOneOf([expect.any(String), null]), + productUuid: expect.any(String), + salablePlan: expect.any(Boolean), + type: expect.toBeOneOf([expect.any(String), undefined]), + updatedAt: expect.any(String), + features: expect.toBeOneOf([expect.anything(), undefined]), + archivedAt: expect.toBeOneOf([expect.any(String), null]), +}; + +export const ProductFeatureSchema: ProductFeature = { + defaultValue: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.any(String), + featureEnumOptions: expect.toBeOneOf([expect.anything()]), + name: expect.any(String), + productUuid: expect.any(String), + showUnlimited: expect.any(Boolean), + sortOrder: expect.any(Number), + status: expect.any(String), + updatedAt: expect.any(String), + uuid: expect.any(String), + valueType: expect.any(String), + variableName: expect.any(String), + visibility: expect.any(String), +}; + +export const ProductCapabilitySchema: ProductCapability = { + uuid: expect.any(String), + name: expect.any(String), + description: expect.any(String), + status: expect.any(String), + productUuid: expect.any(String), + updatedAt: expect.any(String), +}; + +export const ProductCurrencySchema: ProductCurrency = { + productUuid: expect.any(String), + currencyUuid: expect.any(String), + defaultCurrency: expect.any(Boolean), + currency: expect.toBeOneOf([expect.anything()]), +}; + +export const SubscriptionSchema: Subscription = { + uuid: expect.any(String), + paymentIntegrationSubscriptionId: expect.any(String), + productUuid: expect.any(String), + type: expect.any(String), + isTest: expect.any(Boolean), + cancelAtPeriodEnd: expect.any(Boolean), + email: expect.toBeOneOf([expect.any(String), null]), + owner: expect.toBeOneOf([expect.any(String), null]), + organisation: expect.any(String), + quantity: expect.any(Number), + status: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + expiryDate: expect.any(String), + lineItemIds: expect.toBeOneOf([expect.toBeArray(), null]), + planUuid: expect.any(String), +}; + +export const EnumValueSchema: EnumValue = { + uuid: expect.any(String), + name: expect.any(String), + featureUuid: expect.any(String), + updatedAt: expect.any(String), +} + +export const SessionSchema: Session = { + sessionToken: expect.any(String), +}; + +export const PaginationSubscriptionSchema: PaginatedSubscription = { + first: expect.any(String), + last: expect.any(String), + data: expect.arrayContaining([SubscriptionSchema]), +}; + +export const InvoiceSchema: Invoice = { + id: expect.any(String), + object: expect.any(String), + account_country: expect.any(String), + account_name: expect.any(String), + account_tax_ids: expect.toBeOneOf([expect.toBeArray(), null]), + amount_due: expect.any(Number), + amount_paid: expect.any(Number), + amount_overpaid: expect.any(Number), + amount_remaining: expect.any(Number), + amount_shipping: expect.any(Number), + application: expect.toBeOneOf([expect.any(String), null]), + application_fee_amount: expect.toBeOneOf([expect.any(Number), null]), + attempt_count: expect.any(Number), + attempted: expect.any(Boolean), + auto_advance: expect.any(Boolean), + automatic_tax: expect.toBeObject(), + automatically_finalizes_at: expect.toBeOneOf([expect.any(Number), null]), + billing_reason: expect.any(String), + charge: expect.any(String), + collection_method: expect.any(String), + created: expect.any(Number), + currency: expect.any(String), + custom_fields: expect.toBeOneOf([expect.toBeArray(), null]), + customer: expect.any(String), + customer_address: expect.toBeOneOf([expect.toBeObject(), null]), + customer_email: expect.any(String), + customer_name: expect.toBeOneOf([expect.any(String), null]), + customer_phone: expect.toBeOneOf([expect.any(String), null]), + customer_shipping: expect.toBeOneOf([expect.toBeObject, null]), + customer_tax_exempt: expect.any(String), + customer_tax_ids: expect.toBeOneOf([expect.toBeArray(), null]), + default_payment_method: expect.toBeOneOf([expect.any(String), null]), + default_source: expect.toBeOneOf([expect.any(String), null]), + default_tax_rates: expect.toBeOneOf([expect.toBeArray(), null]), + description: expect.toBeOneOf([expect.any(String), null]), + discount: expect.toBeOneOf([expect.toBeObject(), null]), + discounts: expect.toBeOneOf([expect.toBeArray(), null]), + due_date: expect.toBeOneOf([expect.any(Number), null]), + effective_at: expect.any(Number), + ending_balance: expect.any(Number), + footer: expect.toBeOneOf([expect.any(String), null]), + from_invoice: expect.toBeOneOf([expect.toBeObject(), null]), + hosted_invoice_url: expect.any(String), + invoice_pdf: expect.any(String), + issuer: expect.toBeObject(), + last_finalization_error: expect.toBeOneOf([expect.toBeObject(), null]), + latest_revision: expect.toBeOneOf([expect.any(String), null]), + lines: expect.toBeObject(), + livemode: expect.any(Boolean), + metadata: expect.toBeObject(), + next_payment_attempt: expect.toBeOneOf([expect.any(Number), null]), + number: expect.any(String), + on_behalf_of: expect.toBeOneOf([expect.any(String), null]), + paid: expect.any(Boolean), + paid_out_of_band: expect.any(Boolean), + parent: expect.toBeObject(), + payment_intent: expect.any(String), + payment_settings: expect.toBeObject(), + period_end: expect.any(Number), + period_start: expect.any(Number), + post_payment_credit_notes_amount: expect.any(Number), + pre_payment_credit_notes_amount: expect.any(Number), + quote: expect.toBeOneOf([expect.any(String), null]), + receipt_number: expect.toBeOneOf([expect.any(String), null]), + rendering: expect.toBeOneOf([expect.toBeObject(), null]), + rendering_options: expect.toBeOneOf([expect.toBeObject(), undefined]), + shipping_cost: expect.toBeOneOf([expect.toBeObject(), null]), + shipping_details: expect.toBeOneOf([expect.toBeObject(), null]), + starting_balance: expect.any(Number), + statement_descriptor: expect.toBeOneOf([expect.any(String), null]), + status: expect.any(String), + status_transitions: expect.toBeObject(), + subscription: expect.any(String), + subscription_details: expect.toBeObject(), + subtotal: expect.any(Number), + subtotal_excluding_tax: expect.any(Number), + tax: expect.toBeOneOf([expect.any(Number), null]), + test_clock: expect.toBeOneOf([expect.any(String), null]), + total: expect.any(Number), + total_discount_amounts: expect.toBeOneOf([expect.toBeArray(), null]), + total_excluding_tax: expect.any(Number), + total_pretax_credit_amounts: expect.toBeOneOf([expect.toBeArray(), null]), + total_tax_amounts: expect.toBeArray(), + total_taxes: expect.toBeArray(), + transfer_data: expect.toBeOneOf([expect.toBeObject(), null]), + webhooks_delivered_at: expect.toBeOneOf([expect.any(Number), null]), +}; + +export const StripeInvoiceSchema: PaginatedSubscriptionInvoice = { + first: expect.any(String), + last: expect.any(String), + hasMore: expect.any(Boolean), + data: [InvoiceSchema], +}; + +export const StripePaymentMethodSchema = { + id: expect.any(String), + object: expect.any(String), + allow_redisplay: expect.any(String), + billing_details: expect.objectContaining({ + address: { + city: expect.toBeOneOf([expect.any(String), null]), + country: expect.toBeOneOf([expect.any(String), null]), + line1: expect.toBeOneOf([expect.any(String), null]), + line2: expect.toBeOneOf([expect.any(String), null]), + postal_code: expect.toBeOneOf([expect.any(String), null]), + state: expect.toBeOneOf([expect.any(String), null]), + }, + email: expect.toBeOneOf([expect.any(String), null]), + name: expect.toBeOneOf([expect.any(String), null]), + phone: expect.toBeOneOf([expect.any(String), null]), + }), + card: expect.objectContaining({ + brand: expect.any(String), + checks: { + address_line1_check: expect.toBeOneOf([expect.any(String), null]), + address_postal_code_check: expect.toBeOneOf([expect.any(String), null]), + cvc_check: expect.any(String), + }, + country: expect.any(String), + display_brand: expect.any(String), + exp_month: expect.any(Number), + exp_year: expect.any(Number), + fingerprint: expect.any(String), + funding: expect.any(String), + generated_from: expect.toBeOneOf([expect.any(String), null]), + last4: expect.any(String), + networks: expect.objectContaining({ + available: expect.toBeArray(), + preferred: expect.toBeOneOf([expect.any(String), null]), + }), + three_d_secure_usage: expect.objectContaining({ supported: expect.any(Boolean) }), + wallet: expect.toBeOneOf([expect.any(String), null]), + }), + created: expect.any(Number), + customer: expect.any(String), + livemode: expect.any(Boolean), + metadata: expect.toBeObject(), + type: expect.any(String), +}; + +export const UsageRecordSchema: UsageRecord = { + uuid: expect.any(String), + unitCount: expect.any(Number), + type: expect.any(String), + recordedAt: expect.toBeOneOf([expect.any(String), null]), + resetAt: expect.toBeOneOf([expect.any(String), null]), + planUuid: expect.any(String), + licenseUuid: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), +}; + +export const PaginatedUsageRecordsSchema: PaginatedUsageRecords = { + first: expect.toBeOneOf([expect.any(String), null]), + last: expect.toBeOneOf([expect.any(String), null]), + data: expect.arrayContaining([UsageRecordSchema]), +}; \ No newline at end of file diff --git a/src/schemas/v3/schemas-v3.ts b/src/schemas/v3/schemas-v3.ts new file mode 100644 index 00000000..2362d424 --- /dev/null +++ b/src/schemas/v3/schemas-v3.ts @@ -0,0 +1,186 @@ +import { + FeatureEnumOption, + FeatureV3, + LicenseV3, + OrganisationPaymentIntegrationV3, + PaginatedLicenses, + PlanCurrency, + PlanFeatureV3, + PlanV3, + PricingTableV3, + ProductCurrency, + ProductPricingTableV3, + ProductV3, +} from '../../types'; +import { EnumValueSchema} from '../v2/schemas-v2'; + +export const ProductSchemaV3: ProductV3 = { + uuid: expect.any(String), + slug: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + logoUrl: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.toBeOneOf([expect.any(String), null]), + organisation: expect.any(String), + status: expect.any(String), + paid: expect.any(Boolean), + isTest: expect.any(Boolean), + organisationPaymentIntegrationUuid: expect.any(String), + paymentIntegrationProductId: expect.toBeOneOf([expect.any(String), null]), + updatedAt: expect.any(String), + archivedAt: expect.toBeOneOf([expect.any(String), null]), +}; + +export const PlanSchemaV3: PlanV3 = { + uuid: expect.any(String), + slug: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.any(String), + status: expect.any(String), + evalDays: expect.any(Number), + perSeatAmount: expect.any(Number), + maxSeatAmount: expect.any(Number), + organisation: expect.any(String), + visibility: expect.any(String), + licenseType: expect.any(String), + hasAcceptedTransaction: expect.any(Boolean), + interval: expect.any(String), + length: expect.any(Number), + pricingType: expect.any(String), + isTest: expect.any(Boolean), + productUuid: expect.any(String), + updatedAt: expect.any(String), + archivedAt: expect.toBeOneOf([expect.any(String), null]), + isSubscribed: expect.toBeOneOf([expect.any(Boolean), undefined]), +}; + +export const FeatureSchemaV3: FeatureV3 & { + featureEnumOptions: FeatureEnumOption[] +} = { + defaultValue: expect.any(String), + description: expect.toBeOneOf([expect.any(String), null]), + displayName: expect.any(String), + productUuid: expect.any(String), + showUnlimited: expect.any(Boolean), + sortOrder: expect.any(Number), + status: expect.any(String), + updatedAt: expect.any(String), + uuid: expect.any(String), + valueType: expect.any(String), + variableName: expect.any(String), + visibility: expect.any(String), + featureEnumOptions: expect.arrayContaining([EnumValueSchema]) as FeatureEnumOption[] +}; + +export const PlanFeatureSchemaV3: PlanFeatureV3 = { + planUuid: expect.any(String), + featureUuid: expect.any(String), + value: expect.any(String), + enumValueUuid: expect.any(String), + isUnlimited: expect.any(Boolean), + updatedAt: expect.any(String), + feature: FeatureSchemaV3, + enumValue: EnumValueSchema, +}; + +export const ProductCurrencySchema: ProductCurrency = { + productUuid: expect.any(String), + currencyUuid: expect.any(String), + defaultCurrency: expect.any(Boolean), + currency: { + uuid: expect.any(String), + shortName: expect.any(String), + longName: expect.any(String), + symbol: expect.any(String), + }, +}; + +export const PlanCurrencySchema: PlanCurrency = { + planUuid: expect.any(String), + currencyUuid: expect.any(String), + paymentIntegrationPlanId: expect.any(String), + price: expect.any(Number), + hasAcceptedTransaction: expect.any(Boolean), + currency: { + uuid: expect.any(String), + shortName: expect.any(String), + longName: expect.any(String), + symbol: expect.any(String), + }, +}; + +export const ProductPricingTableSchemaV3: ProductPricingTableV3 = { + ...ProductSchemaV3, + features: expect.arrayContaining([FeatureSchemaV3]), + currencies: expect.arrayContaining([ProductCurrencySchema]), + plans: expect.arrayContaining([ + { + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + currencies: expect.arrayContaining([PlanCurrencySchema]), + }, + ]), + status: expect.any(String), + updatedAt: expect.any(String), + uuid: expect.any(String), +}; + +export const OrganisationPaymentIntegrationSchemaV3: OrganisationPaymentIntegrationV3 = { + uuid: expect.any(String), + organisation: expect.any(String), + integrationName: expect.any(String), + accountName: expect.any(String), + accountId: expect.any(String), + updatedAt: expect.any(String), + isTest: expect.any(Boolean), + newPaymentEnabled: expect.any(Boolean), + status: expect.any(String), +}; + +export const LicenseSchemaV3: LicenseV3 = { + uuid: expect.any(String), + subscriptionUuid: expect.any(String), + status: expect.toBeOneOf(['ACTIVE', 'CANCELED', 'EVALUATION', 'SCHEDULED', 'TRIALING', 'INACTIVE']), + granteeId: expect.toBeOneOf([expect.any(String), null]), + paymentService: expect.toBeOneOf(['ad-hoc', 'salable', 'stripe_existing']), + purchaser: expect.any(String), + type: expect.toBeOneOf(['licensed', 'metered', 'perSeat']), + productUuid: expect.any(String), + planUuid: expect.any(String), + startTime: expect.any(String), + endTime: expect.any(String), + updatedAt: expect.any(String), + isTest: expect.any(Boolean), +}; + +export const PaginatedLicensesSchemaV3: PaginatedLicenses = { + first: expect.toBeOneOf([expect.any(String), null]), + last: expect.toBeOneOf([expect.any(String), null]), + data: expect.arrayContaining([LicenseSchemaV3]), +}; + +export const PricingTableSchemaV3: PricingTableV3 = { + productUuid: expect.any(String), + featuredPlanUuid: expect.toBeOneOf([expect.any(String), null]), + status: expect.any(String), + updatedAt: expect.any(String), + uuid: expect.any(String), + featureOrder: expect.any(String), + product: { + ...ProductSchemaV3, + features: expect.arrayContaining([FeatureSchemaV3]), + currencies: expect.arrayContaining([ProductCurrencySchema]), + }, + plans: expect.arrayContaining([ + { + planUuid: expect.any(String), + pricingTableUuid: expect.any(String), + sortOrder: expect.any(Number), + updatedAt: expect.any(String), + plan: { + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + currencies: expect.arrayContaining([PlanCurrencySchema]), + }, + }, + ]), +}; \ No newline at end of file diff --git a/src/sessions/index.ts b/src/sessions/index.ts index 7b6288ce..c308c4ed 100644 --- a/src/sessions/index.ts +++ b/src/sessions/index.ts @@ -1,5 +1,4 @@ -import { ApiRequest, Session, SessionMetaData, SessionScope, TVersion, Version } from '../types'; -import { v2SessionMethods } from './v2'; +import { Session, SessionMetaData, SessionScope, TVersion, Version } from '../types'; export type SessionVersions = { [Version.V2]: { @@ -14,15 +13,7 @@ export type SessionVersions = { */ create: (data: { scope: T; metadata: SessionMetaData }) => Promise; }; + [Version.V3]: SessionVersions['v2'] }; export type SessionVersionedMethods = V extends keyof SessionVersions ? SessionVersions[V] : never; - -export const sessionsInit = (version: V, request: ApiRequest): SessionVersionedMethods => { - switch (version) { - case Version.V2: - return v2SessionMethods(request) as SessionVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; diff --git a/src/sessions/v2/sessions-v2.test.ts b/src/sessions/v2/sessions-v2.test.ts index ce2a6060..8d6a9361 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,65 +1,66 @@ -import Salable from '../..'; -import { Session, SessionScope, Version } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; +import { SessionScope } from '../../types'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { v4 as uuidv4 } from 'uuid'; import getEndTime from 'test-utils/helpers/get-end-time'; +import { randomUUID } from 'crypto'; +import { SessionSchema } from 'src/schemas/v2/schemas-v2'; -const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); - -const licenseUuid = uuidv4(); -const subscriptionUuid = uuidv4(); +const licenseUuid = randomUUID(); const testGrantee = '123456'; -const owner = 'subscription-owner' - -describe('Sessions V2 Tests', () => { - const apiKey = testUuids.devApiKeyV2; - const version = Version.V2; - - const salable = new Salable(apiKey, version); +describe('Sessions Tests for v2, v3', () => { + const salableVersions = {} as Record> + const versions: {version: TVersion; scopes: string[]}[] = [ + { version: 'v2', scopes: ['sessions:write'] }, + { version: 'v3', scopes: ['sessions:write'] } + ]; beforeAll(async () => { await generateTestData(); + for (const {version, scopes} of versions) { + const value = randomUUID() + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key', + organisation: testUuids.organisationId, + value, + scopes: JSON.stringify(scopes), + status: 'ACTIVE', + }, + }); + salableVersions[version] = initSalable(value, version); + } }); - it('createSession: Should successfully create a new session with PricingTable scope', async () => { - const data = await salable.sessions.create({ + it.each(versions)('createSession: Should successfully create a new session with PricingTable scope', async ({ version }) => { + const data = await salableVersions[version].sessions.create({ scope: SessionScope.PricingTable, metadata: { productUuid: testUuids.productUuid, }, }); - - expect(data).toEqual(sessionSchema); + expect(data).toEqual(SessionSchema); }); - - it('createSession: Should successfully create a new session with Checkout scope', async () => { - const data = await salable.sessions.create({ + it.each(versions)('createSession: Should successfully create a new session with Checkout scope', async ({ version }) => { + const data = await salableVersions[version].sessions.create({ scope: SessionScope.Checkout, metadata: { planUuid: testUuids.paidPlanUuid, }, }); - - expect(data).toEqual(sessionSchema); + expect(data).toEqual(SessionSchema); }); - - it('createSession: Should successfully create a new session with Invoice scope', async () => { - const data = await salable.sessions.create({ + it.each(versions)('Should successfully create a new session with Invoice scope', async ({ version }) => { + const data = await salableVersions[version].sessions.create({ scope: SessionScope.Invoice, metadata: { - subscriptionUuid: subscriptionUuid, + subscriptionUuid: testUuids.subscriptionWithInvoicesUuid, }, }); - - expect(data).toEqual(sessionSchema); + expect(data).toEqual(SessionSchema); }); }); -const sessionSchema: Session = { - sessionToken: expect.any(String), -}; - const generateTestData = async () => { await prismaClient.license.create({ data: { @@ -78,7 +79,7 @@ const generateTestData = async () => { capabilities: [ { name: 'CapabilityOne', - uuid: uuidv4(), + uuid: randomUUID(), status: 'ACTIVE', updatedAt: '2022-10-17T11:41:11.626Z', description: null, @@ -86,7 +87,7 @@ const generateTestData = async () => { }, { name: 'CapabilityTwo', - uuid: uuidv4(), + uuid: randomUUID(), status: 'ACTIVE', updatedAt: '2022-10-17T11:41:11.626Z', description: null, @@ -96,23 +97,4 @@ const generateTestData = async () => { endTime: getEndTime(1, 'years'), }, }); - - await prismaClient.subscription.create({ - data: { - lineItemIds: [stripeEnvs.basicSubscriptionFourLineItemId], - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionFourId, - uuid: subscriptionUuid, - email: 'tester@testing.com', - type: 'salable', - status: 'ACTIVE', - organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseUuid }] }, - product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.paidPlanUuid } }, - createdAt: new Date(), - updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), - owner, - }, - }); }; diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index 77304269..8cb3e616 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -6,7 +6,6 @@ import { SubscriptionPaymentMethod, SubscriptionPlan, SubscriptionSeat, - ApiRequest, TVersion, Version, GetAllSubscriptionsOptions, @@ -15,7 +14,6 @@ import { GetSubscriptionSeatsOptions, PaginatedSeats, GetSeatCountResponse, ManageSeatOptions, CreateSubscriptionInput } from '../types'; -import { v2SubscriptionMethods } from './v2'; export type SubscriptionVersions = { [Version.V2]: { @@ -165,7 +163,7 @@ export type SubscriptionVersions = { getUpdatePaymentLink: (subscriptionUuid: string) => Promise; /** - * Retrieves the customer portal link for a subscription. The link opens up a subscription management portal for your payment integration that will have an options for the customer to manage their subscription. + * Retrieves the customer portal link for a subscription. The link opens up a subscription management portal for your payment integration that will have options for the customer to manage their subscription. * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * @@ -288,15 +286,250 @@ export type SubscriptionVersions = { }, ) => Promise; }; -}; + [Version.V3]: { + /** + * Creates a subscription with the details provided + * + * @param {CreateSubscriptionInput} data - The details to create the new subscription with + * @param {CreateSubscriptionInput} data.planUuid - The UUID of the plan associated with the subscription. The planUuid can be found on the Plan view in the Salable dashboard + * @param {CreateSubscriptionInput} data.granteeId - (Optional) The grantee ID for the subscription. + * @param {CreateSubscriptionInput} data.expiryDate - (Optional) Provide a custom expiry date for the subscription; this will override the plan's default interval. + * @param {CreateSubscriptionInput} data.cancelAtPeriodEnd - (Optional) If set to true the subscription will not renew once the endTime date has passed. + * @param {CreateSubscriptionInput} data.quantity - (Optional) The number of seats to create on the subscription. Default is the plan's minimum seat limit. Only applicable to per seat plans. + * + * @returns {Promise} The data for the new subscription created + */ + create: (data: CreateSubscriptionInput) => Promise; + + /** + * Retrieves a list of all subscriptions. + * + * @param {{ status?: SubscriptionStatus; email?: string; cursor?: string; take?: string; expand?: string[] }} options - Filter and pagination options + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptions + * + * @returns {Promise} The data of the subscription requested + */ + getAll: (options?: GetAllSubscriptionsOptions) => Promise; + + /** + * Retrieves the subscription data based on the UUID. By default, the response does not contain any relational data. If you want to expand the relational data, you can do so with the `expand` query parameter. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionByUuid + * + * @returns {Promise} The data of the subscription requested + */ + getOne: ( + subscriptionUuid: string, + options?: { + expand: string[]; + }, + ) => Promise; + + /** + * Retrieves a list of a subscription's seats. Seats with the status "CANCELED" are ignored. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * @param {GetSubscriptionSeatsOptions} data - The properties for cursor pagination + * @param {GetSubscriptionSeatsOptions} data.cursor - The ID (cursor) of the record to take from in the request + * @param {GetSubscriptionSeatsOptions} data.take - The number of records to fetch. Default 20. + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionsSeats + * + * @returns {Promise} The seats of the subscription requested + */ + getSeats: (subscriptionUuid: string, options?: GetSubscriptionSeatsOptions) => Promise; + + /** + * Retrieves the aggregate number of seats. The response is broken down by assigned, unassigned and the total. Seats with the status `CANCELED` are ignored. + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionsSeatCount + * + * @returns {Promise} + */ + getSeatCount: (subscriptionUuid: string) => Promise; + + /** + * Update a subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * @param {UpdateSubscriptionInput} data - The properties of the subscription to update + * @param {UpdateSubscriptionInput} data.owner - The ID of the entity that owns the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/changeSubscriptionsPlan + * + * @returns {Promise} + */ + update: ( + subscriptionUuid: string, + data: UpdateSubscriptionInput, + ) => Promise; + + /** + * Changes a subscription's plan based on UUID. If the subscription is usage-based, the requested subscription will be canceled and a new subscription will be created on the plan you are changing to. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/changeSubscriptionsPlan + * + * @returns {Promise} + */ + changePlan: ( + subscriptionUuid: string, + options: { + planUuid: string; + proration?: string; + }, + ) => Promise; -export type SubscriptionVersionedMethods = V extends keyof SubscriptionVersions ? SubscriptionVersions[V] : never; + /** + * Retrieves a list of invoices for a subscription + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionInvoices + * + * @returns {Promise} + */ + getInvoices: (subscriptionUuid: string, options?: GetAllInvoicesOptions) => Promise; -export const subscriptionsInit = (version: V, request: ApiRequest): SubscriptionVersionedMethods => { - switch (version) { - case Version.V2: - return v2SubscriptionMethods(request) as SubscriptionVersionedMethods; - default: - throw new Error('Unsupported version'); - } + /** + * Cancels a subscription by providing the `subscriptionUuid` It will cancel immediately or at the end of the Subscription based on value of the `when` query parameter. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/cancelSubscription + * + * @returns {Promise} + */ + cancel: ( + subscriptionUuid: string, + options: { + when: 'now' | 'end'; + }, + ) => Promise; + + /** + * Retrieves the update payment link for a specific subscription. The link opens up a management portal for your payment integration that will have an option for the customer to update their payment details. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionUpdatePaymentLink + * + * @returns {Promise} + */ + getUpdatePaymentLink: (subscriptionUuid: string) => Promise; + + /** + * Retrieves the customer portal link for a subscription. The link opens up a subscription management portal for your payment integration that will have options for the customer to manage their subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCustomerPortalLink + * + * @returns {Promise} + */ + getPortalLink: (subscriptionUuid: string) => Promise; + + /** + * Retrieves the cancel subscription link for a specific subscription. The link opens up a subscription management portal for your payment integration that will have an option for the customer to cancel the subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCancelLink + * + * @returns {Promise} + */ + getCancelSubscriptionLink: (subscriptionUuid: string) => Promise; + + /** + * Retrieves the payment method used to pay for a subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionPaymentMethod + * + * @returns {Promise} + */ + getPaymentMethod: (subscriptionUuid: string) => Promise; + + /** + * Reactivate a Subscription's scheduled cancellation before the billing period has passed. If the billing period has passed and the Subscription has already been canceled please create a new Subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionReactivate + * + * @returns {Promise} + */ + reactivateSubscription: (subscriptionUuid: string) => Promise; + + /** + * Manage seats on a subscription + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/manageSubscriptionSeats + * + * @returns {Promise} + */ + manageSeats: ( + subscriptionUuid: string, + options: ManageSeatOptions[], + ) => Promise; + + /** + * Update seat count. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/updateSubscriptionSeatCount + * + * @returns {Promise} + */ + updateSeatCount: ( + subscriptionUuid: string, + options: { + increment?: number; + decrement?: number; + proration?: string; + }, + ) => Promise; + + /** + * Applies the specified coupon to the subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/addCoupon + * + * @returns {Promise} + */ + addCoupon: ( + subscriptionUuid: string, + options: { + couponUuid: string + }, + ) => Promise; + + /** + * Removes the specified coupon from the subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/removeCoupon + * + * @returns {Promise} + */ + removeCoupon: ( + subscriptionUuid: string, + options: { + couponUuid: string + }, + ) => Promise; + }; }; + +export type SubscriptionVersionedMethods = V extends keyof SubscriptionVersions ? SubscriptionVersions[V] : never; \ No newline at end of file diff --git a/src/subscriptions/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index af0e0eb8..98124c12 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -1,39 +1,32 @@ import prismaClient from '../../../test-utils/prisma/prisma-client'; -import Salable from '../..'; -import { PaginatedSubscription, Invoice, Plan, Subscription, PaginatedSubscriptionInvoice, Version, PaginatedLicenses, Capability, License, SeatActionType } from '../../types'; -import getEndTime from '../../../test-utils/helpers/get-end-time'; -import { v4 as uuidv4 } from 'uuid'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { SeatActionType } from '../../types'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; +import { initSalable } from '../../index'; +import { PaginatedLicensesSchema, PaginationSubscriptionSchema, PlanSchema, StripeInvoiceSchema, StripePaymentMethodSchema, SubscriptionSchema } from '../../schemas/v2/schemas-v2'; +import { addMonths } from 'date-fns'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); -const subscriptionUuid = uuidv4(); -const basicSubscriptionUuid = uuidv4(); -const proSubscriptionUuid = uuidv4(); -const perSeatSubscriptionUuid = uuidv4(); -const licenseUuid = uuidv4(); -const licenseTwoUuid = uuidv4(); -const licenseThreeUuid = uuidv4(); -const couponUuid = uuidv4(); -const perSeatBasicLicenseUuids = [uuidv4(), uuidv4(), uuidv4(), uuidv4(), uuidv4(), uuidv4()]; -const testGrantee = '123456'; -const testEmail = 'tester@domain.com'; -const owner = 'subscription-owner'; +const basicSubscriptionUuid = randomUUID(); +const perSeatSubscriptionUuid = randomUUID(); +const couponUuid = randomUUID(); +const testGrantee = randomUUID(); +const testEmail = randomUUID(); +const owner = randomUUID(); +const differentOwner = randomUUID(); +const subscriptionToBeCancelledUuid = randomUUID(); +const differentOwnerSubscriptionUuid = randomUUID() +const subscriptionUuidTwo = randomUUID(); describe('Subscriptions V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V2; - - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v2'); beforeAll(async () => { await generateTestData(); }); - afterAll(async () => { - await deleteTestData(); - }); - it('create: Should successfully create a subscription without a payment integration', async () => { const data = await salable.subscriptions.create({ planUuid: testUuids.paidPlanUuid, @@ -43,13 +36,13 @@ describe('Subscriptions V2 Tests', () => { expiryDate: '2045-07-06T12:00:00.000Z', }); - expect(data).toEqual(expect.objectContaining(subscriptionSchema)); + expect(data).toEqual(expect.objectContaining(SubscriptionSchema)); }); it('getAll: Should successfully fetch subscriptions', async () => { const data = await salable.subscriptions.getAll(); - expect(data).toEqual(paginationSubscriptionSchema); + expect(data).toEqual(PaginationSubscriptionSchema); }); it('getAll (w/ search params): Should successfully fetch subscriptions', async () => { @@ -63,16 +56,16 @@ describe('Subscriptions V2 Tests', () => { expect(dataWithSearchParams).toEqual({ first: expect.any(String), last: expect.any(String), - data: expect.arrayContaining([{ ...subscriptionSchema, plan: planSchema }]), + data: expect.arrayContaining([{ ...SubscriptionSchema, plan: PlanSchema }]), }); expect(dataWithSearchParams.data.length).toEqual(3); expect(dataWithSearchParams.data).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...subscriptionSchema, + ...SubscriptionSchema, status: 'ACTIVE', email: testEmail, - plan: planSchema, + plan: PlanSchema, }), ]), ); @@ -84,21 +77,22 @@ describe('Subscriptions V2 Tests', () => { sort: 'desc', productUuid: testUuids.productUuid, planUuid: testUuids.paidPlanTwoUuid, + take: 2 }); expect(dataWithSearchParams).toEqual({ first: expect.any(String), last: expect.any(String), - data: expect.arrayContaining([{ ...subscriptionSchema, plan: planSchema }]), + data: expect.arrayContaining([{ ...SubscriptionSchema, plan: PlanSchema }]), }); expect(dataWithSearchParams.data.length).toEqual(2); expect(dataWithSearchParams.data).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...subscriptionSchema, + ...SubscriptionSchema, productUuid: testUuids.productUuid, plan: { - ...planSchema, + ...PlanSchema, uuid: testUuids.paidPlanTwoUuid, }, }), @@ -108,21 +102,21 @@ describe('Subscriptions V2 Tests', () => { it('getAll (w/ search params owner): Should successfully fetch subscriptions', async () => { const dataWithSearchParams = await salable.subscriptions.getAll({ - owner: 'different-owner', + owner: differentOwner, }); expect(dataWithSearchParams).toEqual({ first: expect.any(String), last: expect.any(String), - data: expect.arrayContaining([{ ...subscriptionSchema }]), + data: expect.arrayContaining([{ ...SubscriptionSchema, owner: differentOwner }]), }); expect(dataWithSearchParams.data.length).toEqual(1); - expect(dataWithSearchParams.data).toEqual([{ ...subscriptionSchema, owner: 'different-owner' }]); + expect(dataWithSearchParams.data).toEqual([{ ...SubscriptionSchema, owner: differentOwner }]); }); it("getSeats: Should successfully fetch a subscription's seats", async () => { const data = await salable.subscriptions.getSeats(perSeatSubscriptionUuid); - expect(data).toEqual(paginatedLicensesSchema); + expect(data).toEqual(PaginatedLicensesSchema); }); it("getSeatCount: Should successfully fetch a subscription's seat count", async () => { @@ -137,57 +131,57 @@ describe('Subscriptions V2 Tests', () => { it('getOne: Should successfully fetch the specified subscription', async () => { const data = await salable.subscriptions.getOne(basicSubscriptionUuid); - expect(data).toEqual(subscriptionSchema); + expect(data).toEqual(SubscriptionSchema); expect(data).not.toHaveProperty('plan'); }); it('getOne (w/ search params): Should successfully fetch the specified subscription', async () => { const dataWithSearchParams = await salable.subscriptions.getOne(basicSubscriptionUuid, { expand: ['plan'] }); - expect(dataWithSearchParams).toEqual({ ...subscriptionSchema, plan: planSchema }); - expect(dataWithSearchParams).toHaveProperty('plan', planSchema); + expect(dataWithSearchParams).toEqual({ ...SubscriptionSchema, plan: PlanSchema }); + expect(dataWithSearchParams).toHaveProperty('plan', PlanSchema); }); it('getInvoices: Should successfully fetch a subscriptions invoices', async () => { - const data = await salable.subscriptions.getInvoices(basicSubscriptionUuid); + const data = await salable.subscriptions.getInvoices(testUuids.subscriptionWithInvoicesUuid); - expect(data).toEqual(stripeInvoiceSchema); + expect(data).toEqual(StripeInvoiceSchema); }); it('getInvoices (w/ search params): Should successfully fetch a subscriptions invoices', async () => { - const data = await salable.subscriptions.getInvoices(basicSubscriptionUuid, { take: 1 }); + const data = await salable.subscriptions.getInvoices(testUuids.subscriptionWithInvoicesUuid, { take: 1 }); - expect(data).toEqual(stripeInvoiceSchema); + expect(data).toEqual(StripeInvoiceSchema); expect(data.data.length).toEqual(1); }); it('getSwitchablePlans: Should successfully fetch a subscriptions switchable plans', async () => { - const data = await salable.subscriptions.getSwitchablePlans(basicSubscriptionUuid); - expect(data).toEqual(expect.arrayContaining([planSchema])); + const data = await salable.subscriptions.getSwitchablePlans(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(expect.arrayContaining([PlanSchema])); }); it('getUpdatePaymentLink: Should successfully fetch a subscriptions payment link', async () => { - const data = await salable.subscriptions.getUpdatePaymentLink(basicSubscriptionUuid); + const data = await salable.subscriptions.getUpdatePaymentLink(testUuids.subscriptionWithInvoicesUuid); expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); }); it('getPortalLink: Should successfully fetch a subscriptions portal link', async () => { - const data = await salable.subscriptions.getPortalLink(basicSubscriptionUuid); + const data = await salable.subscriptions.getPortalLink(testUuids.subscriptionWithInvoicesUuid); expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); }); it('getCancelSubscriptionLink: Should successfully fetch a subscriptions cancel link', async () => { - const data = await salable.subscriptions.getCancelSubscriptionLink(basicSubscriptionUuid); + const data = await salable.subscriptions.getCancelSubscriptionLink(testUuids.subscriptionWithInvoicesUuid); expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); }); it('getPaymentMethod: Should successfully fetch a subscriptions payment method', async () => { - const data = await salable.subscriptions.getPaymentMethod(basicSubscriptionUuid); + const data = await salable.subscriptions.getPaymentMethod(testUuids.subscriptionWithInvoicesUuid); - expect(data).toEqual(expect.objectContaining(stripePaymentMethodSchema)); + expect(data).toEqual(expect.objectContaining(StripePaymentMethodSchema)); }); it('changePlan: Should successfully change a subscriptions plan', async () => { @@ -208,12 +202,20 @@ describe('Subscriptions V2 Tests', () => { expect(data).toBeUndefined(); }); - it('addSeats: Should successfully add seats to the subscription', async () => { + it('addSeats: Should successfully add seats to the subscription of type none', async () => { const data = await salable.subscriptions.addSeats(perSeatSubscriptionUuid, { increment: 1, }); + expect(data).toBeUndefined(); + }); - expect(data).toEqual({ eventUuid: expect.any(String) }); + it('addSeats: Should successfully add seats to the subscription of type salable', async () => { + const data = await salable.subscriptions.addSeats(testUuids.perSeatSubscriptionUuid, { + increment: 1, + }); + expect(data).toEqual({ + eventUuid: expect.any(String), + }); }); it('removeSeats: Should successfully remove seats from a subscription', async () => { @@ -221,7 +223,16 @@ describe('Subscriptions V2 Tests', () => { decrement: 1, }); - expect(data).toEqual(expect.objectContaining({ eventUuid: expect.any(String) })); + expect(data).toBeUndefined(); + }); + + it('removeSeats: Should successfully remove seats to the subscription of type salable', async () => { + const data = await salable.subscriptions.removeSeats(testUuids.perSeatSubscriptionUuid, { + decrement: 1, + }); + expect(data).toEqual({ + eventUuid: expect.any(String), + }); }); it('update: Should successfully update a subscription owner', async () => { @@ -229,390 +240,73 @@ describe('Subscriptions V2 Tests', () => { owner: 'updated-owner', }); - expect(data).toEqual({ ...subscriptionSchema, owner: 'updated-owner' }); + expect(data).toEqual({ ...SubscriptionSchema, owner: 'updated-owner' }); }); it('addCoupon: Should successfully add the specified coupon to the subscription', async () => { - const data = await salable.subscriptions.addCoupon(subscriptionUuid, { couponUuid }); + const data = await salable.subscriptions.addCoupon(testUuids.couponSubscriptionUuidV2, { couponUuid }); expect(data).toBeUndefined(); }); it('removeCoupon: Should successfully remove the specified coupon from the subscription', async () => { - const data = await salable.subscriptions.removeCoupon(subscriptionUuid, { couponUuid }); + const data = await salable.subscriptions.removeCoupon(testUuids.couponSubscriptionUuidV2, { couponUuid }); expect(data).toBeUndefined(); }); it('cancel: Should successfully cancel the subscription', async () => { - const data = await salable.subscriptions.cancel(subscriptionUuid, { when: 'now' }); + const data = await salable.subscriptions.cancel(subscriptionToBeCancelledUuid, { when: 'now' }); expect(data).toBeUndefined(); }); }); -const licenseCapabilitySchema: Capability = { - uuid: expect.any(String), - productUuid: expect.any(String), - name: expect.any(String), - status: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - updatedAt: expect.any(String), -}; - -const licenseSchema: License = { - uuid: expect.any(String), - name: expect.toBeOneOf([expect.any(String), null]), - email: expect.toBeOneOf([expect.any(String), null]), - subscriptionUuid: expect.toBeOneOf([expect.any(String), null]), - status: expect.toBeOneOf(['ACTIVE', 'CANCELED', 'EVALUATION', 'SCHEDULED', 'TRIALING', 'INACTIVE']), - granteeId: expect.toBeOneOf([expect.any(String), null]), - paymentService: expect.toBeOneOf(['ad-hoc', 'salable', 'stripe_existing']), - purchaser: expect.any(String), - type: expect.toBeOneOf(['licensed', 'metered', 'perSeat', 'customId', 'user']), - productUuid: expect.any(String), - planUuid: expect.any(String), - capabilities: expect.arrayContaining([licenseCapabilitySchema]), - metadata: expect.toBeOneOf([expect.anything(), null]), - startTime: expect.any(String), - endTime: expect.any(String), - updatedAt: expect.any(String), - isTest: expect.any(Boolean), - cancelAtPeriodEnd: expect.any(Boolean), -}; - -const paginatedLicensesSchema: PaginatedLicenses = { - first: expect.toBeOneOf([expect.any(String), null]), - last: expect.toBeOneOf([expect.any(String), null]), - data: expect.arrayContaining([licenseSchema]), -}; - -const planSchema: Plan = { - uuid: expect.any(String), - name: expect.any(String), - slug: expect.any(String), - description: expect.toBeOneOf([expect.any(String), null]), - displayName: expect.any(String), - status: expect.any(String), - trialDays: expect.toBeOneOf([expect.any(Number), null]), - evaluation: expect.any(Boolean), - evalDays: expect.any(Number), - perSeatAmount: expect.any(Number), - maxSeatAmount: expect.any(Number), - organisation: expect.any(String), - visibility: expect.any(String), - licenseType: expect.any(String), - hasAcceptedTransaction: expect.any(Boolean), - interval: expect.any(String), - length: expect.any(Number), - active: expect.any(Boolean), - planType: expect.any(String), - pricingType: expect.any(String), - environment: expect.any(String), - isTest: expect.any(Boolean), - paddlePlanId: expect.toBeOneOf([expect.any(String), null]), - productUuid: expect.any(String), - salablePlan: expect.any(Boolean), - type: expect.toBeOneOf([expect.any(String), undefined]), - updatedAt: expect.any(String), - features: expect.toBeOneOf([expect.anything(), undefined]), - currencies: expect.toBeOneOf([expect.anything(), undefined]), - archivedAt: expect.toBeOneOf([expect.any(String), null]), -}; - -const subscriptionSchema: Subscription = { - uuid: expect.any(String), - paymentIntegrationSubscriptionId: expect.any(String), - productUuid: expect.any(String), - type: expect.any(String), // Todo: use enum type - isTest: expect.any(Boolean), - cancelAtPeriodEnd: expect.any(Boolean), - email: expect.toBeOneOf([expect.any(String), null]), - owner: expect.toBeOneOf([expect.any(String), null]), - organisation: expect.any(String), - quantity: expect.any(Number), - status: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), - expiryDate: expect.any(String), - lineItemIds: expect.toBeOneOf([expect.toBeArray(), null]), - planUuid: expect.any(String), -}; - -const paginationSubscriptionSchema: PaginatedSubscription = { - first: expect.any(String), - last: expect.any(String), - data: expect.arrayContaining([subscriptionSchema]), -}; - -const invoiceSchema: Invoice = { - id: expect.any(String), - object: expect.any(String), - account_country: expect.any(String), - account_name: expect.any(String), - account_tax_ids: expect.toBeOneOf([expect.toBeArray(), null]), - amount_due: expect.any(Number), - amount_paid: expect.any(Number), - amount_overpaid: expect.any(Number), - amount_remaining: expect.any(Number), - amount_shipping: expect.any(Number), - application: expect.toBeOneOf([expect.any(String), null]), - application_fee_amount: expect.toBeOneOf([expect.any(Number), null]), - attempt_count: expect.any(Number), - attempted: expect.any(Boolean), - auto_advance: expect.any(Boolean), - automatic_tax: expect.toBeObject(), - automatically_finalizes_at: expect.toBeOneOf([expect.any(Number), null]), - billing_reason: expect.any(String), - charge: expect.any(String), - collection_method: expect.any(String), - created: expect.any(Number), - currency: expect.any(String), - custom_fields: expect.toBeOneOf([expect.toBeArray(), null]), - customer: expect.any(String), - customer_address: expect.toBeOneOf([expect.toBeObject(), null]), - customer_email: expect.any(String), - customer_name: expect.toBeOneOf([expect.any(String), null]), - customer_phone: expect.toBeOneOf([expect.any(String), null]), - customer_shipping: expect.toBeOneOf([expect.toBeObject, null]), - customer_tax_exempt: expect.any(String), - customer_tax_ids: expect.toBeOneOf([expect.toBeArray(), null]), - default_payment_method: expect.toBeOneOf([expect.any(String), null]), - default_source: expect.toBeOneOf([expect.any(String), null]), - default_tax_rates: expect.toBeOneOf([expect.toBeArray(), null]), - description: expect.toBeOneOf([expect.any(String), null]), - discount: expect.toBeOneOf([expect.toBeObject(), null]), - discounts: expect.toBeOneOf([expect.toBeArray(), null]), - due_date: expect.toBeOneOf([expect.any(Number), null]), - effective_at: expect.any(Number), - ending_balance: expect.any(Number), - footer: expect.toBeOneOf([expect.any(String), null]), - from_invoice: expect.toBeOneOf([expect.toBeObject(), null]), - hosted_invoice_url: expect.any(String), - invoice_pdf: expect.any(String), - issuer: expect.toBeObject(), - last_finalization_error: expect.toBeOneOf([expect.toBeObject(), null]), - latest_revision: expect.toBeOneOf([expect.any(String), null]), - lines: expect.toBeObject(), - livemode: expect.any(Boolean), - metadata: expect.toBeObject(), - next_payment_attempt: expect.toBeOneOf([expect.any(Number), null]), - number: expect.any(String), - on_behalf_of: expect.toBeOneOf([expect.any(String), null]), - paid: expect.any(Boolean), - paid_out_of_band: expect.any(Boolean), - parent: expect.toBeObject(), - payment_intent: expect.any(String), - payment_settings: expect.toBeObject(), - period_end: expect.any(Number), - period_start: expect.any(Number), - post_payment_credit_notes_amount: expect.any(Number), - pre_payment_credit_notes_amount: expect.any(Number), - quote: expect.toBeOneOf([expect.any(String), null]), - receipt_number: expect.toBeOneOf([expect.any(String), null]), - rendering: expect.toBeOneOf([expect.toBeObject(), null]), - rendering_options: expect.toBeOneOf([expect.toBeObject(), undefined]), - shipping_cost: expect.toBeOneOf([expect.toBeObject(), null]), - shipping_details: expect.toBeOneOf([expect.toBeObject(), null]), - starting_balance: expect.any(Number), - statement_descriptor: expect.toBeOneOf([expect.any(String), null]), - status: expect.any(String), - status_transitions: expect.toBeObject(), - subscription: expect.any(String), - subscription_details: expect.toBeObject(), - subtotal: expect.any(Number), - subtotal_excluding_tax: expect.any(Number), - tax: expect.toBeOneOf([expect.any(Number), null]), - test_clock: expect.toBeOneOf([expect.any(String), null]), - total: expect.any(Number), - total_discount_amounts: expect.toBeOneOf([expect.toBeArray(), null]), - total_excluding_tax: expect.any(Number), - total_pretax_credit_amounts: expect.toBeOneOf([expect.toBeArray(), null]), - total_tax_amounts: expect.toBeArray(), - total_taxes: expect.toBeArray(), - transfer_data: expect.toBeOneOf([expect.toBeObject(), null]), - webhooks_delivered_at: expect.toBeOneOf([expect.any(Number), null]), -}; - -const stripeInvoiceSchema: PaginatedSubscriptionInvoice = { - first: expect.any(String), - last: expect.any(String), - hasMore: expect.any(Boolean), - data: [invoiceSchema], -}; - -const stripePaymentMethodSchema = { - id: expect.any(String), - object: expect.any(String), - allow_redisplay: expect.any(String), - billing_details: expect.objectContaining({ - address: { - city: expect.toBeOneOf([expect.any(String), null]), - country: expect.toBeOneOf([expect.any(String), null]), - line1: expect.toBeOneOf([expect.any(String), null]), - line2: expect.toBeOneOf([expect.any(String), null]), - postal_code: expect.toBeOneOf([expect.any(String), null]), - state: expect.toBeOneOf([expect.any(String), null]), - }, - email: expect.toBeOneOf([expect.any(String), null]), - name: expect.toBeOneOf([expect.any(String), null]), - phone: expect.toBeOneOf([expect.any(String), null]), - }), - card: expect.objectContaining({ - brand: expect.any(String), - checks: { - address_line1_check: expect.toBeOneOf([expect.any(String), null]), - address_postal_code_check: expect.toBeOneOf([expect.any(String), null]), - cvc_check: expect.any(String), - }, - country: expect.any(String), - display_brand: expect.any(String), - exp_month: expect.any(Number), - exp_year: expect.any(Number), - fingerprint: expect.any(String), - funding: expect.any(String), - generated_from: expect.toBeOneOf([expect.any(String), null]), - last4: expect.any(String), - networks: expect.objectContaining({ - available: expect.toBeArray(), - preferred: expect.toBeOneOf([expect.any(String), null]), - }), - three_d_secure_usage: expect.objectContaining({ supported: expect.any(Boolean) }), - wallet: expect.toBeOneOf([expect.any(String), null]), - }), - created: expect.any(Number), - customer: expect.any(String), - livemode: expect.any(Boolean), - metadata: expect.toBeObject(), - type: expect.any(String), -}; - -const deleteTestData = async () => { - await prismaClient.license.deleteMany({}); - await prismaClient.couponsOnSubscriptions.deleteMany({}); - await prismaClient.subscription.deleteMany({}); -}; - const generateTestData = async () => { - await prismaClient.license.create({ - data: { - name: null, - email: null, - status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, - product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - - await prismaClient.license.create({ - data: { - name: null, - email: null, - status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseTwoUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, - product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - - await prismaClient.license.create({ - data: { - name: null, - email: null, - status: 'ACTIVE', - granteeId: testGrantee, - paymentService: 'ad-hoc', - purchaser: 'tester@testing.com', - type: 'user', - uuid: licenseThreeUuid, - metadata: undefined, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, - product: { connect: { uuid: testUuids.productUuid } }, - startTime: undefined, - capabilities: [ - { - name: 'CapabilityOne', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - { - name: 'CapabilityTwo', - uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', - status: 'ACTIVE', - updatedAt: '2022-10-17T11:41:11.626Z', - description: null, - productUuid: testUuids.productUuid, - }, - ], - endTime: getEndTime(1, 'years'), - }, - }); - await prismaClient.subscription.create({ data: { uuid: basicSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionTwoId, - lineItemIds: [stripeEnvs.basicSubscriptionTwoLineItemId], + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionV2Id, + lineItemIds: [stripeEnvs.subscriptionV2LineItemId], email: testEmail, owner, type: 'salable', status: 'ACTIVE', organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseUuid }] }, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: addMonths(new Date(), 1), + }, + }, product: { connect: { uuid: testUuids.productUuid } }, plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, createdAt: new Date(), @@ -623,17 +317,50 @@ const generateTestData = async () => { await prismaClient.subscription.create({ data: { - uuid: subscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, - lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], + uuid: differentOwnerSubscriptionUuid, + paymentIntegrationSubscriptionId: differentOwnerSubscriptionUuid, + lineItemIds: [], email: testEmail, - owner, + owner: differentOwner, type: 'salable', status: 'ACTIVE', organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseUuid }] }, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [ + { + name: 'CapabilityOne', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: addMonths(new Date(), 1), + }, + }, product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, createdAt: new Date(), updatedAt: new Date(), expiryDate: new Date(Date.now() + 31536000000), @@ -642,40 +369,72 @@ const generateTestData = async () => { await prismaClient.subscription.create({ data: { - uuid: proSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.proSubscriptionId, - lineItemIds: [stripeEnvs.proSubscriptionLineItemId], + uuid: subscriptionUuidTwo, + paymentIntegrationSubscriptionId: subscriptionUuidTwo, + lineItemIds: [], email: testEmail, - owner: 'different-owner', + owner, type: 'salable', status: 'ACTIVE', organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseThreeUuid }, { uuid: licenseTwoUuid }] }, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: new Date(), + capabilities: [ + { + name: 'CapabilityOne', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: addMonths(new Date(), 1), + }, + }, product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, createdAt: new Date(), updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), + expiryDate: addMonths(new Date(), 1), }, }); await prismaClient.subscription.create({ data: { uuid: perSeatSubscriptionUuid, - lineItemIds: [stripeEnvs.perSeatBasicSubscriptionLineItemId], - paymentIntegrationSubscriptionId: stripeEnvs.perSeatBasicSubscriptionId, + lineItemIds: [], + paymentIntegrationSubscriptionId: perSeatSubscriptionUuid, email: testEmail, owner, - type: 'salable', + type: 'none', status: 'ACTIVE', organisation: testUuids.organisationId, license: { createMany: { - data: perSeatBasicLicenseUuids.slice(3, 6).map((uuid, i) => ({ + data: Array.from({ length: 3 }, (I, i) => ({ name: null, email: null, status: 'ACTIVE', - paymentService: 'salable', + paymentService: 'ad-hoc', purchaser: 'tester@testing.com', metadata: undefined, startTime: undefined, @@ -697,8 +456,7 @@ const generateTestData = async () => { productUuid: testUuids.productUuid, }, ], - endTime: getEndTime(1, 'years'), - uuid, + endTime: addMonths(new Date(), 1), granteeId: i < 2 ? `userId_${i}` : null, type: 'perSeat', planUuid: testUuids.perSeatMaxPlanUuid, @@ -710,11 +468,62 @@ const generateTestData = async () => { plan: { connect: { uuid: testUuids.perSeatPaidPlanUuid } }, createdAt: new Date(), updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), + expiryDate: addMonths(new Date(), 1), quantity: 2, }, }); + await prismaClient.subscription.create({ + data: { + uuid: subscriptionToBeCancelledUuid, + lineItemIds: [], + paymentIntegrationSubscriptionId: subscriptionToBeCancelledUuid, + email: testEmail, + owner, + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: new Date(), + capabilities: [ + { + name: 'CapabilityOne', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + { + name: 'CapabilityTwo', + uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', + status: 'ACTIVE', + updatedAt: '2022-10-17T11:41:11.626Z', + description: null, + productUuid: testUuids.productUuid, + }, + ], + endTime: addMonths(new Date(), 1), + }, + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + } + }) + await prismaClient.coupon.create({ data: { uuid: couponUuid, @@ -728,17 +537,9 @@ const generateTestData = async () => { isTest: false, durationInMonths: 1, status: 'ACTIVE', - product: { - connect: { - uuid: testUuids.productUuid, - }, - }, + product: { connect: { uuid: testUuids.productUuid } }, appliesTo: { - create: { - plan: { - connect: { uuid: testUuids.paidPlanTwoUuid }, - }, - }, + create: { plan: { connect: { uuid: testUuids.paidPlanTwoUuid } } }, }, }, }); diff --git a/src/subscriptions/v3/index.ts b/src/subscriptions/v3/index.ts new file mode 100644 index 00000000..b8c72ab3 --- /dev/null +++ b/src/subscriptions/v3/index.ts @@ -0,0 +1,27 @@ +import { SubscriptionVersions } from '..'; +import { ApiRequest } from '../../types'; +import { RESOURCE_NAMES, SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; + +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.SUBSCRIPTIONS}`; + +export const v3SubscriptionMethods = (request: ApiRequest): SubscriptionVersions['v3'] => ({ + create: (data) => request(getUrl(`${baseUrl}`, data), { method: 'POST', body: JSON.stringify(data) }), + getAll: (options) => request(getUrl(baseUrl, options), { method: 'GET' }), + getSeats: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}/seats`, options), { method: 'GET' }), + getSeatCount: (uuid) => request(getUrl(`${baseUrl}/${uuid}/seats/count`), { method: 'GET' }), + getOne: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}`, options), { method: 'GET' }), + changePlan: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}/change-plan`, options), { method: 'PUT', body: JSON.stringify(options) }), + update: (uuid, data) => request(getUrl(`${baseUrl}/${uuid}`, data), { method: 'PUT', body: JSON.stringify(data) }), + getInvoices: (uuid) => request(getUrl(`${baseUrl}/${uuid}/invoices`, {}), { method: 'GET' }), + cancel: (uuid, options) => request(getUrl(`${baseUrl}/${uuid}/cancel`, options), { method: 'PUT' }), + getUpdatePaymentLink: (uuid) => request(getUrl(`${baseUrl}/${uuid}/update-payment-link`, {}), { method: 'GET' }), + getPortalLink: (uuid) => request(getUrl(`${baseUrl}/${uuid}/customer-portal`, {}), { method: 'GET' }), + getCancelSubscriptionLink: (uuid) => request(getUrl(`${baseUrl}/${uuid}/cancel-payment-link`, {}), { method: 'GET' }), + getPaymentMethod: (uuid) => request(getUrl(`${baseUrl}/${uuid}/payment-method`, {}), { method: 'GET' }), + reactivateSubscription: (uuid) => request(getUrl(`${baseUrl}/${uuid}/reactivate`, {}), { method: 'PUT' }), + manageSeats: (uuid, options) => request(`${baseUrl}/${uuid}/manage-seats`, { method: 'PUT', body: JSON.stringify(options) }), + updateSeatCount: (uuid, options) => request(`${baseUrl}/${uuid}/seats`, { method: 'POST', body: JSON.stringify(options) }), + addCoupon: (uuid, options) => request(`${baseUrl}/${uuid}/coupons`, { method: 'POST', body: JSON.stringify(options) }), + removeCoupon: (uuid, options) => request(`${baseUrl}/${uuid}/coupons`, { method: 'PUT', body: JSON.stringify(options) }), +}); diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts new file mode 100644 index 00000000..f55b5a29 --- /dev/null +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -0,0 +1,408 @@ +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { + SeatActionType, +} from '../../types'; +import getEndTime from '../../../test-utils/helpers/get-end-time'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; +import { PlanFeatureSchemaV3, PlanCurrencySchema, PlanSchemaV3, PaginatedLicensesSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { addMonths } from 'date-fns'; +import { initSalable } from '../../index'; +import { PaginationSubscriptionSchema, StripeInvoiceSchema, StripePaymentMethodSchema, SubscriptionSchema } from '../../schemas/v2/schemas-v2'; + +const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); + +const basicSubscriptionUuid = randomUUID(); +const perSeatSubscriptionUuid = randomUUID(); +const couponUuid = randomUUID(); +const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; +const testGrantee = randomUUID(); +const testEmail = randomUUID(); +const owner = randomUUID(); +const differentOwner = randomUUID(); +const testSubscriptionTwoUuid = randomUUID(); +const differentOwnerSubscriptionUuid = randomUUID(); + + + +describe('Subscriptions V3 Tests', () => { + const apiKey = testUuids.devApiKeyV3; + const salable = initSalable(apiKey, 'v3'); + + beforeAll(async () => { + await generateTestData(); + }); + + it('create: Should successfully create a subscription without a payment integration', async () => { + const data = await salable.subscriptions.create({ + planUuid: testUuids.paidPlanUuid, + owner: 'example', + granteeId: 'test-grantee-id', + status: 'ACTIVE', + expiryDate: '2045-07-06T12:00:00.000Z', + }); + expect(data).toEqual(expect.objectContaining(SubscriptionSchema)); + }); + + it('getAll: Should successfully fetch subscriptions', async () => { + const data = await salable.subscriptions.getAll(); + expect(data).toEqual(PaginationSubscriptionSchema); + }); + + it('getAll (w/ search params): Should successfully fetch subscriptions', async () => { + const dataWithSearchParams = await salable.subscriptions.getAll({ + status: 'ACTIVE', + take: 3, + owner, + expand: ['plan'], + }); + expect(dataWithSearchParams.first).toEqual(expect.any(String)) + expect(dataWithSearchParams.last).toEqual(expect.any(String)) + expect(dataWithSearchParams.data.length).toEqual(3); + expect(dataWithSearchParams.data).toEqual( + expect.arrayContaining([ + { + ...SubscriptionSchema, + status: 'ACTIVE', + owner, + plan: PlanSchemaV3 + }, + ]), + ); + }); + + it('getAll (w/ search params sort, productUuid & planUuid): Should successfully fetch subscriptions', async () => { + const dataWithSearchParams = await salable.subscriptions.getAll({ + sort: 'desc', + productUuid: testUuids.productUuid, + planUuid: testUuids.paidPlanTwoUuid, + take: 4 + }); + expect(dataWithSearchParams).toEqual({ + first: expect.any(String), + last: expect.any(String), + data: expect.arrayContaining([SubscriptionSchema]), + }); + expect(dataWithSearchParams.data.length).toEqual(4); + expect(dataWithSearchParams.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + ...SubscriptionSchema, + productUuid: testUuids.productUuid, + planUuid: testUuids.paidPlanTwoUuid, + }), + ]), + ); + }); + + it('getAll (w/ search params owner): Should successfully fetch subscriptions', async () => { + const dataWithSearchParams = await salable.subscriptions.getAll({ + owner: differentOwner, + }); + expect(dataWithSearchParams).toEqual({ + first: expect.any(String), + last: expect.any(String), + data: expect.arrayContaining([SubscriptionSchema]), + }); + expect(dataWithSearchParams.data.length).toEqual(1); + expect(dataWithSearchParams.data).toEqual([{ ...SubscriptionSchema, owner: differentOwner }]); + }); + + it("getSeats: Should successfully fetch a subscription's seats", async () => { + const data = await salable.subscriptions.getSeats(perSeatSubscriptionUuid); + expect(data).toEqual(PaginatedLicensesSchemaV3); + }); + + it("getSeatCount: Should successfully fetch a subscription's seat count", async () => { + const data = await salable.subscriptions.getSeatCount(perSeatSubscriptionUuid); + expect(data).toEqual({ + count: expect.any(Number), + assigned: expect.any(Number), + unassigned: expect.any(Number), + }); + }); + + it('getOne: Should successfully fetch the specified subscription', async () => { + const data = await salable.subscriptions.getOne(basicSubscriptionUuid); + expect(data).toEqual(SubscriptionSchema); + expect(data).not.toHaveProperty('plan'); + }); + + it('getOne (w/ search params): Should successfully fetch the specified subscription', async () => { + const dataWithSearchParams = await salable.subscriptions.getOne(basicSubscriptionUuid, { expand: ['plan'] }); + expect(dataWithSearchParams).toEqual({ + ...SubscriptionSchema, + plan: { + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + currencies: expect.arrayContaining([PlanCurrencySchema]), + } + }); + }); + + it('getInvoices: Should successfully fetch a subscriptions invoices', async () => { + const data = await salable.subscriptions.getInvoices(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(StripeInvoiceSchema); + }); + + it('getInvoices (w/ search params): Should successfully fetch a subscriptions invoices', async () => { + const data = await salable.subscriptions.getInvoices(testUuids.subscriptionWithInvoicesUuid, { take: 1 }); + expect(data).toEqual(StripeInvoiceSchema); + expect(data.data.length).toEqual(1); + }); + + it('getUpdatePaymentLink: Should successfully fetch a subscriptions payment link', async () => { + const data = await salable.subscriptions.getUpdatePaymentLink(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); + }); + + it('getPortalLink: Should successfully fetch a subscriptions portal link', async () => { + const data = await salable.subscriptions.getPortalLink(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); + }); + + it('getCancelSubscriptionLink: Should successfully fetch a subscriptions cancel link', async () => { + const data = await salable.subscriptions.getCancelSubscriptionLink(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(expect.objectContaining({ url: expect.any(String) })); + }); + + it('getPaymentMethod: Should successfully fetch a subscriptions payment method', async () => { + const data = await salable.subscriptions.getPaymentMethod(testUuids.subscriptionWithInvoicesUuid); + expect(data).toEqual(expect.objectContaining(StripePaymentMethodSchema)); + }); + + it('changePlan: Should successfully change a subscriptions plan', async () => { + const data = await salable.subscriptions.changePlan(basicSubscriptionUuid, { + planUuid: testUuids.perSeatPaidPlanUuid, + }); + expect(data).toEqual(SubscriptionSchema); + }); + + it('manageSeats: Should successfully perform multiple seat actions', async () => { + const data = await salable.subscriptions.manageSeats(perSeatSubscriptionUuid, [ + { type: SeatActionType.assign, granteeId: 'assign_grantee_id' }, + { type: SeatActionType.unassign, granteeId: 'userId_0' }, + { type: SeatActionType.replace, granteeId: 'userId_1', newGranteeId: 'replace_grantee_id' }, + ]); + expect(data).toBeUndefined(); + }); + + it('updateSeatCount: Should successfully add seat to the subscription of type none', async () => { + const data = await salable.subscriptions.updateSeatCount(perSeatSubscriptionUuid, { + increment: 1, + }); + expect(data).toBeUndefined(); + }); + + it('updateSeatCount: Should successfully add seat to the subscription of type salable', async () => { + const data = await salable.subscriptions.updateSeatCount(testUuids.perSeatSubscriptionUuid, { + increment: 1, + }); + expect(data).toEqual({ + eventUuid: expect.any(String), + }); + }); + + it('updateSeatCount: Should successfully remove seat from the subscription of type none', async () => { + const data = await salable.subscriptions.updateSeatCount(perSeatSubscriptionUuid, { + decrement: 1, + }); + expect(data).toBeUndefined(); + }); + + it('updateSeatCount: Should successfully remove seat to the subscription of type salable', async () => { + const data = await salable.subscriptions.updateSeatCount(testUuids.perSeatSubscriptionUuid, { + decrement: 1, + }); + expect(data).toEqual({ + eventUuid: expect.any(String), + }); + }); + + it('update: Should successfully update a subscription owner', async () => { + const data = await salable.subscriptions.update(perSeatSubscriptionUuid, { + owner: 'updated-owner', + }); + expect(data).toEqual({ ...SubscriptionSchema, owner: 'updated-owner' }); + }); + + it('addCoupon: Should successfully add the specified coupon to the subscription', async () => { + const data = await salable.subscriptions.addCoupon(testUuids.couponSubscriptionUuidV3, { couponUuid }); + expect(data).toBeUndefined(); + }); + + it('removeCoupon: Should successfully remove the specified coupon from the subscription', async () => { + const data = await salable.subscriptions.removeCoupon(testUuids.couponSubscriptionUuidV3, { couponUuid }); + expect(data).toBeUndefined(); + }); + + it('cancel: Should successfully cancel the subscription', async () => { + const data = await salable.subscriptions.cancel(perSeatSubscriptionUuid, { when: 'now' }); + expect(data).toBeUndefined(); + }); +}); + +const generateTestData = async () => { + await prismaClient.subscription.create({ + data: { + uuid: basicSubscriptionUuid, + paymentIntegrationSubscriptionId: basicSubscriptionUuid, + lineItemIds: [], + email: testEmail, + owner, + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [], + endTime: addMonths(new Date(), 1), + } + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: testSubscriptionTwoUuid, + paymentIntegrationSubscriptionId: testSubscriptionTwoUuid, + lineItemIds: [], + email: testEmail, + owner, + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [], + endTime: addMonths(new Date(), 1), + } + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + }, + }); + await prismaClient.subscription.create({ + data: { + uuid: differentOwnerSubscriptionUuid, + paymentIntegrationSubscriptionId: differentOwnerSubscriptionUuid, + lineItemIds: [], + email: testEmail, + owner: differentOwner, + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: testGrantee, + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + type: 'licensed', + metadata: undefined, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: undefined, + capabilities: [], + endTime: addMonths(new Date(), 1), + } + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: addMonths(new Date(), 1), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: perSeatSubscriptionUuid, + lineItemIds: [], + paymentIntegrationSubscriptionId: perSeatSubscriptionUuid, + email: testEmail, + owner, + type: 'none', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + createMany: { + data: perSeatBasicLicenseUuids.slice(3, 6).map((uuid, i) => ({ + name: null, + email: null, + status: 'ACTIVE', + paymentService: 'ad-hoc', + purchaser: 'tester@testing.com', + metadata: undefined, + startTime: undefined, + capabilities: [], + endTime: getEndTime(1, 'years'), + uuid, + granteeId: i < 2 ? `userId_${i}` : null, + type: 'perSeat', + planUuid: testUuids.perSeatMaxPlanUuid, + productUuid: testUuids.productUuid, + })), + }, + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.perSeatPaidPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + quantity: 2, + }, + }); + + await prismaClient.coupon.create({ + data: { + uuid: couponUuid, + paymentIntegrationCouponId: stripeEnvs.couponV3Id, + name: 'Percentage Coupon', + duration: 'ONCE', + discountType: 'PERCENTAGE', + percentOff: 10, + expiresAt: null, + maxRedemptions: null, + isTest: false, + durationInMonths: 1, + status: 'ACTIVE', + product: { connect: { uuid: testUuids.productUuid } }, + appliesTo: { + create: { plan: { connect: { uuid: testUuids.paidPlanTwoUuid } } }, + }, + }, + }); +}; diff --git a/src/types.ts b/src/types.ts index 99ebbd97..2d6804ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import { EventStatus } from '@prisma/client'; export const Version = { V2: 'v2', + V3: 'v3', } as const; export type TVersion = (typeof Version)[keyof typeof Version]; export type ApiFetch = (apiKey: string, version: string) => ApiRequest; @@ -287,6 +288,51 @@ export type Plan = { archivedAt?: string; }; +export type PlanV3 = { + uuid: string; + slug: string; + description: string | null; + perSeatAmount: number; + maxSeatAmount: null; + displayName: string; + hasAcceptedTransaction: boolean; + status: string; + evalDays: number; + organisation: string; + visibility: string; + licenseType: string; + interval: string; + length: number; + pricingType: string; + productUuid: string; + updatedAt: string; + isTest: boolean; + archivedAt: string | null; + isSubscribed?: boolean; +}; + +export type GetAllPlansOptionsV3 = { + cursor?: string; + take?: number + sort?: 'asc' | 'desc'; + productUuid?: string; + archived?: boolean; +} + +export type GetAllPlansV3 = { + first: string; + last: string; + data: (PlanV3 & { + features?: (PlanFeatureV3 & { + feature: FeatureV3, + enumValue: EnumValue + })[] + currencies: (PlanCurrency & { + currency: Currency + })[] + })[] +} + export type IFeature = { uuid: string; name: string; @@ -352,6 +398,18 @@ export type PricingTable = { plans: PricingTablePlan[]; }; +export type PricingTableV3 = { + uuid: string; + status: ProductStatus; + featureOrder: string; + productUuid: string; + featuredPlanUuid: string; + updatedAt: string; + product: ProductV3 & { features: Feature[]; currencies: ProductCurrency[] }; + plans: PricingTablePlanV3[]; +}; + + export interface IPlanCheckoutParams extends ICheckoutDefaultParams, ICheckoutCustomerParams, ICheckoutVatParams { successUrl: string; cancelUrl: string; @@ -412,6 +470,10 @@ export type GetPlanOptions = { automaticTax?: string; }; +export type GetPlanOptionsV3 = { + expand?: ('features' | 'product' | 'currencies')[]; +}; + export type GetPlanCheckoutOptions = { successUrl: string; cancelUrl: string; @@ -429,6 +491,22 @@ export type GetPlanCheckoutOptions = { requirePaymentMethod?: boolean; }; +export type GetPlanCheckoutOptionsV3 = { + successUrl: string; + cancelUrl: string; + granteeId: string; + owner: string; + promoCode?: string; + allowPromoCode?: boolean; + customerEmail?: string; + customerId?: string; + currency?: string; + automaticTax?: string; + quantity?: string; + changeQuantity?: string; + requirePaymentMethod?: boolean; +}; + export type PlanFeature = { planUuid: string; featureUuid: string; @@ -444,6 +522,54 @@ export type PlanFeature = { enumValue: string | null; }; +export type EnumValue = { + uuid: string; + name: string; + featureUuid: string; + updatedAt: string; +} + +export type FeatureV3 = { + uuid: string; + description: string | null; + displayName: string; + variableName: string; + status: string; + visibility: string; + valueType: string; + defaultValue: string; + showUnlimited: boolean; + updatedAt: string; + sortOrder: string; + productUuid: string; +} + +export type GetAllFeaturesOptionsV3 = { + productUuid: string; + cursor?: string; + take?: number + sort?: 'asc' | 'desc'; +} + +export type GetAllFeaturesV3 = { + first: string; + last: string; + data: FeatureV3 & { + featureEnumOptions: FeatureEnumOption[] + }[] +} + +export type PlanFeatureV3 = { + planUuid: string; + featureUuid: string; + value: string; + enumValueUuid: string | null; + isUnlimited: boolean; + updatedAt: string; + feature: FeatureV3; + enumValue: EnumValue | null; +}; + export type PlanCapability = { planUuid: string; capabilityUuid: string; @@ -494,6 +620,37 @@ export type Product = { isTest: boolean; }; +export type ProductV3 = { + uuid: string; + slug: string; + description: string | null; + logoUrl: string | null; + displayName: string; + organisation: string; + status: string; + paid: boolean; + organisationPaymentIntegrationUuid: string; + paymentIntegrationProductId: string | null; + updatedAt: string; + archivedAt: string | null; + isTest: boolean; +}; + +export type GetAllProductsOptionsV3 = { + cursor?: string; + take?: number + sort?: 'asc' | 'desc'; + archived?: boolean; +} + + +export type GetAllProductsV3 = { + first: string; + last: string; + data: ProductV3[]; +}; + + export type ProductCapability = { uuid: string; name: string; @@ -548,6 +705,17 @@ export type PricingTablePlan = { }; }; +export type PricingTablePlanV3 = { + planUuid: string; + pricingTableUuid: string; + sortOrder: number; + updatedAt: string; + plan: PlanV3 & { + features: PlanFeatureV3[]; + currencies: PlanCurrency[]; + }; +}; + export type PricingTableParameters = { globalPlanOptions: { granteeId: string; @@ -585,18 +753,16 @@ export type PricingTableParameters = { }; }; -export type IOrganisationPaymentIntegration = { +export type OrganisationPaymentIntegrationV3 = { uuid: string; organisation: string; integrationName: string; accountName: string; - accountData: { - key: string; - encryptedData: string; - }; accountId: string; updatedAt: string; isTest: boolean; + newPaymentEnabled: boolean, + status: string; }; export type ProductPricingTable = { @@ -608,6 +774,15 @@ export type ProductPricingTable = { })[]; } & Product; +export type ProductPricingTableV3 = { + features: FeatureV3[]; + currencies: ProductCurrency[]; + plans: (PlanV3 & { + features: PlanFeatureV3[]; + currencies: PlanCurrency[]; + })[]; +} & ProductV3; + export type CheckLicensesCapabilitiesResponse = { capabilities: { capability: string; @@ -616,6 +791,14 @@ export type CheckLicensesCapabilitiesResponse = { signature: string; }; +export type EntitlementCheck = { + features: { + feature: string; + expiry: Date; + }[]; + signature: string; +} + export type CapabilitiesEndDates = { [key: string]: string; }; @@ -649,6 +832,22 @@ export type LicenseGetUsage = { unitCount: number; }; +export type LicenseV3 = { + uuid: string; + subscriptionUuid: string; + status: string; + granteeId: string | null; + paymentService: string; + purchaser: string; + type: string; + productUuid: string; + planUuid: string; + startTime: string; + endTime: string; + updatedAt: string; + isTest: boolean; +} + export type GetAllInvoicesOptions = { cursor?: string; take?: number; diff --git a/src/usage/index.ts b/src/usage/index.ts index 69c933d6..132b45de 100644 --- a/src/usage/index.ts +++ b/src/usage/index.ts @@ -1,12 +1,11 @@ -import { ApiRequest, TVersion, Version } from '..'; +import { TVersion, Version } from '..'; import { CurrentUsageOptions, CurrentUsageRecord, GetUsageOptions, PaginatedUsageRecords, UpdateLicenseUsageOptions -} from '../../src/types'; -import { v2UsageMethods } from './v2'; +} from '../types'; export type UsageVersions = { [Version.V2]: { @@ -41,15 +40,7 @@ export type UsageVersions = { */ updateLicenseUsage: (params: UpdateLicenseUsageOptions) => Promise; }; + [Version.V3]: UsageVersions['v2'] }; -export type UsageVersionedMethods = V extends keyof UsageVersions ? UsageVersions[V] : never; - -export const usageInit = (version: V, request: ApiRequest): UsageVersionedMethods => { - switch (version) { - case Version.V2: - return v2UsageMethods(request) as UsageVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; +export type UsageVersionedMethods = V extends keyof UsageVersions ? UsageVersions[V] : never; \ No newline at end of file diff --git a/src/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 28b0a595..4fa5594a 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,59 +1,67 @@ -import Salable, { Version } from '../..'; -import { PaginatedUsageRecords, UsageRecord } from '../../types'; +import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; -import { v4 as uuidv4 } from 'uuid'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; - -const version = Version.V2; +import { PaginatedUsageRecordsSchema, UsageRecordSchema } from '../../schemas/v2/schemas-v2'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); -const meteredLicenseUuid = uuidv4(); -const usageSubscriptionUuid = uuidv4(); +const meteredLicenseUuid = randomUUID(); +const usageSubscriptionUuid = randomUUID(); const testGrantee = 'userId_metered'; const owner = 'subscription-owner' -describe('Usage V2 Tests', () => { - const salable = new Salable(testUuids.devApiKeyV2, version); - +describe('Usage Tests for v2, v3', () => { + const salableVersions = {} as Record> + const versions: {version: TVersion; scopes: string[]}[] = [ + { version: 'v2', scopes: ['usage:read', 'usage:write'] }, + { version: 'v3', scopes: ['usage:read', 'usage:write'] } + ]; beforeAll(async () => { await generateTestData(); + for (const {version, scopes} of versions) { + const value = randomUUID() + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key', + organisation: testUuids.organisationId, + value, + scopes: JSON.stringify(scopes), + status: 'ACTIVE', + }, + }); + salableVersions[version] = initSalable(value, version); + } }); - it('getAllUsageRecords: Should successfully fetch the grantees usage records', async () => { - const data = await salable.usage.getAllUsageRecords({ + it.each(versions)('getAllUsageRecords: Should successfully fetch the grantees usage records', async ({ version }) => { + const data = await salableVersions[version].usage.getAllUsageRecords({ granteeId: testGrantee, }); - - expect(data).toEqual(paginatedUsageRecordsSchema); + expect(data).toEqual(PaginatedUsageRecordsSchema); }); - - it('getAllUsageRecords (w/ search params): Should successfully fetch the grantees usage records', async () => { - const data = await salable.usage.getAllUsageRecords({ + it.each(versions)('getAllUsageRecords (w/ search params): Should successfully fetch the grantees usage records', async ({ version }) => { + const data = await salableVersions[version].usage.getAllUsageRecords({ granteeId: testGrantee, type: 'recorded', }); - expect(data).toEqual( expect.objectContaining({ first: expect.toBeOneOf([expect.any(String), null]), last: expect.toBeOneOf([expect.any(String), null]), data: expect.arrayContaining([ { - ...usageRecordSchema, + ...UsageRecordSchema, type: 'recorded', }, ]), }), ); }); - - it('getCurrentUsageRecord: Should successfully fetch the current usage record for the grantee on plan', async () => { - const data = await salable.usage.getCurrentUsageRecord({ + it.each(versions)('getCurrentUsageRecord: Should successfully fetch the current usage record for the grantee on plan', async ({ version }) => { + const data = await salableVersions[version].usage.getCurrentUsageRecord({ granteeId: testGrantee, planUuid: testUuids.usageBasicMonthlyPlanUuid, }); - expect(data).toEqual( expect.objectContaining({ unitCount: expect.any(Number), @@ -61,37 +69,17 @@ describe('Usage V2 Tests', () => { }), ); }); - - it('updateLicenseUsage: Should successfully update the usage of the specified grantee', async () => { - const data = await salable.usage.updateLicenseUsage({ + it.each(versions)('updateLicenseUsage: Should successfully update the usage of the specified grantee', async ({ version }) => { + const data = await salableVersions[version].usage.updateLicenseUsage({ granteeId: testGrantee, planUuid: testUuids.usageBasicMonthlyPlanUuid, increment: 10, idempotencyKey: randomUUID(), }); - expect(data).toBeUndefined(); }); }); -const usageRecordSchema: UsageRecord = { - uuid: expect.any(String), - unitCount: expect.any(Number), - type: expect.any(String), - recordedAt: expect.toBeOneOf([expect.any(String), null]), - resetAt: expect.toBeOneOf([expect.any(String), null]), - planUuid: expect.any(String), - licenseUuid: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String), -}; - -const paginatedUsageRecordsSchema: PaginatedUsageRecords = { - first: expect.toBeOneOf([expect.any(String), null]), - last: expect.toBeOneOf([expect.any(String), null]), - data: expect.arrayContaining([usageRecordSchema]), -}; - const generateTestData = async () => { await prismaClient.subscription.create({ data: { diff --git a/test-utils/scripts/create-test-data.ts b/test-utils/scripts/create-salable-test-data.ts similarity index 72% rename from test-utils/scripts/create-test-data.ts rename to test-utils/scripts/create-salable-test-data.ts index 3973c67d..835bb356 100644 --- a/test-utils/scripts/create-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -3,13 +3,15 @@ import { generateKeyPairSync } from 'crypto'; import kmsSymmetricEncrypt from '../kms/kms-symmetric-encrypt'; import getConsoleLoader from '../helpers/console-loading-wheel'; import { config } from 'dotenv'; -import { StripeEnvsTypes } from '../stripe/create-stripe-test-data'; +import { StripeEnvsTypes } from './create-stripe-test-data'; +import { addMonths } from 'date-fns'; config({ path: '.env.test' }); export type TestDbData = { organisationId: string; devApiKeyV2: string; + devApiKeyV3: string; productUuid: string; productTwoUuid: string; freeMonthlyPlanUuid: string; @@ -27,56 +29,45 @@ export type TestDbData = { perSeatRangePlanUuid: string; usageBasicMonthlyPlanUuid: string; usageProMonthlyPlanUuid: string; + subscriptionWithInvoicesUuid: string; + perSeatSubscriptionUuid: string; + couponSubscriptionUuidV2: string; + couponSubscriptionUuidV3: string; currencyUuids: { gbp: string; usd: string; }; }; -const organisationId = 'test-org'; -const devApiKeyV2 = 'dddf2aa585c285478dae404803335c0013e795aa'; -const productUuid = '29c9a7c8-9a41-4e87-9e7e-7c62d293c131'; -const productTwoUuid = '2e0ac383-ee7e-44ba-90cb-ab3eabd56722'; -const freeMonthlyPlanUuid = '5a866dba-20c9-466f-88ac-e05c8980c90b'; -const paidPlanUuid = '351eefac-9b21-4299-8cde-302249d6fb1e'; -const paidPlanTwoUuid = 'bcd626d6-9507-42dd-9105-40c149853403'; -const perSeatPaidPlanUuid = 'cee50a36-c012-4a78-8e1a-b2bab93830ba'; -const paidYearlyPlanUuid = '111eefac-9b21-4299-8cde-302249d6f111'; -const freeYearlyPlanUuid = '22266dba-20c9-466f-88ac-e05c8980c222'; -const meteredPaidPlanUuid = 'a770ac97-4a36-4815-870c-396586b2d565'; -const meteredPaidPlanTwoUuid = '07cebad1-e2dc-44e0-8585-1ba4c91c032b'; -const comingSoonPlanUuid = '50238f96-4f2e-4fe9-a9a2-f2e917ae78bf'; -const perSeatUnlimitedPlanUuid = 'cab9b1b0-4b0f-4d6e-9dbb-a647ef1f8834'; -const perSeatMaxPlanUuid = 'fe8c96eb-88ea-4261-876c-951cec530e63'; -const perSeatMinPlanUuid = '9cbaf4e7-166a-447d-91ed-662b569b111d'; -const perSeatRangePlanUuid = '4606094a-0cec-40f3-b733-10cf65fdd5ce'; -const usageBasicMonthlyPlanUuid = '14f0c504-489f-4123-8f8d-1612e389c457'; -const usageProMonthlyPlanUuid = '447f2a62-5634-467d-83bb-1b7cead08779'; -const currencyUuids = { - gbp: 'b1b12bc9-6da7-4fd9-97e5-401d996c261c', - usd: '6ebfb42a-a78b-481c-bd79-9e857b432af9', -}; export const testUuids: TestDbData = { - organisationId, - devApiKeyV2, - productUuid, - productTwoUuid, - freeMonthlyPlanUuid, - paidPlanUuid, - paidPlanTwoUuid, - perSeatPaidPlanUuid, - paidYearlyPlanUuid, - freeYearlyPlanUuid, - meteredPaidPlanUuid, - meteredPaidPlanTwoUuid, - comingSoonPlanUuid, - perSeatUnlimitedPlanUuid, - perSeatMaxPlanUuid, - perSeatMinPlanUuid, - perSeatRangePlanUuid, - usageBasicMonthlyPlanUuid, - usageProMonthlyPlanUuid, - currencyUuids, + organisationId: 'c3016597-7677-415f-967e-e45643719141', + devApiKeyV2: 'bc4fcc73-de0f-4f65-ab19-ef76cf50f3d1', + devApiKeyV3: '8fb1637b-25cd-45ad-b5d0-5b06ac8da151', + productUuid: '2a5d3e36-45db-46ff-967e-b969b20718eb', + productTwoUuid: '5472a373-ce9c-4723-a467-35cce0bc71f5', + freeMonthlyPlanUuid: 'cc46dafa-cb0b-4409-beb8-5b111cb71133', + paidPlanUuid: 'f95ffb48-9df5-4cc0-9c0c-c425fb2876d0', + paidPlanTwoUuid: '0d2babfd-ab28-4d74-a5bb-6ed6f55e2675', + perSeatPaidPlanUuid: '2fc9e0c4-eb8d-4abd-8a66-b14b51e20915', + paidYearlyPlanUuid: 'b7964b46-a8bd-44ec-bd86-7225b6fcd384', + freeYearlyPlanUuid: '60cd5764-0543-4d47-a6a9-08dde004d263', + meteredPaidPlanUuid: 'da9585e1-6cdd-4f70-9a84-7f433e53601a', + meteredPaidPlanTwoUuid: 'b67c2d2b-40ef-4ce8-b70c-e81286dc467a', + comingSoonPlanUuid: 'c65e0952-6df4-4b9b-89f8-85538a87be04', + perSeatUnlimitedPlanUuid: '0f7518a9-c834-4e87-afcc-d3d9918c737b', + perSeatMaxPlanUuid: 'e9c8499a-0ab2-4cc0-bbbd-f60c4f0b3684', + perSeatMinPlanUuid: '060a6454-ca08-4796-b141-d70e5bbcc834', + perSeatRangePlanUuid: '65399089-76df-4cb7-b983-0efeae2976bf', + usageBasicMonthlyPlanUuid: 'f21c1c62-5421-4276-82f7-e44653aff400', + usageProMonthlyPlanUuid: '8ee7a446-b9bc-4e8c-93aa-8d9571a13707', + currencyUuids: { + gbp: '4efd9bde-61a6-4306-aed6-04a473496cf7', + usd: '6ec1a282-07b3-4716-bc3c-678c40b5d98e' + }, + subscriptionWithInvoicesUuid: 'b37357c6-bad1-4a6a-8c79-06935c66384f', + perSeatSubscriptionUuid: '9cd1d096-bd45-45a3-977d-5912895eabf2', + couponSubscriptionUuidV2: '893cd5cb-b313-4e8a-8e54-35781e7b0669', + couponSubscriptionUuidV3: 'd5b45c18-2a84-49c5-a099-2b2422fd1b80' }; const features = [ @@ -165,20 +156,39 @@ const apiKeyScopesV2 = [ 'usage:write', ]; +const apiKeyScopesV3 = [ + 'entitlements:check', + 'events:read', + 'features:read', + 'currencies:read', + 'billing:read', + 'billing:write', + 'organisations:read', + 'organisations:write', + 'subscriptions:read', + 'subscriptions:write', + 'pricing-tables:read', + 'plans:read', + 'features:read', + 'products:read', + 'sessions:write', + 'usage:read', + 'usage:write', +]; + const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256', publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); -export default async function createTestData(stripeEnvs: StripeEnvsTypes) { +export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) { const loadingWheel = getConsoleLoader('CREATING TEST DATA'); - const encryptedPrivateKey = await kmsSymmetricEncrypt(privateKey); await prismaClient.currency.create({ data: { - uuid: currencyUuids.gbp, + uuid: testUuids.currencyUuids.gbp, shortName: 'USD', longName: 'United States Dollar', symbol: '$', @@ -187,7 +197,7 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.currency.create({ data: { - uuid: currencyUuids.usd, + uuid: testUuids.currencyUuids.usd, shortName: 'GBP', longName: 'British Pound', symbol: '£', @@ -196,9 +206,9 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.organisation.create({ data: { - clerkOrgId: organisationId, - salablePlanUuid: organisationId, - svixAppId: organisationId, + clerkOrgId: testUuids.organisationId, + salablePlanUuid: testUuids.organisationId, + svixAppId: testUuids.organisationId, logoUrl: 'https://example.com/xxxxx.png', billingEmailId: 'xxxxx', addressDetails: {}, @@ -213,33 +223,42 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.apiKey.create({ data: { - name: 'Sample API Key', - organisation: organisationId, - value: devApiKeyV2, + name: 'Sample API Key V2', + organisation: testUuids.organisationId, + value: testUuids.devApiKeyV2, scopes: JSON.stringify(apiKeyScopesV2), status: 'ACTIVE', }, }); + await prismaClient.apiKey.create({ + data: { + name: 'Sample API Key V3', + organisation: testUuids.organisationId, + value: testUuids.devApiKeyV3, + scopes: JSON.stringify(apiKeyScopesV3), + status: 'ACTIVE', + }, + }); + const product = await prismaClient.product.create({ data: { name: 'Sample Product', description: 'This is a sample product for testing purposes', logoUrl: 'https://example.com/logo.png', displayName: 'Sample Product', - organisation: organisationId, + organisation: testUuids.organisationId, status: 'ACTIVE', paid: false, appType: 'CUSTOM', - isTest: false, - uuid: productUuid, + uuid: testUuids.productUuid, organisationPaymentIntegration: { create: { - organisation: organisationId, + organisation: testUuids.organisationId, accountId: process.env.STRIPE_ACCOUNT_ID, accountName: 'Widgy Widgets', integrationName: 'salable', - isTest: true, + status: 'active', accountData: {}, }, }, @@ -247,11 +266,11 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { create: [ { defaultCurrency: true, - currency: { connect: { uuid: currencyUuids.gbp } }, + currency: { connect: { uuid: testUuids.currencyUuids.gbp } }, }, { defaultCurrency: false, - currency: { connect: { uuid: currencyUuids.usd } }, + currency: { connect: { uuid: testUuids.currencyUuids.usd } }, }, ], }, @@ -275,27 +294,26 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { description: 'This is a sample product for testing purposes', logoUrl: 'https://example.com/logo.png', displayName: 'Sample Product Two', - organisation: organisationId, + organisation: testUuids.organisationId, status: 'ACTIVE', paid: false, appType: 'CUSTOM', - isTest: false, - uuid: productTwoUuid, + uuid: testUuids.productTwoUuid, organisationPaymentIntegration: { create: { - organisation: organisationId, + organisation: testUuids.organisationId, accountId: process.env.STRIPE_ACCOUNT_ID, accountName: 'Widgy Widgets Two', integrationName: 'salable', - isTest: true, accountData: {}, + status: 'active', }, }, currencies: { create: [ { defaultCurrency: true, - currency: { connect: { uuid: currencyUuids.gbp } }, + currency: { connect: { uuid: testUuids.currencyUuids.gbp } }, }, ], }, @@ -315,14 +333,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', - perSeatAmount: 2, + perSeatAmount: 1, name: 'Per Seat Basic Monthly Plan Name', description: 'Per Seat Basic Monthly Plan description', displayName: 'Per Seat Basic Monthly Plan Display Name', - uuid: perSeatPaidPlanUuid, + uuid: testUuids.perSeatPaidPlanUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -363,13 +381,13 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'licensed', name: 'Basic Monthly Plan Name', description: 'Basic Monthly Plan description', displayName: 'Basic Monthly Plan Display Name', - uuid: paidPlanUuid, + uuid: testUuids.paidPlanUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -410,13 +428,13 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'licensed', name: 'Basic Monthly Plan Two Name', description: 'Basic Monthly Plan Two description', displayName: 'Basic Monthly Plan Two Display Name', - uuid: paidPlanTwoUuid, + uuid: testUuids.paidPlanTwoUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -457,13 +475,13 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'free', licenseType: 'licensed', name: 'Free Monthly Plan Name', description: 'Free Monthly Plan description', displayName: 'Free Monthly Plan Display Name', - uuid: freeMonthlyPlanUuid, + uuid: testUuids.freeMonthlyPlanUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -497,13 +515,13 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'licensed', name: 'Basic Yearly Plan Name', description: 'Basic Yearly Plan description', displayName: 'Basic Yearly Plan Display Name', - uuid: paidYearlyPlanUuid, + uuid: testUuids.paidYearlyPlanUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -544,13 +562,13 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'free', licenseType: 'licensed', name: 'Free Yearly Plan Name', description: 'Free Yearly Plan description', displayName: 'Free Yearly Plan Display Name', - uuid: freeYearlyPlanUuid, + uuid: testUuids.freeYearlyPlanUuid, product: { connect: { uuid: product.uuid } }, status: 'ACTIVE', trialDays: 0, @@ -584,14 +602,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'metered', name: 'Usage Basic Monthly Plan Name', description: 'Usage Basic Monthly Plan description', displayName: 'Usage Basic Monthly Plan Display Name', - uuid: meteredPaidPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.meteredPaidPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -631,14 +649,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'metered', name: 'Usage Pro Monthly Plan Name', description: 'Usage Pro Monthly Plan description', displayName: 'Usage Pro Monthly Plan Display Name', - uuid: meteredPaidPlanTwoUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.meteredPaidPlanTwoUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -678,14 +696,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'free', licenseType: 'licensed', name: 'Future Plan Name', description: 'Future Plan description', displayName: 'Future Plan Display Name', - uuid: comingSoonPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.comingSoonPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -718,14 +736,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', name: 'Per Seat Unlimited Plan', description: 'Per Seat Unlimited Plan description', displayName: 'Per Seat Unlimited Plan', - uuid: perSeatUnlimitedPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.perSeatUnlimitedPlanUuid, + product: { connect: { uuid: testUuids.productUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -740,24 +758,19 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { maxSeatAmount: -1, visibility: 'public', currencies: { - create: productTwo.currencies.map((c) => ({ + create: product.currencies.map((c) => ({ currency: { connect: { uuid: c.currencyUuid } }, - price: 100, + price: 1500, paymentIntegrationPlanId: stripeEnvs.planPerSeatUnlimitedMonthlyGbpId, })), }, features: { - create: productTwo.features.map((f) => ({ + create: product.features.map((f) => ({ feature: { connect: { uuid: f.uuid } }, enumValue: { create: { name: 'Access', feature: { connect: { uuid: f.uuid } } }, }, value: getFeatureValue(f.variableName!), - isUnlimited: undefined as boolean | undefined, - isUsage: undefined as boolean | undefined, - pricePerUnit: 10, - minUsage: 1, - maxUsage: 100, })), }, }, @@ -766,14 +779,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', name: 'Per Seat Maximum Plan', description: 'Per Seat Maximum Plan description', displayName: 'Per Seat Maximum Plan', - uuid: perSeatMaxPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.perSeatMaxPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -814,14 +827,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', name: 'Per Seat Minimum Plan', description: 'Per Seat Minimum Plan description', displayName: 'Per Seat Minimum Plan', - uuid: perSeatMinPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.perSeatMinPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -862,14 +875,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', name: 'Per Seat Range Plan', description: 'Per Seat Range Plan description', displayName: 'Per Seat Range Plan', - uuid: perSeatRangePlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.perSeatRangePlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -910,14 +923,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'metered', name: 'Usage Basic Monthly Plan Name', description: 'Usage Basic Monthly Plan description', displayName: 'Usage Basic Monthly Plan Display Name', - uuid: usageBasicMonthlyPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.usageBasicMonthlyPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -958,14 +971,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'metered', name: 'Usage Pro Monthly Plan Name', description: 'Usage Pro Monthly Plan description', displayName: 'Usage Pro Monthly Plan Display Name', - uuid: usageProMonthlyPlanUuid, - product: { connect: { uuid: productTwoUuid } }, + uuid: testUuids.usageProMonthlyPlanUuid, + product: { connect: { uuid: testUuids.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -1006,20 +1019,156 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.capabilitiesOnPlans.createMany({ data: [ - { capabilityUuid: product.capabilities[0].uuid, planUuid: paidPlanUuid }, - { capabilityUuid: product.capabilities[0].uuid, planUuid: freeMonthlyPlanUuid }, + { capabilityUuid: product.capabilities[0].uuid, planUuid: testUuids.paidPlanUuid }, + { capabilityUuid: product.capabilities[0].uuid, planUuid: testUuids.freeMonthlyPlanUuid }, { capabilityUuid: product.capabilities[0].uuid, - planUuid: paidYearlyPlanUuid, + planUuid: testUuids.paidYearlyPlanUuid, }, - { capabilityUuid: product.capabilities[0].uuid, planUuid: freeYearlyPlanUuid }, + { capabilityUuid: product.capabilities[0].uuid, planUuid: testUuids.freeYearlyPlanUuid }, { capabilityUuid: product.capabilities[0].uuid, - planUuid: perSeatPaidPlanUuid, + planUuid: testUuids.perSeatPaidPlanUuid, }, ], }); + await prismaClient.subscription.create({ + data: { + uuid: testUuids.subscriptionWithInvoicesUuid, + organisation: testUuids.organisationId, + type: 'salable', + status: 'ACTIVE', + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithInvoicesId, + lineItemIds: [stripeEnvs.subscriptionWithInvoicesLineItemId], + productUuid: testUuids.productUuid, + planUuid: testUuids.paidPlanUuid, + owner: 'xxxxx', + quantity: 1, + createdAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: null, + paymentService: 'salable', + purchaser: 'xxxxx', + type: 'licensed', + plan: { connect: { uuid: testUuids.paidPlanUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: new Date(), + capabilities: [], + endTime: addMonths(new Date(), 1), + } + } + } + }) + + await prismaClient.subscription.create({ + data: { + uuid: testUuids.perSeatSubscriptionUuid, + organisation: testUuids.organisationId, + type: 'salable', + status: 'ACTIVE', + paymentIntegrationSubscriptionId: stripeEnvs.perSeatBasicSubscriptionId, + lineItemIds: [stripeEnvs.perSeatBasicSubscriptionLineItemId], + productUuid: testUuids.productUuid, + planUuid: testUuids.perSeatUnlimitedPlanUuid, + owner: 'xxxxx', + quantity: 3, + createdAt: new Date(), + expiryDate: addMonths(new Date(), 1), + license: { + createMany: { + data: Array.from({length: 3}, () => ({ + name: null, + email: null, + status: 'ACTIVE', + granteeId: null, + paymentService: 'salable', + purchaser: 'xxxxx', + type: 'licensed', + planUuid: testUuids.perSeatUnlimitedPlanUuid, + productUuid: testUuids.productUuid, + startTime: new Date(), + capabilities: [], + endTime: addMonths(new Date(), 1), + })) + } + } + } + }) + + await prismaClient.subscription.create({ + data: { + uuid: testUuids.couponSubscriptionUuidV2, + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithCouponV2Id, + lineItemIds: [stripeEnvs.subscriptionWithCouponV2LineItemId], + email: 'customer@email.com', + owner: 'xxxxx', + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: null, + paymentService: 'salable', + purchaser: 'xxxxx', + type: 'licensed', + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: new Date(), + capabilities: [], + endTime: addMonths(new Date(), 1), + } + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + }, + }); + + const v3CouponSubscription = await prismaClient.subscription.create({ + data: { + uuid: testUuids.couponSubscriptionUuidV3, + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithCouponV3Id, + lineItemIds: [stripeEnvs.subscriptionWithCouponV3LineItemId], + email: 'customer@email.com', + owner: 'xxxxx', + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + create: { + name: null, + email: null, + status: 'ACTIVE', + granteeId: null, + paymentService: 'salable', + purchaser: 'xxxxx', + type: 'licensed', + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, + startTime: new Date(), + capabilities: [], + endTime: addMonths(new Date(), 1), + } + }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + }, + }); + clearInterval(loadingWheel); } diff --git a/test-utils/stripe/create-stripe-custom-account.ts b/test-utils/scripts/create-stripe-custom-account.ts similarity index 100% rename from test-utils/stripe/create-stripe-custom-account.ts rename to test-utils/scripts/create-stripe-custom-account.ts diff --git a/test-utils/stripe/create-stripe-test-data.ts b/test-utils/scripts/create-stripe-test-data.ts similarity index 88% rename from test-utils/stripe/create-stripe-test-data.ts rename to test-utils/scripts/create-stripe-test-data.ts index 3960f9bb..41f392e9 100644 --- a/test-utils/stripe/create-stripe-test-data.ts +++ b/test-utils/scripts/create-stripe-test-data.ts @@ -1,5 +1,5 @@ import Stripe from 'stripe'; -import createStripeCustomAccount from '../../test-utils/stripe/create-stripe-custom-account'; +import createStripeCustomAccount from './create-stripe-custom-account'; import getConsoleLoader from '../helpers/console-loading-wheel'; import { config } from 'dotenv'; @@ -23,14 +23,14 @@ export interface StripeEnvsTypes { planBasicMonthlyUsdId: string; planTwoBasicMonthlyUsdId: string; planProMonthlyUsdId: string; - basicSubscriptionId: string; - basicSubscriptionTwoId: string; - basicSubscriptionThreeId: string; - basicSubscriptionFourId: string; - basicSubscriptionLineItemId: string; - basicSubscriptionTwoLineItemId: string; - basicSubscriptionThreeLineItemId: string; - basicSubscriptionFourLineItemId: string; + subscriptionWithInvoicesId: string; + subscriptionV2Id: string; + subscriptionWithCouponV2Id: string; + subscriptionWithCouponV3Id: string; + subscriptionWithInvoicesLineItemId: string; + subscriptionV2LineItemId: string; + subscriptionWithCouponV2LineItemId: string; + subscriptionWithCouponV3LineItemId: string; perSeatBasicSubscriptionId: string; perSeatBasicSubscriptionLineItemId: string; proSubscriptionId: string; @@ -40,6 +40,7 @@ export interface StripeEnvsTypes { planPerSeatMinimumMonthlyGbpId: string; planPerSeatRangeMonthlyGbpId: string; couponId: string; + couponV3Id: string; } export default async function createStripeData(): Promise { @@ -219,6 +220,11 @@ export default async function createStripeData(): Promise { duration_in_months: 3, percent_off: 10 }); + const couponV3 = await stripeConnect.coupons.create({ + duration: 'repeating', + duration_in_months: 3, + percent_off: 10 + }); for (let i = 10; i < 10; i++) { await stripeConnect.invoiceItems.create({ customer: stripeCustomer.id, @@ -258,20 +264,21 @@ export default async function createStripeData(): Promise { planBasicYearlyGbpId: stripePlanBasicYearlyGbp.id, perSeatBasicSubscriptionId: stripePerSeatBasicSubscription.id, perSeatBasicSubscriptionLineItemId: stripePerSeatBasicSubscription.items.data[0].id, - basicSubscriptionId: stripeBasicSubscription.id, - basicSubscriptionLineItemId: stripeBasicSubscription.items.data[0].id, - basicSubscriptionTwoId: stripeBasicSubscriptionTwo.id, - basicSubscriptionTwoLineItemId: stripeBasicSubscriptionTwo.items.data[0].id, - basicSubscriptionThreeId: stripeBasicSubscriptionThree.id, - basicSubscriptionFourId: stripeBasicSubscriptionFour.id, - basicSubscriptionThreeLineItemId: stripeBasicSubscriptionThree.items.data[0].id, + subscriptionWithInvoicesId: stripeBasicSubscription.id, + subscriptionWithInvoicesLineItemId: stripeBasicSubscription.items.data[0].id, + subscriptionV2Id: stripeBasicSubscriptionTwo.id, + subscriptionV2LineItemId: stripeBasicSubscriptionTwo.items.data[0].id, + subscriptionWithCouponV2Id: stripeBasicSubscriptionThree.id, + subscriptionWithCouponV3Id: stripeBasicSubscriptionFour.id, + subscriptionWithCouponV2LineItemId: stripeBasicSubscriptionThree.items.data[0].id, planProMonthlyGbpId: stripePlanProGbpMonthly.id, planBasicMonthlyUsdId: stripePlanBasicUsdMonthly.id, planTwoBasicMonthlyUsdId: stripePlanTwoBasicUsdMonthly.id, planProMonthlyUsdId: stripePlanProUsdMonthly.id, proSubscriptionId: stripeProSubscription.id, proSubscriptionLineItemId: stripeProSubscription.items.data[0].id, - basicSubscriptionFourLineItemId: stripeBasicSubscriptionFour.items.data[0].id, + subscriptionWithCouponV3LineItemId: stripeBasicSubscriptionFour.items.data[0].id, couponId: coupon.id, + couponV3Id: couponV3.id, }; }