From 9c050b53253ff5d053482264270108195ae44e0a Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 22 Aug 2025 13:04:44 +0100 Subject: [PATCH 01/28] feat: v3 of salable api added --- src/events/index.ts | 2 + src/events/v2/events-v2.test.ts | 34 +- src/index.ts | 15 +- src/plans/index.ts | 55 +- src/plans/v3/index.ts | 11 + src/plans/v3/plan-v3.test.ts | 71 ++ src/pricing-tables/index.ts | 14 + src/pricing-tables/v3/index.ts | 10 + .../v3/pricing-table-v3.test.ts | 112 +++ src/products/index.ts | 33 + src/products/v3/index.ts | 12 + src/products/v3/product-v3.test.ts | 63 ++ src/schemas/v3/schemas-v3.ts | 169 +++++ src/sessions/index.ts | 2 + src/sessions/v2/sessions-v2.test.ts | 47 +- src/subscriptions/index.ts | 246 +++++++ src/subscriptions/v3/index.ts | 27 + src/subscriptions/v3/subscriptions-v3.test.ts | 641 ++++++++++++++++++ src/types.ts | 141 +++- src/usage/index.ts | 4 +- src/usage/v2/usage-v2.test.ts | 54 +- test-utils/scripts/create-test-data.ts | 2 + 22 files changed, 1697 insertions(+), 68 deletions(-) create mode 100644 src/plans/v3/index.ts create mode 100644 src/plans/v3/plan-v3.test.ts create mode 100644 src/pricing-tables/v3/index.ts create mode 100644 src/pricing-tables/v3/pricing-table-v3.test.ts create mode 100644 src/products/v3/index.ts create mode 100644 src/products/v3/product-v3.test.ts create mode 100644 src/schemas/v3/schemas-v3.ts create mode 100644 src/subscriptions/v3/index.ts create mode 100644 src/subscriptions/v3/subscriptions-v3.test.ts diff --git a/src/events/index.ts b/src/events/index.ts index de92847b..e565e589 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -12,6 +12,7 @@ export type EventVersions = { */ getOne: (uuid: string) => Promise; }; + [Version.V3]: EventVersions['v2']; }; export type EventVersionedMethods = V extends keyof EventVersions ? EventVersions[V] : never; @@ -19,6 +20,7 @@ export type EventVersionedMethods = V extends keyof EventVer export const eventsInit = (version: V, request: ApiRequest): EventVersionedMethods => { switch (version) { case Version.V2: + case Version.V3: return v2EventMethods(request) as EventVersionedMethods; default: throw new Error('Unsupported version'); diff --git a/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index b0247765..6618b2d5 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,23 +1,37 @@ -import Salable from '../..'; -import { Event, EventTypeEnum, Version } from '../../types'; +import Salable, { TVersion } from '../..'; +import { Event, EventTypeEnum } from '../../types'; 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'; - -const version = Version.V2; +import { randomUUID } from 'crypto'; const eventUuid = uuidv4(); -describe('Events V2 Tests', () => { - const salable = new Salable(testUuids.devApiKeyV2, version); - +describe('Events Tests for v2, v3', () => { + const salableVersions = {} as Record> + const versions: {version: TVersion; scopes: string[]}[] = [ + { version: 'v2', scopes: ['events:read'] }, + { 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] = new Salable(value, version); + } }); - - it('getOne: Should successfully fetch the specified event', async () => { - const data = await salable.events.getOne(eventUuid); + it.each(versions)('getOne: Should successfully fetch the specified event', async ({ version }) => { + const data = await salableVersions[version].events.getOne(eventUuid); expect(data).toEqual(eventSchema); }); }); diff --git a/src/index.ts b/src/index.ts index 11f30f06..50eca2f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ 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 { subscriptionsInit, SubscriptionVersionedMethods } from './subscriptions'; +import { plansInit, PlanVersionedMethods } from './plans'; +import { productsInit, ProductVersionedMethods } from './products'; +import { pricingTablesInit, PricingTableVersionedMethods } from './pricing-tables'; import { UsageVersionedMethods, usageInit } from './usage'; import { eventsInit, EventVersionedMethods } from './events'; import { sessionsInit, SessionVersionedMethods } from './sessions'; @@ -13,6 +13,7 @@ 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]; @@ -59,7 +60,7 @@ export default class Salable { plans: PlanVersionedMethods; pricingTables: PricingTableVersionedMethods; subscriptions: SubscriptionVersionedMethods; - licenses: LicenseVersionedMethods; + licenses: LicenseVersionedMethods | undefined; usage: UsageVersionedMethods; events: EventVersionedMethods; sessions: SessionVersionedMethods @@ -71,7 +72,9 @@ export default class Salable { this.plans = plansInit(version, request); this.pricingTables = pricingTablesInit(version, request); this.subscriptions = subscriptionsInit(version, request); - this.licenses = licensesInit(version, request); + if (version === 'v2') { + this.licenses = licensesInit(version, request); + } this.usage = usageInit(version, request); this.events = eventsInit(version, request); this.sessions = sessionsInit(version, request); diff --git a/src/plans/index.ts b/src/plans/index.ts index feebf195..1a845a01 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 { + Plan, + PlanCheckout, + PlanFeature, + PlanCapability, + PlanCurrency, + ApiRequest, + TVersion, + Version, + GetPlanOptions, + GetPlanCheckoutOptions, + PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3 +} from '../types'; import { v2PlanMethods } from './v2'; +import { v3PlanMethods } from './v3'; export type PlanVersions = { [Version.V2]: { @@ -65,6 +78,44 @@ export type PlanVersions = { */ getCurrencies: (planUuid: string) => Promise; }; + [Version.V3]: { + /** + * Retrieves information about a plan 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} planUuid - The UUID of the plan + * + * Docs - https://docs.salable.app/api/v3#tag/Plans/operation/getPlanByUuid + * + * @returns {Promise} + */ + getOne: ( + planUuid: string, + options?: GetPlanOptionsV3 + ) => Promise; + + /** + + /** + * 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: GetPlanCheckoutOptions + ) => Promise; + }; }; export type PlanVersionedMethods = V extends keyof PlanVersions ? PlanVersions[V] : never; @@ -73,6 +124,8 @@ export const plansInit = (version: V, request: ApiRequest): switch (version) { case Version.V2: return v2PlanMethods(request) as PlanVersionedMethods; + case Version.V3: + return v3PlanMethods(request) as PlanVersionedMethods; default: throw new Error('Unsupported version'); } diff --git a/src/plans/v3/index.ts b/src/plans/v3/index.ts new file mode 100644 index 00000000..2bd7623b --- /dev/null +++ b/src/plans/v3/index.ts @@ -0,0 +1,11 @@ +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'] => ({ + 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..d7bcf1e6 --- /dev/null +++ b/src/plans/v3/plan-v3.test.ts @@ -0,0 +1,71 @@ +import Salable from '../..'; +import { + PlanCheckout, + Version +} from '../../types'; +import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { + PlanFeatureSchemaV3, + OrganisationPaymentIntegrationSchemaV3, PlanCurrencySchema, + PlanSchemaV3, + ProductSchemaV3 +} from '../../schemas/v3/schemas-v3'; + +describe('Plans V3 Tests', () => { + const apiKey = testUuids.devApiKeyV2; + const version = Version.V3; + + const salable = new Salable(apiKey, version); + + const planUuid = testUuids.paidPlanUuid; + + 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); + }); +}); + +const PlanCheckoutLinkSchema: PlanCheckout = { + checkoutUrl: expect.any(String), +}; diff --git a/src/pricing-tables/index.ts b/src/pricing-tables/index.ts index cf0bbaa8..20e2b4b4 100644 --- a/src/pricing-tables/index.ts +++ b/src/pricing-tables/index.ts @@ -1,5 +1,6 @@ import { PricingTable, ApiRequest, TVersion, Version } from '../types'; import { v2PricingTableMethods } from './v2'; +import { v3PricingTableMethods } from './v3'; export type PricingTableVersions = { [Version.V2]: { @@ -13,6 +14,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/v2#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; @@ -21,6 +33,8 @@ export const pricingTablesInit = (version: V, request: ApiRe switch (version) { case Version.V2: return v2PricingTableMethods(request) as PricingTableVersionedMethods; + case Version.V3: + return v3PricingTableMethods(request) as PricingTableVersionedMethods; default: throw new Error('Unsupported version'); } 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..ae525f64 --- /dev/null +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -0,0 +1,112 @@ +import Salable from '../..'; +import { PricingTableV3, Version } from '../../types'; +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { + FeatureSchemaV3, + PlanFeatureSchemaV3, PlanCurrencySchema, + PlanSchemaV3, + ProductCurrencySchema, + ProductSchemaV3 +} from '../../schemas/v3/schemas-v3'; + +const pricingTableUuid = 'aec06de8-3a3e-46eb-bd09-f1094c1b1b8d'; +describe('Pricing Table V3 Tests', () => { + const apiKey = testUuids.devApiKeyV2; + const version = Version.V3; + const salable = new Salable(apiKey, version); + beforeAll(async() => { + await generateTestData() + }) + + it('getAll: should successfully fetch all products', async () => { + const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); + expect(data).toEqual(expect.objectContaining(pricingTableSchema)); + }); +}); + +const pricingTableSchema: PricingTableV3 = { + 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), + 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]), + } + }]), +}; + + +const generateTestData = async () => { + + const product = await prismaClient.product.findFirst({ + where: { uuid: testUuids.productUuid }, + select: { + features: true, + uuid: 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..f1cb3197 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -1,5 +1,6 @@ import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, ApiRequest, TVersion, Version } from '../types'; import { v2ProductMethods } from './v2'; +import { v3ProductMethods } from './v3'; export type ProductVersions = { [Version.V2]: { @@ -76,6 +77,36 @@ export type ProductVersions = { */ getCurrencies(productUuid: string): Promise; }; + [Version.V3]: { + /** + * Retrieves a list of all products + * + * Docs - https://docs.salable.app/api/v2#tag/Products/operation/getProducts + * + * @returns {Promise} All products present on the account + */ + 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 {{ expand: string[]}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductByUuid + * + * @returns {Promise} + */ + getOne: (productUuid: string, options?: { expand: ('features' | 'currencies' | 'organisationPaymentIntegration' | 'plans')[] }) => Promise; + + /** + * 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 {{ granteeId?: string; currency?: string }} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductPricingTable + * + * @returns {Promise} + */ + getPricingTable: (productUuid: string, options: { owner: string; currency?: 'GBP' | 'USD' | 'EUR' | 'CAD' }) => Promise; + }; }; export type ProductVersionedMethods = V extends keyof ProductVersions ? ProductVersions[V] : never; @@ -84,6 +115,8 @@ export const productsInit = (version: V, request: ApiRequest switch (version) { case Version.V2: return v2ProductMethods(request) as ProductVersionedMethods; + case Version.V3: + return v3ProductMethods(request) as ProductVersionedMethods; default: throw new Error('Unsupported version'); } diff --git a/src/products/v3/index.ts b/src/products/v3/index.ts new file mode 100644 index 00000000..2ed5d75d --- /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: () => request(baseUrl, { 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..bb197a7e --- /dev/null +++ b/src/products/v3/product-v3.test.ts @@ -0,0 +1,63 @@ +import Salable from '../..'; +import { Version } from '../../types'; +import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { + EnumValueSchema, + FeatureSchemaV3, + PlanFeatureSchemaV3, + OrganisationPaymentIntegrationSchemaV3, + PlanCurrencySchema, + PlanSchemaV3, + ProductCurrencySchema, + ProductPricingTableSchemaV3, + ProductSchemaV3 +} from '../../schemas/v3/schemas-v3'; + +describe('Products V3 Tests', () => { + const apiKey = testUuids.devApiKeyV2; + const version = Version.V3; + const salable = new Salable(apiKey, version); + const productUuid = testUuids.productUuid; + + it('getAll: should successfully fetch all products', async () => { + const data = await salable.products.getAll(); + expect(data).toEqual(expect.arrayContaining([ProductSchemaV3])); + }); + 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: ['features', 'plans', 'currencies', 'organisationPaymentIntegration'], + }); + expect(data).toEqual({ + ...ProductSchemaV3, + currencies: expect.arrayContaining([ProductCurrencySchema]), + features: expect.arrayContaining([{ + ...FeatureSchemaV3, + featureEnumOptions: expect.arrayContaining([EnumValueSchema]) + }]), + plans: expect.arrayContaining([{ + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + currencies: expect.arrayContaining([PlanCurrencySchema]) + }]), + 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', + currency: 'GBP', + }); + expect(data).toEqual(ProductPricingTableSchemaV3); + }); +}); diff --git a/src/schemas/v3/schemas-v3.ts b/src/schemas/v3/schemas-v3.ts new file mode 100644 index 00000000..83f11348 --- /dev/null +++ b/src/schemas/v3/schemas-v3.ts @@ -0,0 +1,169 @@ +import { + EnumValue, + FeatureV3, LicenseV3, OrganisationPaymentIntegrationV3, + PlanCurrency, + PlanFeatureV3, + PlanV3, + ProductCurrency, + ProductPricingTableV3, + ProductV3, Subscription +} from '../../types'; + +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 = { + 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), +}; + +export const EnumValueSchema: EnumValue = { + uuid: expect.any(String), + name: expect.any(String), + featureUuid: expect.any(String), + updatedAt: expect.any(String), +} + +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 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), +}; \ No newline at end of file diff --git a/src/sessions/index.ts b/src/sessions/index.ts index 7b6288ce..73a3b4f1 100644 --- a/src/sessions/index.ts +++ b/src/sessions/index.ts @@ -14,6 +14,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; @@ -21,6 +22,7 @@ export type SessionVersionedMethods = V extends keyof Sessio export const sessionsInit = (version: V, request: ApiRequest): SessionVersionedMethods => { switch (version) { case Version.V2: + case Version.V3: 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..0fe80084 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,9 +1,10 @@ -import Salable from '../..'; -import { Session, SessionScope, Version } from '../../types'; +import Salable, { TVersion } from '../..'; +import { Session, SessionScope } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-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'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); @@ -12,46 +13,54 @@ const subscriptionUuid = uuidv4(); 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] = new Salable(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); }); - - 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); }); - - 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, }, }); - expect(data).toEqual(sessionSchema); }); }); diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index 77304269..4ce2cf23 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -16,6 +16,7 @@ import { PaginatedSeats, GetSeatCountResponse, ManageSeatOptions, CreateSubscriptionInput } from '../types'; import { v2SubscriptionMethods } from './v2'; +import { v3SubscriptionMethods } from './v3'; export type SubscriptionVersions = { [Version.V2]: { @@ -288,6 +289,249 @@ 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/v2#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/v2#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/v2#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/v2#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/v2#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/v2#tag/Subscriptions/operation/changeSubscriptionsPlan + * + * @returns {Promise} + */ + changePlan: ( + subscriptionUuid: string, + options: { + planUuid: string; + proration?: string; + }, + ) => Promise; + + /** + * Retrieves a list of invoices for a subscription + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionInvoices + * + * @returns {Promise} + */ + getInvoices: (subscriptionUuid: string, options?: GetAllInvoicesOptions) => Promise; + + /** + * 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/v2#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/v2#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 an options for the customer to manage their subscription. + * + * @param {string} subscriptionUuid - The UUID of the subscription to cancel + * + * Docs - https://docs.salable.app/api/v2#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/v2#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/v2#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/v2#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/v2#tag/Subscriptions/operation/manageSubscriptionSeats + * + * @returns {Promise} + */ + manageSeats: ( + subscriptionUuid: string, + options: ManageSeatOptions[], + ) => Promise; + + /** + * Incrementing will create unassigned licenses. + * + * @param {string} subscriptionUuid - The UUID of the subscription + * + * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/incrementSubscriptionSeats + * + * @returns {Promise} + */ + addSeats: ( + subscriptionUuid: string, + options: { + increment: 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/v2#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/v2#tag/Subscriptions/operation/removeCoupon + * + * @returns {Promise} + */ + removeCoupon: ( + subscriptionUuid: string, + options: { + couponUuid: string + }, + ) => Promise; + }; }; export type SubscriptionVersionedMethods = V extends keyof SubscriptionVersions ? SubscriptionVersions[V] : never; @@ -296,6 +540,8 @@ export const subscriptionsInit = (version: V, request: ApiRe switch (version) { case Version.V2: return v2SubscriptionMethods(request) as SubscriptionVersionedMethods; + case Version.V3: + return v3SubscriptionMethods(request) as SubscriptionVersionedMethods; default: throw new Error('Unsupported version'); } diff --git a/src/subscriptions/v3/index.ts b/src/subscriptions/v3/index.ts new file mode 100644 index 00000000..2864b224 --- /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) }), + addSeats: (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..a9964d0a --- /dev/null +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -0,0 +1,641 @@ +import prismaClient from '../../../test-utils/prisma/prisma-client'; +import Salable from '../..'; +import { + PaginatedSubscription, + Invoice, + PaginatedSubscriptionInvoice, + Version, + PaginatedLicenses, + SeatActionType, +} from '../../types'; +import getEndTime from '../../../test-utils/helpers/get-end-time'; +import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { randomUUID } from 'crypto'; +import { + PlanFeatureSchemaV3, + LicenseSchemaV3, + PlanCurrencySchema, + PlanSchemaV3, SubscriptionSchema +} from '../../schemas/v3/schemas-v3'; + +const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); + +const subscriptionUuid = randomUUID(); +const basicSubscriptionUuid = randomUUID(); +const proSubscriptionUuid = randomUUID(); +const perSeatSubscriptionUuid = randomUUID(); +const licenseUuid = randomUUID(); +const licenseTwoUuid = randomUUID(); +const licenseThreeUuid = randomUUID(); +const couponUuid = randomUUID(); +const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; +const testGrantee = '123456'; +const testEmail = 'tester@domain.com'; +const owner = 'subscription-owner'; + +describe('Subscriptions V3 Tests', () => { + const apiKey = testUuids.devApiKeyV2; + const version = Version.V3; + const salable = new Salable(apiKey, version); + + 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, + 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, + email: testEmail, + 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', + email: testEmail, + 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, + }); + expect(dataWithSearchParams).toEqual({ + first: expect.any(String), + last: expect.any(String), + data: expect.arrayContaining([SubscriptionSchema]), + }); + expect(dataWithSearchParams.data.length).toEqual(2); + 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: 'different-owner', + }); + 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: 'different-owner' }]); + }); + + it("getSeats: Should successfully fetch a subscription's seats", async () => { + const data = await salable.subscriptions.getSeats(perSeatSubscriptionUuid); + expect(data).toEqual(paginatedLicensesSchema); + }); + + 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(basicSubscriptionUuid); + 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 }); + 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(basicSubscriptionUuid); + 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); + 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); + 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); + 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).toBeUndefined(); + }); + + 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('addSeats: Should successfully add seats to the subscription', async () => { + const data = await salable.subscriptions.addSeats(perSeatSubscriptionUuid, { + increment: 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(subscriptionUuid, { couponUuid }); + expect(data).toBeUndefined(); + }); + + it('removeCoupon: Should successfully remove the specified coupon from the subscription', async () => { + const data = await salable.subscriptions.removeCoupon(subscriptionUuid, { couponUuid }); + expect(data).toBeUndefined(); + }); + + it('cancel: Should successfully cancel the subscription', async () => { + const data = await salable.subscriptions.cancel(subscriptionUuid, { when: 'now' }); + expect(data).toBeUndefined(); + }); +}); + +const paginatedLicensesSchema: PaginatedLicenses = { + first: expect.toBeOneOf([expect.any(String), null]), + last: expect.toBeOneOf([expect.any(String), null]), + data: expect.arrayContaining([LicenseSchemaV3]), +}; + +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], + email: testEmail, + owner, + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { connect: [{ uuid: licenseUuid }] }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: subscriptionUuid, + paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, + lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], + email: testEmail, + owner, + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { connect: [{ uuid: licenseUuid }] }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: proSubscriptionUuid, + paymentIntegrationSubscriptionId: stripeEnvs.proSubscriptionId, + lineItemIds: [stripeEnvs.proSubscriptionLineItemId], + email: testEmail, + owner: 'different-owner', + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { connect: [{ uuid: licenseThreeUuid }, { uuid: licenseTwoUuid }] }, + product: { connect: { uuid: testUuids.productUuid } }, + plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: perSeatSubscriptionUuid, + lineItemIds: [stripeEnvs.perSeatBasicSubscriptionLineItemId], + paymentIntegrationSubscriptionId: stripeEnvs.perSeatBasicSubscriptionId, + email: testEmail, + owner, + type: 'salable', + status: 'ACTIVE', + organisation: testUuids.organisationId, + license: { + createMany: { + data: perSeatBasicLicenseUuids.slice(3, 6).map((uuid, i) => ({ + name: null, + email: null, + status: 'ACTIVE', + paymentService: 'salable', + purchaser: 'tester@testing.com', + metadata: undefined, + 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'), + 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.couponId, + 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..edcd63e8 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,29 @@ 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 IFeature = { uuid: string; name: string; @@ -352,6 +376,23 @@ export type PricingTable = { plans: PricingTablePlan[]; }; +export type PricingTableV3 = { + uuid: string; + name: string; + status: ProductStatus; + title: string | null; + text: string | null; + theme: 'light' | 'dark' | string; + featureOrder: string; + productUuid: string; + customTheme: 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,12 +453,15 @@ export type GetPlanOptions = { automaticTax?: string; }; +export type GetPlanOptionsV3 = { + expand?: ('features' | 'product' | 'currencies')[]; +}; + export type GetPlanCheckoutOptions = { successUrl: string; cancelUrl: string; granteeId: string; - member?: string; - owner?: string; + owner: string; promoCode?: string; allowPromoCode?: boolean; customerEmail?: string; @@ -444,6 +488,39 @@ 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 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 +571,22 @@ 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 ProductCapability = { uuid: string; name: string; @@ -548,6 +641,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 +689,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 +710,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; @@ -649,6 +760,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..0b59e937 100644 --- a/src/usage/index.ts +++ b/src/usage/index.ts @@ -5,7 +5,7 @@ import { GetUsageOptions, PaginatedUsageRecords, UpdateLicenseUsageOptions -} from '../../src/types'; +} from '../types'; import { v2UsageMethods } from './v2'; export type UsageVersions = { @@ -41,6 +41,7 @@ export type UsageVersions = { */ updateLicenseUsage: (params: UpdateLicenseUsageOptions) => Promise; }; + [Version.V3]: UsageVersions['v2'] }; export type UsageVersionedMethods = V extends keyof UsageVersions ? UsageVersions[V] : never; @@ -48,6 +49,7 @@ export type UsageVersionedMethods = V extends keyof UsageVer export const usageInit = (version: V, request: ApiRequest): UsageVersionedMethods => { switch (version) { case Version.V2: + case Version.V3: return v2UsageMethods(request) as UsageVersionedMethods; default: throw new Error('Unsupported version'); diff --git a/src/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 28b0a595..8b0444b2 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,39 +1,49 @@ -import Salable, { Version } from '../..'; +import Salable, { TVersion } from '../..'; import { PaginatedUsageRecords, UsageRecord } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-test-data'; -import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto'; -const version = Version.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] = new Salable(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); }); - - 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]), @@ -47,13 +57,11 @@ describe('Usage V2 Tests', () => { }), ); }); - - 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,15 +69,13 @@ 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(); }); }); diff --git a/test-utils/scripts/create-test-data.ts b/test-utils/scripts/create-test-data.ts index 3973c67d..4d6993e3 100644 --- a/test-utils/scripts/create-test-data.ts +++ b/test-utils/scripts/create-test-data.ts @@ -239,6 +239,7 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { accountId: process.env.STRIPE_ACCOUNT_ID, accountName: 'Widgy Widgets', integrationName: 'salable', + status: 'active', isTest: true, accountData: {}, }, @@ -289,6 +290,7 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { integrationName: 'salable', isTest: true, accountData: {}, + status: 'active', }, }, currencies: { From aba2bfe1b3c80f2840761290f504523685e67fef Mon Sep 17 00:00:00 2001 From: Perry George Date: Tue, 2 Sep 2025 13:05:25 +0100 Subject: [PATCH 02/28] fix: created more global stripe and salable data to stop the test overwrtting each other --- __tests__/_setup/setup-test-envs.ts | 6 +- package-lock.json | 4 +- src/events/v2/events-v2.test.ts | 2 +- src/index.ts | 22 +- src/licenses/v2/licenses-v2.test.ts | 2 +- src/plans/index.ts | 2 +- src/plans/v2/plan-v2.test.ts | 2 +- src/plans/v3/plan-v3.test.ts | 2 +- .../v2/pricing-table-v2.test.ts | 2 +- .../v3/pricing-table-v3.test.ts | 2 +- src/products/index.ts | 27 +- src/products/v2/product-v2.test.ts | 2 +- src/products/v3/product-v3.test.ts | 2 +- src/sessions/v2/sessions-v2.test.ts | 2 +- src/subscriptions/index.ts | 13 +- src/subscriptions/v2/subscriptions-v2.test.ts | 84 +++--- src/subscriptions/v3/index.ts | 2 +- src/subscriptions/v3/subscriptions-v3.test.ts | 254 ++++------------ src/usage/v2/usage-v2.test.ts | 2 +- ...st-data.ts => create-salable-test-data.ts} | 272 +++++++++++------- .../create-stripe-custom-account.ts | 0 .../create-stripe-test-data.ts | 9 +- 22 files changed, 313 insertions(+), 402 deletions(-) rename test-utils/scripts/{create-test-data.ts => create-salable-test-data.ts} (79%) rename test-utils/{stripe => scripts}/create-stripe-custom-account.ts (100%) rename test-utils/{stripe => scripts}/create-stripe-test-data.ts (97%) 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/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/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index 6618b2d5..31a2bf7e 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,7 +1,7 @@ import Salable, { TVersion } from '../..'; import { Event, EventTypeEnum } 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 { v4 as uuidv4 } from 'uuid'; import { EventStatus } from '@prisma/client'; import { randomUUID } from 'crypto'; diff --git a/src/index.ts b/src/index.ts index 50eca2f3..17deb927 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,15 +55,27 @@ export const initRequest: ApiFetch = } }; -export default class Salable { + +interface SalableMethods { + // version: V; products: ProductVersionedMethods; plans: PlanVersionedMethods; pricingTables: PricingTableVersionedMethods; subscriptions: SubscriptionVersionedMethods; - licenses: LicenseVersionedMethods | undefined; usage: UsageVersionedMethods; events: EventVersionedMethods; - sessions: SessionVersionedMethods + sessions: SessionVersionedMethods; +} + + +export default class Salable implements SalableMethods { + products: ProductVersionedMethods; + plans: PlanVersionedMethods; + pricingTables: PricingTableVersionedMethods; + subscriptions: SubscriptionVersionedMethods; + usage: UsageVersionedMethods; + events: EventVersionedMethods; + sessions: SessionVersionedMethods; constructor(apiKey: string, version: V) { const request = initRequest(apiKey, version); @@ -72,9 +84,7 @@ export default class Salable { this.plans = plansInit(version, request); this.pricingTables = pricingTablesInit(version, request); this.subscriptions = subscriptionsInit(version, request); - if (version === 'v2') { - this.licenses = licensesInit(version, request); - } + if (version === 'v2') this.licenses = licensesInit(version, request); this.usage = usageInit(version, request); this.events = eventsInit(version, request); this.sessions = sessionsInit(version, request); diff --git a/src/licenses/v2/licenses-v2.test.ts b/src/licenses/v2/licenses-v2.test.ts index d0018c70..897ec93e 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -1,7 +1,7 @@ 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'; diff --git a/src/plans/index.ts b/src/plans/index.ts index 1a845a01..3d4ff700 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -86,7 +86,7 @@ export type PlanVersions = { * * Docs - https://docs.salable.app/api/v3#tag/Plans/operation/getPlanByUuid * - * @returns {Promise { const apiKey = testUuids.devApiKeyV2; diff --git a/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index d7bcf1e6..1886b9ac 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -3,7 +3,7 @@ import { PlanCheckout, Version } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { PlanFeatureSchemaV3, OrganisationPaymentIntegrationSchemaV3, PlanCurrencySchema, diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index d2128748..2d31dae2 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -1,7 +1,7 @@ import Salable from '../..'; import { PricingTable, 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'; const pricingTableUuid = 'aec06de8-3a3e-46eb-bd09-f1094c1b1b8d'; describe('Pricing Table V2 Tests', () => { diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index ae525f64..4cfe5f55 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -1,7 +1,7 @@ import Salable from '../..'; import { PricingTableV3, 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 { FeatureSchemaV3, PlanFeatureSchemaV3, PlanCurrencySchema, diff --git a/src/products/index.ts b/src/products/index.ts index f1cb3197..5efea148 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -1,4 +1,15 @@ -import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, ApiRequest, TVersion, Version } from '../types'; +import { + Plan, + Product, + ProductCapability, + ProductCurrency, + ProductFeature, + ProductPricingTable, + ApiRequest, + TVersion, + Version, + ProductV3, ProductPricingTableV3 +} from '../types'; import { v2ProductMethods } from './v2'; import { v3ProductMethods } from './v3'; @@ -83,17 +94,17 @@ 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 present on the account */ - getAll: () => Promise; + 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 {{ expand: string[]}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductByUuid + * @param {{ expand: string[]}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Products/operation/getProductByUuid * - * @returns {Promise} + * @returns {Promise} */ getOne: (productUuid: string, options?: { expand: ('features' | 'currencies' | 'organisationPaymentIntegration' | 'plans')[] }) => Promise; @@ -101,11 +112,11 @@ 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 {{ granteeId?: string; currency?: string }} options - (Optional) Filter parameters. See https://docs.salable.app/api/v2#tag/Products/operation/getProductPricingTable + * @param {{ owner: string; currency?: string }} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Products/operation/getProductPricingTable * - * @returns {Promise} + * @returns {Promise} */ - getPricingTable: (productUuid: string, options: { owner: string; currency?: 'GBP' | 'USD' | 'EUR' | 'CAD' }) => Promise; + getPricingTable: (productUuid: string, options: { owner: string; currency?: 'GBP' | 'USD' | 'EUR' | 'CAD' }) => Promise; }; }; diff --git a/src/products/v2/product-v2.test.ts b/src/products/v2/product-v2.test.ts index 7e8b2e32..8f5b19c1 100644 --- a/src/products/v2/product-v2.test.ts +++ b/src/products/v2/product-v2.test.ts @@ -1,6 +1,6 @@ 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'; describe('Products V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index bb197a7e..d51732f1 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -1,6 +1,6 @@ import Salable from '../..'; import { Version } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { EnumValueSchema, FeatureSchemaV3, diff --git a/src/sessions/v2/sessions-v2.test.ts b/src/sessions/v2/sessions-v2.test.ts index 0fe80084..70465e2c 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,6 +1,6 @@ import Salable, { TVersion } from '../..'; import { Session, SessionScope } from '../../types'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +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'; diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index 4ce2cf23..7e0b0ce7 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -484,21 +484,22 @@ export type SubscriptionVersions = { ) => Promise; /** - * Incrementing will create unassigned licenses. + * Update seat count. * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/incrementSubscriptionSeats + * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/updateSubscriptionSeatCount * - * @returns {Promise} + * @returns {Promise} */ - addSeats: ( + updateSeatCount: ( subscriptionUuid: string, options: { - increment: number; + increment?: number; + decrement?: number; proration?: string; }, - ) => Promise; + ) => Promise; /** * Applies the specified coupon to the subscription. diff --git a/src/subscriptions/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index af0e0eb8..179304d1 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -2,20 +2,18 @@ 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 { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; 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 basicSubscriptionUuid = randomUUID(); +const perSeatSubscriptionUuid = randomUUID(); +const licenseUuid = randomUUID(); +const licenseTwoUuid = randomUUID(); +const licenseThreeUuid = randomUUID(); +const couponUuid = randomUUID(); +const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; const testGrantee = '123456'; const testEmail = 'tester@domain.com'; const owner = 'subscription-owner'; @@ -149,43 +147,43 @@ describe('Subscriptions V2 Tests', () => { }); 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); }); 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.data.length).toEqual(1); }); it('getSwitchablePlans: Should successfully fetch a subscriptions switchable plans', async () => { - const data = await salable.subscriptions.getSwitchablePlans(basicSubscriptionUuid); + 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)); }); @@ -213,7 +211,7 @@ describe('Subscriptions V2 Tests', () => { increment: 1, }); - expect(data).toEqual({ eventUuid: expect.any(String) }); + expect(data).toBeUndefined(); }); it('removeSeats: Should successfully remove seats from a subscription', async () => { @@ -221,9 +219,11 @@ describe('Subscriptions V2 Tests', () => { decrement: 1, }); - expect(data).toEqual(expect.objectContaining({ eventUuid: expect.any(String) })); + expect(data).toBeUndefined(); }); + // TODO: paid per seat + it('update: Should successfully update a subscription owner', async () => { const data = await salable.subscriptions.update(perSeatSubscriptionUuid, { owner: 'updated-owner', @@ -233,19 +233,19 @@ describe('Subscriptions V2 Tests', () => { }); 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.couponSubscriptionUuid, { 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.couponSubscriptionUuid, { 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(perSeatSubscriptionUuid, { when: 'now' }); expect(data).toBeUndefined(); }); @@ -621,38 +621,20 @@ const generateTestData = async () => { }, }); + const differentOwnerSubscriptionUuid = randomUUID() await prismaClient.subscription.create({ data: { - uuid: subscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, - lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], - email: testEmail, - owner, - type: 'salable', - status: 'ACTIVE', - organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseUuid }] }, - product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, - createdAt: new Date(), - updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), - }, - }); - - await prismaClient.subscription.create({ - data: { - uuid: proSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.proSubscriptionId, - lineItemIds: [stripeEnvs.proSubscriptionLineItemId], + uuid: differentOwnerSubscriptionUuid, + paymentIntegrationSubscriptionId: differentOwnerSubscriptionUuid, + lineItemIds: [], email: testEmail, owner: 'different-owner', type: 'salable', status: 'ACTIVE', organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseThreeUuid }, { uuid: licenseTwoUuid }] }, + license: { connect: [{ uuid: licenseUuid }] }, 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), @@ -662,11 +644,11 @@ const generateTestData = async () => { 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: { @@ -675,7 +657,7 @@ const generateTestData = async () => { name: null, email: null, status: 'ACTIVE', - paymentService: 'salable', + paymentService: 'ad-hoc', purchaser: 'tester@testing.com', metadata: undefined, startTime: undefined, diff --git a/src/subscriptions/v3/index.ts b/src/subscriptions/v3/index.ts index 2864b224..b8c72ab3 100644 --- a/src/subscriptions/v3/index.ts +++ b/src/subscriptions/v3/index.ts @@ -21,7 +21,7 @@ export const v3SubscriptionMethods = (request: ApiRequest): SubscriptionVersions 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) }), - addSeats: (uuid, options) => request(`${baseUrl}/${uuid}/seats`, { method: 'POST', 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 index a9964d0a..48a1e11e 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -9,7 +9,7 @@ import { SeatActionType, } from '../../types'; import getEndTime from '../../../test-utils/helpers/get-end-time'; -import { testUuids } from '../../../test-utils/scripts/create-test-data'; +import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; import { PlanFeatureSchemaV3, @@ -17,16 +17,12 @@ import { PlanCurrencySchema, PlanSchemaV3, SubscriptionSchema } from '../../schemas/v3/schemas-v3'; +import { addMonths } from 'date-fns'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); -const subscriptionUuid = randomUUID(); const basicSubscriptionUuid = randomUUID(); -const proSubscriptionUuid = randomUUID(); const perSeatSubscriptionUuid = randomUUID(); -const licenseUuid = randomUUID(); -const licenseTwoUuid = randomUUID(); -const licenseThreeUuid = randomUUID(); const couponUuid = randomUUID(); const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; const testGrantee = '123456'; @@ -153,33 +149,33 @@ describe('Subscriptions V3 Tests', () => { }); 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); }); 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.data.length).toEqual(1); }); 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)); }); @@ -187,7 +183,7 @@ describe('Subscriptions V3 Tests', () => { const data = await salable.subscriptions.changePlan(basicSubscriptionUuid, { planUuid: testUuids.perSeatPaidPlanUuid, }); - expect(data).toBeUndefined(); + expect(data).toEqual(SubscriptionSchema); }); it('manageSeats: Should successfully perform multiple seat actions', async () => { @@ -199,11 +195,18 @@ describe('Subscriptions V3 Tests', () => { expect(data).toBeUndefined(); }); - it('addSeats: Should successfully add seats to the subscription', async () => { - const data = await salable.subscriptions.addSeats(perSeatSubscriptionUuid, { + it('updateSeatCount: Should successfully add seat to the subscription', async () => { + const data = await salable.subscriptions.updateSeatCount(perSeatSubscriptionUuid, { increment: 1, }); - expect(data).toEqual({ eventUuid: expect.any(String) }); + expect(data).toBeUndefined(); + }); + + it('updateSeatCount: Should successfully remove seat from the subscription', async () => { + const data = await salable.subscriptions.updateSeatCount(perSeatSubscriptionUuid, { + decrement: 1, + }); + expect(data).toBeUndefined(); }); it('update: Should successfully update a subscription owner', async () => { @@ -214,17 +217,17 @@ describe('Subscriptions V3 Tests', () => { }); 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.couponSubscriptionUuid, { 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.couponSubscriptionUuid, { 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(perSeatSubscriptionUuid, { when: 'now' }); expect(data).toBeUndefined(); }); }); @@ -390,179 +393,49 @@ const deleteTestData = async () => { }; 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], - email: testEmail, - owner, - type: 'salable', - status: 'ACTIVE', - organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseUuid }] }, - product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, - createdAt: new Date(), - updatedAt: new Date(), - expiryDate: new Date(Date.now() + 31536000000), - }, - }); - - await prismaClient.subscription.create({ - data: { - uuid: subscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, - lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], + paymentIntegrationSubscriptionId: basicSubscriptionUuid, + lineItemIds: [], email: testEmail, owner, - type: 'salable', + type: 'none', 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: [], + 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), - }, - }); - - await prismaClient.subscription.create({ - data: { - uuid: proSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.proSubscriptionId, - lineItemIds: [stripeEnvs.proSubscriptionLineItemId], - email: testEmail, - owner: 'different-owner', - type: 'salable', - status: 'ACTIVE', - organisation: testUuids.organisationId, - license: { connect: [{ uuid: licenseThreeUuid }, { uuid: licenseTwoUuid }] }, - product: { connect: { uuid: testUuids.productUuid } }, - plan: { connect: { uuid: testUuids.freeMonthlyPlanUuid } }, - 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: { @@ -571,28 +444,11 @@ const generateTestData = async () => { name: null, email: null, status: 'ACTIVE', - paymentService: 'salable', + paymentService: 'ad-hoc', purchaser: 'tester@testing.com', metadata: undefined, 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, - }, - ], + capabilities: [], endTime: getEndTime(1, 'years'), uuid, granteeId: i < 2 ? `userId_${i}` : null, @@ -614,7 +470,7 @@ const generateTestData = async () => { await prismaClient.coupon.create({ data: { uuid: couponUuid, - paymentIntegrationCouponId: stripeEnvs.couponId, + paymentIntegrationCouponId: stripeEnvs.couponV3Id, name: 'Percentage Coupon', duration: 'ONCE', discountType: 'PERCENTAGE', @@ -624,17 +480,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/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 8b0444b2..24d5d5f4 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,7 +1,7 @@ import Salable, { TVersion } from '../..'; import { PaginatedUsageRecords, UsageRecord } 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 { randomUUID } from 'crypto'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); diff --git a/test-utils/scripts/create-test-data.ts b/test-utils/scripts/create-salable-test-data.ts similarity index 79% rename from test-utils/scripts/create-test-data.ts rename to test-utils/scripts/create-salable-test-data.ts index 4d6993e3..2097b308 100644 --- a/test-utils/scripts/create-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -1,9 +1,11 @@ import prismaClient from '../../test-utils/prisma/prisma-client'; -import { generateKeyPairSync } from 'crypto'; +import { generateKeyPairSync, randomUUID } 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'; +import * as console from 'node:console'; config({ path: '.env.test' }); @@ -27,56 +29,40 @@ export type TestDbData = { perSeatRangePlanUuid: string; usageBasicMonthlyPlanUuid: string; usageProMonthlyPlanUuid: string; + subscriptionWithInvoicesUuid: string; + couponSubscriptionUuid: 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', + 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', + couponSubscriptionUuid: '893cd5cb-b313-4e8a-8e54-35781e7b0669' }; const features = [ @@ -171,14 +157,15 @@ const { publicKey, privateKey } = generateKeyPairSync('ec', { 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'); + console.log('===== testUuids', testUuids) 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 +174,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 +183,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: {}, @@ -214,8 +201,8 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.apiKey.create({ data: { name: 'Sample API Key', - organisation: organisationId, - value: devApiKeyV2, + organisation: testUuids.organisationId, + value: testUuids.devApiKeyV2, scopes: JSON.stringify(apiKeyScopesV2), status: 'ACTIVE', }, @@ -227,20 +214,18 @@ 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', - 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', status: 'active', - isTest: true, accountData: {}, }, }, @@ -248,11 +233,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 } }, }, ], }, @@ -270,25 +255,25 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { }, }); + console.log('===== product', product) + const productTwo = await prismaClient.product.create({ data: { name: 'Sample Product Two', 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', }, @@ -297,7 +282,7 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { create: [ { defaultCurrency: true, - currency: { connect: { uuid: currencyUuids.gbp } }, + currency: { connect: { uuid: testUuids.currencyUuids.gbp } }, }, ], }, @@ -317,14 +302,14 @@ export default async function createTestData(stripeEnvs: StripeEnvsTypes) { await prismaClient.plan.create({ data: { - organisation: organisationId, + organisation: testUuids.organisationId, pricingType: 'paid', licenseType: 'perSeat', perSeatAmount: 2, 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, @@ -365,13 +350,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, @@ -412,13 +397,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, @@ -459,13 +444,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, @@ -499,13 +484,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, @@ -546,13 +531,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, @@ -586,14 +571,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, @@ -633,14 +618,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, @@ -680,14 +665,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, @@ -720,14 +705,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.productTwoUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -768,14 +753,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, @@ -816,14 +801,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, @@ -864,14 +849,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, @@ -912,14 +897,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, @@ -960,14 +945,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, @@ -1008,20 +993,87 @@ 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.basicSubscriptionId, + lineItemIds: [stripeEnvs.basicSubscriptionLineItemId], + 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.couponSubscriptionUuid, + paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, + lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], + 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 97% rename from test-utils/stripe/create-stripe-test-data.ts rename to test-utils/scripts/create-stripe-test-data.ts index 3960f9bb..0fd645ec 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'; @@ -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, @@ -273,5 +279,6 @@ export default async function createStripeData(): Promise { proSubscriptionLineItemId: stripeProSubscription.items.data[0].id, basicSubscriptionFourLineItemId: stripeBasicSubscriptionFour.items.data[0].id, couponId: coupon.id, + couponV3Id: couponV3.id, }; } From a72d4e1c5dc3098e4b9246375b32afed3f5f38f1 Mon Sep 17 00:00:00 2001 From: Perry George Date: Tue, 2 Sep 2025 18:07:57 +0100 Subject: [PATCH 03/28] feat: wip v3 removed Salable class for initSalable function --- src/events/v2/events-v2.test.ts | 13 +- src/events/v2/index.ts | 2 +- src/index.ts | 108 ++-- src/licenses/v2/licenses-v2.test.ts | 588 +++++++++++------- src/plans/index.ts | 18 +- src/plans/v2/plan-v2.test.ts | 8 +- src/plans/v3/plan-v3.test.ts | 7 +- src/pricing-tables/index.ts | 4 +- .../v2/pricing-table-v2.test.ts | 11 +- .../v3/pricing-table-v3.test.ts | 51 +- src/products/index.ts | 2 +- src/products/v2/product-v2.test.ts | 6 +- src/products/v3/product-v3.test.ts | 6 +- src/schemas/v3/schemas-v3.ts | 36 +- src/sessions/v2/sessions-v2.test.ts | 31 +- src/subscriptions/v2/subscriptions-v2.test.ts | 375 +++++------ src/subscriptions/v3/subscriptions-v3.test.ts | 99 ++- src/types.ts | 22 +- src/usage/v2/usage-v2.test.ts | 6 +- .../scripts/create-salable-test-data.ts | 54 +- test-utils/scripts/create-stripe-test-data.ts | 32 +- 21 files changed, 850 insertions(+), 629 deletions(-) diff --git a/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index 31a2bf7e..96ab0758 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,18 +1,17 @@ -import Salable, { TVersion } from '../..'; +import { initSalable, TVersion, Version, VersionedMethods } from '../..'; import { Event, EventTypeEnum } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; -import { v4 as uuidv4 } from 'uuid'; import { EventStatus } from '@prisma/client'; import { randomUUID } from 'crypto'; -const eventUuid = uuidv4(); +const eventUuid = randomUUID(); describe('Events Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ - { version: 'v2', scopes: ['events:read'] }, - { version: 'v3', scopes: ['events:read'] } + { version: Version.V2, scopes: ['events:read'] }, + { version: Version.V3, scopes: ['events:read'] } ]; beforeAll(async () => { await generateTestData(); @@ -27,7 +26,7 @@ describe('Events Tests for v2, v3', () => { status: 'ACTIVE', }, }); - salableVersions[version] = new Salable(value, version); + salableVersions[version] = initSalable(value, version); } }); it.each(versions)('getOne: Should successfully fetch the specified event', async ({ version }) => { 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/index.ts b/src/index.ts index 17deb927..76bee6e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,24 @@ import { ErrorCodes, ResponseError, SalableParseError, SalableRequestError, SalableResponseError, SalableUnknownError, SalableValidationError, ValidationError } from './exceptions/salable-error'; -import { licensesInit, LicenseVersionedMethods } from './licenses'; -import { subscriptionsInit, SubscriptionVersionedMethods } from './subscriptions'; -import { plansInit, PlanVersionedMethods } from './plans'; -import { productsInit, ProductVersionedMethods } from './products'; -import { pricingTablesInit, PricingTableVersionedMethods } from './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'; export { ErrorCodes, SalableParseError, SalableRequestError, SalableResponseError, SalableUnknownError, SalableValidationError } from './exceptions/salable-error'; export type { ResponseError, ValidationError } from './exceptions/salable-error'; @@ -55,38 +67,62 @@ export const initRequest: ApiFetch = } }; - -interface SalableMethods { - // version: V; - products: ProductVersionedMethods; - plans: PlanVersionedMethods; - pricingTables: PricingTableVersionedMethods; - subscriptions: SubscriptionVersionedMethods; - usage: UsageVersionedMethods; - events: EventVersionedMethods; - sessions: SessionVersionedMethods; +type Methods = { + [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'> + }, + [Version.V3] : { + events: EventVersionedMethods<'v3'> + subscriptions: SubscriptionVersionedMethods<'v3'> + plans: PlanVersionedMethods<'v3'> + pricingTables: PricingTableVersionedMethods<'v3'> + products: ProductVersionedMethods<'v3'> + sessions: SessionVersionedMethods<'v3'> + usage: UsageVersionedMethods<'v3'> + } } +export type VersionedMethods = V extends keyof Methods ? Methods[V] : never; +function versionedMethods(request: ApiRequest): Methods { + return { + 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), + }, + v3: { + events: v2EventMethods(request), + subscriptions: v3SubscriptionMethods(request), + plans: v3PlanMethods(request), + pricingTables: v3PricingTableMethods(request), + products: v3ProductMethods(request), + sessions: v2SessionMethods(request), + usage: v2UsageMethods(request), + }, + }; +} -export default class Salable implements SalableMethods { - products: ProductVersionedMethods; - plans: PlanVersionedMethods; - pricingTables: PricingTableVersionedMethods; - subscriptions: SubscriptionVersionedMethods; - usage: UsageVersionedMethods; - events: EventVersionedMethods; - sessions: SessionVersionedMethods; - +class SalableBase { constructor(apiKey: string, version: V) { const request = initRequest(apiKey, version); - - this.products = productsInit(version, request); - this.plans = plansInit(version, request); - this.pricingTables = pricingTablesInit(version, request); - this.subscriptions = subscriptionsInit(version, request); - if (version === 'v2') this.licenses = licensesInit(version, request); - this.usage = usageInit(version, request); - this.events = eventsInit(version, request); - this.sessions = sessionsInit(version, request); + const versioned = versionedMethods(request)[version]; + if (!versioned) throw new Error('Unknown Version ' + version); + return versioned; } } + +export function initSalable(apiKey: string, version: V): SalableBase & VersionedMethods { + return new SalableBase(apiKey, version) as SalableBase & VersionedMethods; +} \ 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 897ec93e..e4974b4d 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -1,28 +1,28 @@ -import Salable from '../..'; -import { Capability, License, PaginatedLicenses, Plan, Version } from '../../types'; +import { Capability, License, PaginatedLicenses, Plan } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; 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'; + +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'); + + // TODO: add entitlements method for v3 beforeAll(async () => { await generateTestData(); @@ -180,7 +180,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', }, { @@ -315,274 +315,382 @@ const deleteTestData = async () => { }; 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(), + 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), }, - { - name: 'CapabilityTwo', - uuid: uuidv4(), - 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), - }, - }); + } + } + }) - 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 3d4ff700..69b289e1 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -1,16 +1,4 @@ -import { - Plan, - PlanCheckout, - PlanFeature, - PlanCapability, - PlanCurrency, - ApiRequest, - TVersion, - Version, - GetPlanOptions, - GetPlanCheckoutOptions, - PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3 -} from '../types'; +import { Plan, PlanCheckout, PlanFeature, PlanCapability, PlanCurrency, ApiRequest, TVersion, Version, GetPlanOptions, GetPlanCheckoutOptions, PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3, GetPlanCheckoutOptionsV3, PlanV3 } from '../types'; import { v2PlanMethods } from './v2'; import { v3PlanMethods } from './v3'; @@ -95,7 +83,7 @@ export type PlanVersions = { getOne: ( planUuid: string, options?: GetPlanOptionsV3 - ) => Promise Promise Promise; }; }; diff --git a/src/plans/v2/plan-v2.test.ts b/src/plans/v2/plan-v2.test.ts index 6288ab8e..35b302f4 100644 --- a/src/plans/v2/plan-v2.test.ts +++ b/src/plans/v2/plan-v2.test.ts @@ -1,12 +1,10 @@ -import Salable from '../..'; -import { Plan, PlanCapability, PlanCheckout, PlanCurrency, PlanFeature, Version } from '../../types'; +import { Plan, PlanCapability, PlanCheckout, PlanCurrency, PlanFeature } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { initSalable } from '../../index'; 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; diff --git a/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index 1886b9ac..fbb19c94 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -1,7 +1,5 @@ -import Salable from '../..'; import { PlanCheckout, - Version } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { @@ -10,12 +8,11 @@ import { PlanSchemaV3, ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { initSalable } from '../../index'; describe('Plans V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V3; - - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v3'); const planUuid = testUuids.paidPlanUuid; diff --git a/src/pricing-tables/index.ts b/src/pricing-tables/index.ts index 20e2b4b4..fb565a8b 100644 --- a/src/pricing-tables/index.ts +++ b/src/pricing-tables/index.ts @@ -1,4 +1,4 @@ -import { PricingTable, ApiRequest, TVersion, Version } from '../types'; +import { PricingTable, ApiRequest, TVersion, Version, PricingTableV3 } from '../types'; import { v2PricingTableMethods } from './v2'; import { v3PricingTableMethods } from './v3'; @@ -23,7 +23,7 @@ export type PricingTableVersions = { * * @returns {Promise} */ - getOne: (pricingTableUuid: string, options: { owner: string; currency?: string }) => Promise; + getOne: (pricingTableUuid: string, options: { owner: string; currency?: string }) => Promise; }; }; diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 2d31dae2..535b6663 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -1,14 +1,13 @@ -import Salable from '../..'; -import { PricingTable, Version } from '../../types'; +import { initSalable } from '../..'; +import { PricingTable } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; +import { randomUUID } from 'crypto'; -const pricingTableUuid = 'aec06de8-3a3e-46eb-bd09-f1094c1b1b8d'; +const pricingTableUuid = randomUUID(); describe('Pricing Table 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() diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index 4cfe5f55..06c4a8cc 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -1,60 +1,23 @@ -import Salable from '../..'; -import { PricingTableV3, Version } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; -import { - FeatureSchemaV3, - PlanFeatureSchemaV3, PlanCurrencySchema, - PlanSchemaV3, - ProductCurrencySchema, - ProductSchemaV3 -} from '../../schemas/v3/schemas-v3'; +import { PricingTableSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { initSalable } from '../../index'; +import { randomUUID } from 'crypto'; -const pricingTableUuid = 'aec06de8-3a3e-46eb-bd09-f1094c1b1b8d'; +const pricingTableUuid = randomUUID(); describe('Pricing Table V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V3; - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v3'); beforeAll(async() => { await generateTestData() }) - it('getAll: should successfully fetch all products', async () => { + it('getOne: should successfully fetch all products', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); - expect(data).toEqual(expect.objectContaining(pricingTableSchema)); + expect(data).toEqual(expect.objectContaining(PricingTableSchemaV3)); }); }); -const pricingTableSchema: PricingTableV3 = { - 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), - 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]), - } - }]), -}; - const generateTestData = async () => { diff --git a/src/products/index.ts b/src/products/index.ts index 5efea148..9c416524 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -106,7 +106,7 @@ export type ProductVersions = { * * @returns {Promise} */ - getOne: (productUuid: string, options?: { expand: ('features' | 'currencies' | 'organisationPaymentIntegration' | 'plans')[] }) => Promise; + getOne: (productUuid: string, options?: { expand: ('features' | 'currencies' | 'organisationPaymentIntegration' | 'plans')[] }) => Promise; /** * 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. diff --git a/src/products/v2/product-v2.test.ts b/src/products/v2/product-v2.test.ts index 8f5b19c1..9bd6f1cc 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-salable-test-data'; +import { initSalable } from '../../index'; 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; diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index d51732f1..2b136160 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -1,5 +1,3 @@ -import Salable from '../..'; -import { Version } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { EnumValueSchema, @@ -12,11 +10,11 @@ import { ProductPricingTableSchemaV3, ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; +import { initSalable } from '../../index'; describe('Products V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V3; - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v3'); const productUuid = testUuids.productUuid; it('getAll: should successfully fetch all products', async () => { diff --git a/src/schemas/v3/schemas-v3.ts b/src/schemas/v3/schemas-v3.ts index 83f11348..ebfa1a05 100644 --- a/src/schemas/v3/schemas-v3.ts +++ b/src/schemas/v3/schemas-v3.ts @@ -1,13 +1,4 @@ -import { - EnumValue, - FeatureV3, LicenseV3, OrganisationPaymentIntegrationV3, - PlanCurrency, - PlanFeatureV3, - PlanV3, - ProductCurrency, - ProductPricingTableV3, - ProductV3, Subscription -} from '../../types'; +import { EnumValue, FeatureV3, LicenseV3, OrganisationPaymentIntegrationV3, PlanCurrency, PlanFeatureV3, PlanV3, PricingTableV3, ProductCurrency, ProductPricingTableV3, ProductV3, Subscription } from '../../types'; export const ProductSchemaV3: ProductV3 = { uuid: expect.any(String), @@ -166,4 +157,29 @@ export const SubscriptionSchema: Subscription = { expiryDate: expect.any(String), lineItemIds: expect.toBeOneOf([expect.toBeArray(), null]), planUuid: expect.any(String), +}; + +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/v2/sessions-v2.test.ts b/src/sessions/v2/sessions-v2.test.ts index 70465e2c..3339b4ee 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,4 +1,4 @@ -import Salable, { TVersion } from '../..'; +import { initSalable, TVersion, VersionedMethods } from '../..'; import { Session, SessionScope } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import prismaClient from '../../../test-utils/prisma/prisma-client'; @@ -6,15 +6,11 @@ import { v4 as uuidv4 } from 'uuid'; import getEndTime from 'test-utils/helpers/get-end-time'; import { randomUUID } from 'crypto'; -const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); - const licenseUuid = uuidv4(); -const subscriptionUuid = uuidv4(); const testGrantee = '123456'; -const owner = 'subscription-owner' describe('Sessions Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ { version: 'v2', scopes: ['sessions:write'] }, { version: 'v3', scopes: ['sessions:write'] } @@ -32,7 +28,7 @@ describe('Sessions Tests for v2, v3', () => { status: 'ACTIVE', }, }); - salableVersions[version] = new Salable(value, version); + salableVersions[version] = initSalable(value, version); } }); @@ -58,7 +54,7 @@ describe('Sessions Tests for v2, v3', () => { const data = await salableVersions[version].sessions.create({ scope: SessionScope.Invoice, metadata: { - subscriptionUuid: subscriptionUuid, + subscriptionUuid: testUuids.subscriptionWithInvoicesUuid, }, }); expect(data).toEqual(sessionSchema); @@ -105,23 +101,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/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index 179304d1..c4865fc5 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -1,28 +1,25 @@ 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 { PaginatedSubscription, Invoice, Plan, PaginatedSubscriptionInvoice, PaginatedLicenses, Capability, License, SeatActionType } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; +import { initSalable } from '../../index'; +import { SubscriptionSchema } from '../../schemas/v3/schemas-v3'; +import { addMonths } from 'date-fns'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); const basicSubscriptionUuid = randomUUID(); const perSeatSubscriptionUuid = randomUUID(); -const licenseUuid = randomUUID(); -const licenseTwoUuid = randomUUID(); -const licenseThreeUuid = randomUUID(); const couponUuid = randomUUID(); -const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; -const testGrantee = '123456'; -const testEmail = 'tester@domain.com'; -const owner = 'subscription-owner'; +const testGrantee = randomUUID(); +const testEmail = randomUUID(); +const owner = randomUUID(); +const differentOwner = randomUUID(); +const subscriptionToBeCancelledUuid = 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(); @@ -41,7 +38,7 @@ 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 () => { @@ -61,13 +58,13 @@ 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, @@ -82,18 +79,19 @@ 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, @@ -106,16 +104,16 @@ 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 () => { @@ -135,14 +133,14 @@ 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).toEqual({ ...SubscriptionSchema, plan: planSchema }); expect(dataWithSearchParams).toHaveProperty('plan', planSchema); }); @@ -229,23 +227,23 @@ 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(testUuids.couponSubscriptionUuid, { 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(testUuids.couponSubscriptionUuid, { 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(perSeatSubscriptionUuid, { when: 'now' }); + const data = await salable.subscriptions.cancel(subscriptionToBeCancelledUuid, { when: 'now' }); expect(data).toBeUndefined(); }); @@ -320,29 +318,10 @@ const planSchema: Plan = { 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]), + data: expect.arrayContaining([SubscriptionSchema]), }; const invoiceSchema: Invoice = { @@ -494,150 +473,158 @@ const deleteTestData = async () => { }; 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({ + await prismaClient.subscription.create({ data: { - name: null, - email: null, + uuid: basicSubscriptionUuid, + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionV2Id, + lineItemIds: [stripeEnvs.subscriptionV2LineItemId], + email: testEmail, + owner, + type: 'salable', 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', + organisation: testUuids.organisationId, + 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: '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), }, - ], - 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'), + plan: { connect: { uuid: testUuids.paidPlanTwoUuid } }, + createdAt: new Date(), + updatedAt: new Date(), + expiryDate: new Date(Date.now() + 31536000000), }, }); + const differentOwnerSubscriptionUuid = randomUUID() await prismaClient.subscription.create({ data: { - uuid: basicSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionTwoId, - lineItemIds: [stripeEnvs.basicSubscriptionTwoLineItemId], + 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), }, }); - const differentOwnerSubscriptionUuid = randomUUID() await prismaClient.subscription.create({ data: { - uuid: differentOwnerSubscriptionUuid, - paymentIntegrationSubscriptionId: differentOwnerSubscriptionUuid, + paymentIntegrationSubscriptionId: randomUUID(), lineItemIds: [], email: testEmail, - owner: 'different-owner', + 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', + 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: new Date(Date.now() + 31536000000), + expiryDate: addMonths(new Date(), 1), }, }); @@ -653,7 +640,7 @@ const generateTestData = async () => { 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', @@ -679,8 +666,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, @@ -692,11 +678,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, @@ -710,17 +747,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/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index 48a1e11e..935745c1 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -1,10 +1,8 @@ import prismaClient from '../../../test-utils/prisma/prisma-client'; -import Salable from '../..'; import { PaginatedSubscription, Invoice, PaginatedSubscriptionInvoice, - Version, PaginatedLicenses, SeatActionType, } from '../../types'; @@ -18,6 +16,7 @@ import { PlanSchemaV3, SubscriptionSchema } from '../../schemas/v3/schemas-v3'; import { addMonths } from 'date-fns'; +import { initSalable } from '../../index'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); @@ -25,14 +24,14 @@ const basicSubscriptionUuid = randomUUID(); const perSeatSubscriptionUuid = randomUUID(); const couponUuid = randomUUID(); const perSeatBasicLicenseUuids = [randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID(), randomUUID()]; -const testGrantee = '123456'; -const testEmail = 'tester@domain.com'; -const owner = 'subscription-owner'; +const testGrantee = randomUUID(); +const testEmail = randomUUID(); +const owner = randomUUID(); +const differentOwner = randomUUID(); describe('Subscriptions V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; - const version = Version.V3; - const salable = new Salable(apiKey, version); + const salable = initSalable(apiKey, 'v3'); beforeAll(async () => { await generateTestData(); @@ -62,7 +61,7 @@ describe('Subscriptions V3 Tests', () => { const dataWithSearchParams = await salable.subscriptions.getAll({ status: 'ACTIVE', take: 3, - email: testEmail, + owner, expand: ['plan'], }); expect(dataWithSearchParams.first).toEqual(expect.any(String)) @@ -73,7 +72,7 @@ describe('Subscriptions V3 Tests', () => { { ...SubscriptionSchema, status: 'ACTIVE', - email: testEmail, + owner, plan: PlanSchemaV3 }, ]), @@ -85,13 +84,14 @@ describe('Subscriptions V3 Tests', () => { 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(2); + expect(dataWithSearchParams.data.length).toEqual(4); expect(dataWithSearchParams.data).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -105,7 +105,7 @@ describe('Subscriptions V3 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), @@ -113,7 +113,7 @@ describe('Subscriptions V3 Tests', () => { data: expect.arrayContaining([SubscriptionSchema]), }); 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 () => { @@ -217,12 +217,12 @@ describe('Subscriptions V3 Tests', () => { }); it('addCoupon: Should successfully add the specified coupon to the subscription', async () => { - const data = await salable.subscriptions.addCoupon(testUuids.couponSubscriptionUuid, { couponUuid }); + 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.couponSubscriptionUuid, { couponUuid }); + const data = await salable.subscriptions.removeCoupon(testUuids.couponSubscriptionUuidV3, { couponUuid }); expect(data).toBeUndefined(); }); @@ -428,6 +428,77 @@ const generateTestData = async () => { }, }); + const testSubscriptionUuid = randomUUID(); + await prismaClient.subscription.create({ + data: { + uuid: testSubscriptionUuid, + paymentIntegrationSubscriptionId: testSubscriptionUuid, + 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), + }, + }); + const differentOwnerSubscriptionUuid = randomUUID(); + 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, diff --git a/src/types.ts b/src/types.ts index edcd63e8..e6d2c225 100644 --- a/src/types.ts +++ b/src/types.ts @@ -378,14 +378,9 @@ export type PricingTable = { export type PricingTableV3 = { uuid: string; - name: string; status: ProductStatus; - title: string | null; - text: string | null; - theme: 'light' | 'dark' | string; featureOrder: string; productUuid: string; - customTheme: string; featuredPlanUuid: string; updatedAt: string; product: ProductV3 & { features: Feature[]; currencies: ProductCurrency[] }; @@ -458,6 +453,23 @@ export type GetPlanOptionsV3 = { }; export type GetPlanCheckoutOptions = { + successUrl: string; + cancelUrl: string; + granteeId: string; + member?: string; + owner?: string; + promoCode?: string; + allowPromoCode?: boolean; + customerEmail?: string; + customerId?: string; + currency?: string; + automaticTax?: string; + quantity?: string; + changeQuantity?: string; + requirePaymentMethod?: boolean; +}; + +export type GetPlanCheckoutOptionsV3 = { successUrl: string; cancelUrl: string; granteeId: string; diff --git a/src/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 24d5d5f4..14d73f1a 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,4 +1,4 @@ -import Salable, { TVersion } from '../..'; +import { initSalable, TVersion, VersionedMethods } from '../..'; import { PaginatedUsageRecords, UsageRecord } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; @@ -11,7 +11,7 @@ const testGrantee = 'userId_metered'; const owner = 'subscription-owner' describe('Usage Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ { version: 'v2', scopes: ['usage:read', 'usage:write'] }, { version: 'v3', scopes: ['usage:read', 'usage:write'] } @@ -29,7 +29,7 @@ describe('Usage Tests for v2, v3', () => { status: 'ACTIVE', }, }); - salableVersions[version] = new Salable(value, version); + salableVersions[version] = initSalable(value, version); } }); diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index 2097b308..f7b03b82 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -30,7 +30,8 @@ export type TestDbData = { usageBasicMonthlyPlanUuid: string; usageProMonthlyPlanUuid: string; subscriptionWithInvoicesUuid: string; - couponSubscriptionUuid: string; + couponSubscriptionUuidV2: string; + couponSubscriptionUuidV3: string; currencyUuids: { gbp: string; usd: string; @@ -62,7 +63,8 @@ export const testUuids: TestDbData = { usd: '6ec1a282-07b3-4716-bc3c-678c40b5d98e' }, subscriptionWithInvoicesUuid: 'b37357c6-bad1-4a6a-8c79-06935c66384f', - couponSubscriptionUuid: '893cd5cb-b313-4e8a-8e54-35781e7b0669' + couponSubscriptionUuidV2: '893cd5cb-b313-4e8a-8e54-35781e7b0669', + couponSubscriptionUuidV3: 'd5b45c18-2a84-49c5-a099-2b2422fd1b80' }; const features = [ @@ -159,8 +161,6 @@ const { publicKey, privateKey } = generateKeyPairSync('ec', { export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) { const loadingWheel = getConsoleLoader('CREATING TEST DATA'); - console.log('===== testUuids', testUuids) - const encryptedPrivateKey = await kmsSymmetricEncrypt(privateKey); await prismaClient.currency.create({ @@ -255,8 +255,6 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) }, }); - console.log('===== product', product) - const productTwo = await prismaClient.product.create({ data: { name: 'Sample Product Two', @@ -1013,8 +1011,8 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) organisation: testUuids.organisationId, type: 'salable', status: 'ACTIVE', - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionId, - lineItemIds: [stripeEnvs.basicSubscriptionLineItemId], + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithInvoicesId, + lineItemIds: [stripeEnvs.subscriptionWithInvoicesLineItemId], productUuid: testUuids.productUuid, planUuid: testUuids.paidPlanUuid, owner: 'xxxxx', @@ -1042,9 +1040,43 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) await prismaClient.subscription.create({ data: { - uuid: testUuids.couponSubscriptionUuid, - paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionThreeId, - lineItemIds: [stripeEnvs.basicSubscriptionThreeLineItemId], + 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), + }, + }); + + await prismaClient.subscription.create({ + data: { + uuid: testUuids.couponSubscriptionUuidV3, + paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithCouponV3Id, + lineItemIds: [stripeEnvs.subscriptionWithCouponV3LineItemId], email: 'customer@email.com', owner: 'xxxxx', type: 'salable', diff --git a/test-utils/scripts/create-stripe-test-data.ts b/test-utils/scripts/create-stripe-test-data.ts index 0fd645ec..41f392e9 100644 --- a/test-utils/scripts/create-stripe-test-data.ts +++ b/test-utils/scripts/create-stripe-test-data.ts @@ -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; @@ -264,20 +264,20 @@ 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, }; From e8d528883860f96f9669c72bef668190ab6df02f Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 3 Sep 2025 10:15:23 +0100 Subject: [PATCH 04/28] fix: removed clean up from tests --- src/licenses/v2/licenses-v2.test.ts | 9 -------- .../v2/pricing-table-v2.test.ts | 7 +++--- .../v3/pricing-table-v3.test.ts | 5 ++--- src/subscriptions/v2/subscriptions-v2.test.ts | 16 ++++---------- src/subscriptions/v3/subscriptions-v3.test.ts | 22 ++++++------------- .../scripts/create-salable-test-data.ts | 4 ++-- 6 files changed, 18 insertions(+), 45 deletions(-) diff --git a/src/licenses/v2/licenses-v2.test.ts b/src/licenses/v2/licenses-v2.test.ts index e4974b4d..29ef223b 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -28,10 +28,6 @@ describe('Licenses V2 Tests', () => { await generateTestData(); }); - afterAll(async () => { - await deleteTestData(); - }); - it('getOne: Should successfully fetch the specified license', async () => { const data = await salable.licenses.getOne(licenseUuid); @@ -309,11 +305,6 @@ const planSchema: Plan = { 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.subscription.create({ data: { diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 535b6663..6ef13779 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -11,7 +11,7 @@ describe('Pricing Table V2 Tests', () => { beforeAll(async() => { await generateTestData() - }) + }, 10000) it('getAll: should successfully fetch all products', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); @@ -40,11 +40,10 @@ const pricingTableSchema: PricingTable = { const generateTestData = async () => { - 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/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index 06c4a8cc..d73aa516 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -21,11 +21,10 @@ describe('Pricing Table V3 Tests', () => { const generateTestData = async () => { - 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/subscriptions/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index c4865fc5..bc371f2a 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -16,6 +16,8 @@ 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; @@ -25,10 +27,6 @@ describe('Subscriptions V2 Tests', () => { 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, @@ -466,12 +464,6 @@ const stripePaymentMethodSchema = { type: expect.any(String), }; -const deleteTestData = async () => { - await prismaClient.license.deleteMany({}); - await prismaClient.couponsOnSubscriptions.deleteMany({}); - await prismaClient.subscription.deleteMany({}); -}; - const generateTestData = async () => { await prismaClient.subscription.create({ data: { @@ -525,7 +517,6 @@ const generateTestData = async () => { }, }); - const differentOwnerSubscriptionUuid = randomUUID() await prismaClient.subscription.create({ data: { uuid: differentOwnerSubscriptionUuid, @@ -580,7 +571,8 @@ const generateTestData = async () => { await prismaClient.subscription.create({ data: { - paymentIntegrationSubscriptionId: randomUUID(), + uuid: subscriptionUuidTwo, + paymentIntegrationSubscriptionId: subscriptionUuidTwo, lineItemIds: [], email: testEmail, owner, diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index 935745c1..b88d851f 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -28,6 +28,10 @@ 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.devApiKeyV2; @@ -35,11 +39,7 @@ describe('Subscriptions V3 Tests', () => { beforeAll(async () => { await generateTestData(); - }); - - afterAll(async () => { - await deleteTestData(); - }); + }, 10000); it('create: Should successfully create a subscription without a payment integration', async () => { const data = await salable.subscriptions.create({ @@ -386,12 +386,6 @@ const stripePaymentMethodSchema = { type: expect.any(String), }; -const deleteTestData = async () => { - await prismaClient.license.deleteMany({}); - await prismaClient.couponsOnSubscriptions.deleteMany({}); - await prismaClient.subscription.deleteMany({}); -}; - const generateTestData = async () => { await prismaClient.subscription.create({ data: { @@ -428,11 +422,10 @@ const generateTestData = async () => { }, }); - const testSubscriptionUuid = randomUUID(); await prismaClient.subscription.create({ data: { - uuid: testSubscriptionUuid, - paymentIntegrationSubscriptionId: testSubscriptionUuid, + uuid: testSubscriptionTwoUuid, + paymentIntegrationSubscriptionId: testSubscriptionTwoUuid, lineItemIds: [], email: testEmail, owner, @@ -463,7 +456,6 @@ const generateTestData = async () => { expiryDate: addMonths(new Date(), 1), }, }); - const differentOwnerSubscriptionUuid = randomUUID(); await prismaClient.subscription.create({ data: { uuid: differentOwnerSubscriptionUuid, diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index f7b03b82..b382424d 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -1038,7 +1038,7 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) } }) - await prismaClient.subscription.create({ + const v2CouponSubscription = await prismaClient.subscription.create({ data: { uuid: testUuids.couponSubscriptionUuidV2, paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithCouponV2Id, @@ -1072,7 +1072,7 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) }, }); - await prismaClient.subscription.create({ + const v3CouponSubscription = await prismaClient.subscription.create({ data: { uuid: testUuids.couponSubscriptionUuidV3, paymentIntegrationSubscriptionId: stripeEnvs.subscriptionWithCouponV3Id, From 06eaab853d8994a5a3eea8ecf9c6863485a10f27 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 3 Sep 2025 10:27:41 +0100 Subject: [PATCH 05/28] refactor: removed init methods which are no longer needed --- src/events/index.ts | 15 ++------------- src/licenses/index.ts | 14 ++------------ src/plans/index.ts | 15 +-------------- src/pricing-tables/index.ts | 17 ++--------------- src/products/index.ts | 14 -------------- src/sessions/index.ts | 13 +------------ src/subscriptions/index.ts | 16 +--------------- src/usage/index.ts | 15 ++------------- 8 files changed, 11 insertions(+), 108 deletions(-) diff --git a/src/events/index.ts b/src/events/index.ts index e565e589..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]: { @@ -15,14 +14,4 @@ export type EventVersions = { [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: - case Version.V3: - 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/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/plans/index.ts b/src/plans/index.ts index 69b289e1..a7a79558 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -1,6 +1,4 @@ -import { Plan, PlanCheckout, PlanFeature, PlanCapability, PlanCurrency, ApiRequest, TVersion, Version, GetPlanOptions, GetPlanCheckoutOptions, PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3, GetPlanCheckoutOptionsV3, PlanV3 } from '../types'; -import { v2PlanMethods } from './v2'; -import { v3PlanMethods } from './v3'; +import { Plan, PlanCheckout, PlanFeature, PlanCapability, PlanCurrency, TVersion, Version, GetPlanOptions, GetPlanCheckoutOptions, PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3, GetPlanCheckoutOptionsV3, PlanV3 } from '../types'; export type PlanVersions = { [Version.V2]: { @@ -107,14 +105,3 @@ export type PlanVersions = { }; 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; - case Version.V3: - return v3PlanMethods(request) as PlanVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; diff --git a/src/pricing-tables/index.ts b/src/pricing-tables/index.ts index fb565a8b..1b7cc3ea 100644 --- a/src/pricing-tables/index.ts +++ b/src/pricing-tables/index.ts @@ -1,6 +1,4 @@ -import { PricingTable, ApiRequest, TVersion, Version, PricingTableV3 } from '../types'; -import { v2PricingTableMethods } from './v2'; -import { v3PricingTableMethods } from './v3'; +import { PricingTable, TVersion, Version, PricingTableV3 } from '../types'; export type PricingTableVersions = { [Version.V2]: { @@ -27,15 +25,4 @@ export type PricingTableVersions = { }; }; -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; - case Version.V3: - return v3PricingTableMethods(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/products/index.ts b/src/products/index.ts index 9c416524..12a3827b 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -5,13 +5,10 @@ import { ProductCurrency, ProductFeature, ProductPricingTable, - ApiRequest, TVersion, Version, ProductV3, ProductPricingTableV3 } from '../types'; -import { v2ProductMethods } from './v2'; -import { v3ProductMethods } from './v3'; export type ProductVersions = { [Version.V2]: { @@ -121,14 +118,3 @@ export type ProductVersions = { }; export type ProductVersionedMethods = V extends keyof ProductVersions ? ProductVersions[V] : never; - -export const productsInit = (version: V, request: ApiRequest): ProductVersionedMethods => { - switch (version) { - case Version.V2: - return v2ProductMethods(request) as ProductVersionedMethods; - case Version.V3: - return v3ProductMethods(request) as ProductVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; diff --git a/src/sessions/index.ts b/src/sessions/index.ts index 73a3b4f1..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]: { @@ -18,13 +17,3 @@ export type SessionVersions = { }; export type SessionVersionedMethods = V extends keyof SessionVersions ? SessionVersions[V] : never; - -export const sessionsInit = (version: V, request: ApiRequest): SessionVersionedMethods => { - switch (version) { - case Version.V2: - case Version.V3: - return v2SessionMethods(request) as SessionVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index 7e0b0ce7..8fdb3c83 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -6,7 +6,6 @@ import { SubscriptionPaymentMethod, SubscriptionPlan, SubscriptionSeat, - ApiRequest, TVersion, Version, GetAllSubscriptionsOptions, @@ -15,8 +14,6 @@ import { GetSubscriptionSeatsOptions, PaginatedSeats, GetSeatCountResponse, ManageSeatOptions, CreateSubscriptionInput } from '../types'; -import { v2SubscriptionMethods } from './v2'; -import { v3SubscriptionMethods } from './v3'; export type SubscriptionVersions = { [Version.V2]: { @@ -535,15 +532,4 @@ export type SubscriptionVersions = { }; }; -export type SubscriptionVersionedMethods = V extends keyof SubscriptionVersions ? SubscriptionVersions[V] : never; - -export const subscriptionsInit = (version: V, request: ApiRequest): SubscriptionVersionedMethods => { - switch (version) { - case Version.V2: - return v2SubscriptionMethods(request) as SubscriptionVersionedMethods; - case Version.V3: - return v3SubscriptionMethods(request) as SubscriptionVersionedMethods; - default: - throw new Error('Unsupported version'); - } -}; +export type SubscriptionVersionedMethods = V extends keyof SubscriptionVersions ? SubscriptionVersions[V] : never; \ No newline at end of file diff --git a/src/usage/index.ts b/src/usage/index.ts index 0b59e937..132b45de 100644 --- a/src/usage/index.ts +++ b/src/usage/index.ts @@ -1,4 +1,4 @@ -import { ApiRequest, TVersion, Version } from '..'; +import { TVersion, Version } from '..'; import { CurrentUsageOptions, CurrentUsageRecord, @@ -6,7 +6,6 @@ import { PaginatedUsageRecords, UpdateLicenseUsageOptions } from '../types'; -import { v2UsageMethods } from './v2'; export type UsageVersions = { [Version.V2]: { @@ -44,14 +43,4 @@ export type UsageVersions = { [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: - case Version.V3: - 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 From 887e8da7cd3b38909b2f7b14fda56f7ae69cf4f5 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 3 Sep 2025 13:40:52 +0100 Subject: [PATCH 06/28] feat: entitlements method --- src/entitlements/index.ts | 20 ++ src/entitlements/v3/entitlements-v3.test.ts | 202 ++++++++++++++++++ src/entitlements/v3/index.ts | 10 + src/index.ts | 115 +++++----- src/schemas/v2/schemas-v2.ts | 0 src/types.ts | 8 + .../scripts/create-salable-test-data.ts | 34 ++- 7 files changed, 335 insertions(+), 54 deletions(-) create mode 100644 src/entitlements/index.ts create mode 100644 src/entitlements/v3/entitlements-v3.test.ts create mode 100644 src/entitlements/v3/index.ts create mode 100644 src/schemas/v2/schemas-v2.ts diff --git a/src/entitlements/index.ts b/src/entitlements/index.ts new file mode 100644 index 00000000..e96249e2 --- /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 IDs of the grantee to be checked + * @param {string} productUuid - The ID 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..5a76d4ea --- /dev/null +++ b/src/entitlements/v3/index.ts @@ -0,0 +1,10 @@ +import { SALABLE_BASE_URL } from '../../constants'; +import { EntitlementVersions } from '..'; +import getUrl from '../../utils/get-url'; +import { ApiRequest } from '../../types'; + +const baseUrl = `${SALABLE_BASE_URL}/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/index.ts b/src/index.ts index 76bee6e0..71e243aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,8 @@ 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'; export { ErrorCodes, SalableParseError, SalableRequestError, SalableResponseError, SalableUnknownError, SalableValidationError } from './exceptions/salable-error'; export type { ResponseError, ValidationError } from './exceptions/salable-error'; @@ -67,62 +69,73 @@ export const initRequest: ApiFetch = } }; -type Methods = { - [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'> - }, - [Version.V3] : { - events: EventVersionedMethods<'v3'> - subscriptions: SubscriptionVersionedMethods<'v3'> - plans: PlanVersionedMethods<'v3'> - pricingTables: PricingTableVersionedMethods<'v3'> - products: ProductVersionedMethods<'v3'> - sessions: SessionVersionedMethods<'v3'> - usage: UsageVersionedMethods<'v3'> - } + +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'> } -export type VersionedMethods = V extends keyof Methods ? Methods[V] : never; -function versionedMethods(request: ApiRequest): Methods { - return { - 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), - }, - v3: { - events: v2EventMethods(request), - subscriptions: v3SubscriptionMethods(request), - plans: v3PlanMethods(request), - pricingTables: v3PricingTableMethods(request), - products: v3ProductMethods(request), - sessions: v2SessionMethods(request), - usage: v2UsageMethods(request), - }, - }; +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'> } -class SalableBase { - constructor(apiKey: string, version: V) { - const request = initRequest(apiKey, version); - const versioned = versionedMethods(request)[version]; - if (!versioned) throw new Error('Unknown Version ' + version); - return versioned; +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) + } as VersionedMethodsReturn; + default: + throw new Error('Unknown version ' + version); } } -export function initSalable(apiKey: string, version: V): SalableBase & VersionedMethods { - return new SalableBase(apiKey, version) as SalableBase & VersionedMethods; +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/schemas/v2/schemas-v2.ts b/src/schemas/v2/schemas-v2.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/types.ts b/src/types.ts index e6d2c225..9ef6ea3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -739,6 +739,14 @@ export type CheckLicensesCapabilitiesResponse = { signature: string; }; +export type EntitlementCheck = { + feature: { + feature: string; + expiry: Date; + }[]; + signature: string; +} + export type CapabilitiesEndDates = { [key: string]: string; }; diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index b382424d..9a0427a2 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -1,17 +1,17 @@ import prismaClient from '../../test-utils/prisma/prisma-client'; -import { generateKeyPairSync, randomUUID } from 'crypto'; +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 './create-stripe-test-data'; import { addMonths } from 'date-fns'; -import * as console from 'node:console'; config({ path: '.env.test' }); export type TestDbData = { organisationId: string; devApiKeyV2: string; + devApiKeyV3: string; productUuid: string; productTwoUuid: string; freeMonthlyPlanUuid: string; @@ -41,6 +41,7 @@ export type TestDbData = { export const testUuids: TestDbData = { 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', @@ -153,6 +154,23 @@ const apiKeyScopesV2 = [ 'usage:write', ]; +const apiKeyScopesV3 = [ + 'entitlements:check', + 'events:read', + 'billing:read', + 'billing:write', + 'organisations:read', + 'organisations:write', + 'subscriptions:read', + 'subscriptions:write', + 'pricing-tables:read', + 'plans:read', + 'products:read', + 'sessions:write', + 'usage:read', + 'usage:write', +]; + const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256', publicKeyEncoding: { type: 'spki', format: 'pem' }, @@ -200,7 +218,7 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) await prismaClient.apiKey.create({ data: { - name: 'Sample API Key', + name: 'Sample API Key V2', organisation: testUuids.organisationId, value: testUuids.devApiKeyV2, scopes: JSON.stringify(apiKeyScopesV2), @@ -208,6 +226,16 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) }, }); + 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', From 1b354ba060d7e5b470d9ef8963c1cad1a8adcbe6 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 3 Sep 2025 13:48:40 +0100 Subject: [PATCH 07/28] fix: fixed imports --- src/events/v2/events-v2.test.ts | 4 ++-- src/index.ts | 2 +- src/licenses/v2/licenses-v2.test.ts | 2 -- src/sessions/v2/sessions-v2.test.ts | 4 ++-- src/usage/v2/usage-v2.test.ts | 4 ++-- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index 96ab0758..0f5a3074 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,4 +1,4 @@ -import { initSalable, TVersion, Version, VersionedMethods } from '../..'; +import { initSalable, TVersion, Version, VersionedMethodsReturn } from '../..'; import { Event, EventTypeEnum } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; @@ -8,7 +8,7 @@ import { randomUUID } from 'crypto'; const eventUuid = randomUUID(); describe('Events Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ { version: Version.V2, scopes: ['events:read'] }, { version: Version.V3, scopes: ['events:read'] } diff --git a/src/index.ts b/src/index.ts index 71e243aa..1c8615ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,7 +94,7 @@ type MethodsV3 = { entitlements: EntitlementVersionedMethods<'v3'> } -type VersionedMethodsReturn = +export type VersionedMethodsReturn = V extends 'v2' ? MethodsV2 : V extends 'v3' ? MethodsV3 : never; diff --git a/src/licenses/v2/licenses-v2.test.ts b/src/licenses/v2/licenses-v2.test.ts index 29ef223b..7e083b35 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -22,8 +22,6 @@ const organisation = randomUUID(); describe('Licenses V2 Tests', () => { const salable = initSalable(testUuids.devApiKeyV2, 'v2'); - // TODO: add entitlements method for v3 - beforeAll(async () => { await generateTestData(); }); diff --git a/src/sessions/v2/sessions-v2.test.ts b/src/sessions/v2/sessions-v2.test.ts index 3339b4ee..356ece81 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,4 +1,4 @@ -import { initSalable, TVersion, VersionedMethods } from '../..'; +import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; import { Session, SessionScope } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import prismaClient from '../../../test-utils/prisma/prisma-client'; @@ -10,7 +10,7 @@ const licenseUuid = uuidv4(); const testGrantee = '123456'; describe('Sessions Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ { version: 'v2', scopes: ['sessions:write'] }, { version: 'v3', scopes: ['sessions:write'] } diff --git a/src/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 14d73f1a..64ceac94 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,4 +1,4 @@ -import { initSalable, TVersion, VersionedMethods } from '../..'; +import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; import { PaginatedUsageRecords, UsageRecord } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; @@ -11,7 +11,7 @@ const testGrantee = 'userId_metered'; const owner = 'subscription-owner' describe('Usage Tests for v2, v3', () => { - const salableVersions = {} as Record> + const salableVersions = {} as Record> const versions: {version: TVersion; scopes: string[]}[] = [ { version: 'v2', scopes: ['usage:read', 'usage:write'] }, { version: 'v3', scopes: ['usage:read', 'usage:write'] } From 3922be1fceea8ddcb9316103b3a19ef0cc8e2e11 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 3 Sep 2025 13:54:24 +0100 Subject: [PATCH 08/28] refactor: removed /v2/schemas-v2.ts --- src/schemas/v2/schemas-v2.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/schemas/v2/schemas-v2.ts diff --git a/src/schemas/v2/schemas-v2.ts b/src/schemas/v2/schemas-v2.ts deleted file mode 100644 index e69de29b..00000000 From b6ff867ab42105f81937f1c6725bfb27c23f00db Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 4 Sep 2025 09:31:26 +0100 Subject: [PATCH 09/28] fix: moved v2 schemas into a single file --- src/events/v2/events-v2.test.ts | 18 +- src/licenses/v2/licenses-v2.test.ts | 96 +--- src/plans/v2/plan-v2.test.ts | 78 +-- src/plans/v3/plan-v3.test.ts | 7 +- .../v2/pricing-table-v2.test.ts | 21 +- src/products/v2/product-v2.test.ts | 94 +--- src/products/v3/product-v3.test.ts | 4 +- src/schemas/v2/schemas-v2.ts | 452 ++++++++++++++++++ src/schemas/v3/schemas-v3.ts | 76 ++- src/sessions/v2/sessions-v2.test.ts | 20 +- src/subscriptions/v2/subscriptions-v2.test.ts | 245 +--------- src/subscriptions/v3/subscriptions-v3.test.ts | 176 +------ src/usage/v2/usage-v2.test.ts | 24 +- 13 files changed, 540 insertions(+), 771 deletions(-) create mode 100644 src/schemas/v2/schemas-v2.ts diff --git a/src/events/v2/events-v2.test.ts b/src/events/v2/events-v2.test.ts index 0f5a3074..8a239bb1 100644 --- a/src/events/v2/events-v2.test.ts +++ b/src/events/v2/events-v2.test.ts @@ -1,9 +1,8 @@ import { initSalable, TVersion, Version, VersionedMethodsReturn } from '../..'; -import { Event, EventTypeEnum } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; -import { EventStatus } from '@prisma/client'; import { randomUUID } from 'crypto'; +import { EventSchema } from '../../schemas/v2/schemas-v2'; const eventUuid = randomUUID(); @@ -31,23 +30,10 @@ describe('Events Tests for v2, v3', () => { }); it.each(versions)('getOne: Should successfully fetch the specified event', async ({ version }) => { const data = await salableVersions[version].events.getOne(eventUuid); - expect(data).toEqual(eventSchema); + 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/licenses/v2/licenses-v2.test.ts b/src/licenses/v2/licenses-v2.test.ts index 7e083b35..75798eb9 100644 --- a/src/licenses/v2/licenses-v2.test.ts +++ b/src/licenses/v2/licenses-v2.test.ts @@ -1,10 +1,10 @@ -import { Capability, License, PaginatedLicenses, Plan } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import getEndTime from '../../../test-utils/helpers/get-end-time'; 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(); @@ -29,21 +29,21 @@ describe('Licenses V2 Tests', () => { 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 () => { @@ -56,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) { @@ -90,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 () => { @@ -103,7 +103,7 @@ describe('Licenses V2 Tests', () => { expect(dataWithSearchParams).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, + ...LicenseSchema, status: 'ACTIVE', purchaser: testPurchaser, productUuid: testUuids.productUuid, @@ -115,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 () => { @@ -124,8 +124,8 @@ describe('Licenses V2 Tests', () => { expect(dataWithSearchParams).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, - plan: planSchema, + ...LicenseSchema, + plan: PlanSchema, }), ]), ); @@ -140,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 () => { @@ -162,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 () => { @@ -187,7 +187,7 @@ describe('Licenses V2 Tests', () => { expect(data).toEqual( expect.arrayContaining([ expect.objectContaining({ - ...licenseSchema, + ...LicenseSchema, granteeId: 'updated-grantee-id', }), ]), @@ -235,74 +235,6 @@ 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 generateTestData = async () => { await prismaClient.subscription.create({ data: { diff --git a/src/plans/v2/plan-v2.test.ts b/src/plans/v2/plan-v2.test.ts index 35b302f4..646ae831 100644 --- a/src/plans/v2/plan-v2.test.ts +++ b/src/plans/v2/plan-v2.test.ts @@ -1,6 +1,6 @@ -import { Plan, PlanCapability, PlanCheckout, PlanCurrency, PlanFeature } from '../../types'; 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; @@ -10,13 +10,13 @@ describe('Plans V2 Tests', () => { 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 () => { @@ -65,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/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index fbb19c94..6448ad01 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -9,6 +9,7 @@ import { ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; import { initSalable } from '../../index'; +import { PlanCheckoutLinkSchema } from '../../schemas/v2/schemas-v2'; describe('Plans V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; @@ -61,8 +62,4 @@ describe('Plans V3 Tests', () => { expect(data).toEqual(PlanCheckoutLinkSchema); }); -}); - -const PlanCheckoutLinkSchema: PlanCheckout = { - checkoutUrl: expect.any(String), -}; +}); \ 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 6ef13779..8d09d11b 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -1,8 +1,8 @@ import { initSalable } from '../..'; -import { PricingTable } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; +import { PricingTableSchema } from '../../schemas/v2/schemas-v2'; const pricingTableUuid = randomUUID(); describe('Pricing Table V2 Tests', () => { @@ -16,27 +16,10 @@ describe('Pricing Table V2 Tests', () => { it('getAll: should successfully fetch all products', 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 () => { diff --git a/src/products/v2/product-v2.test.ts b/src/products/v2/product-v2.test.ts index 9bd6f1cc..d0a43cdf 100644 --- a/src/products/v2/product-v2.test.ts +++ b/src/products/v2/product-v2.test.ts @@ -1,6 +1,6 @@ -import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, Version } from '../../types'; 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; @@ -73,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/product-v3.test.ts b/src/products/v3/product-v3.test.ts index 2b136160..dff23e95 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -1,16 +1,14 @@ import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { - EnumValueSchema, FeatureSchemaV3, PlanFeatureSchemaV3, OrganisationPaymentIntegrationSchemaV3, - PlanCurrencySchema, PlanSchemaV3, - ProductCurrencySchema, ProductPricingTableSchemaV3, ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; import { initSalable } from '../../index'; +import { EnumValueSchema, PlanCurrencySchema, ProductCurrencySchema } from '../../schemas/v2/schemas-v2'; describe('Products V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; 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 index ebfa1a05..fbfd0ed0 100644 --- a/src/schemas/v3/schemas-v3.ts +++ b/src/schemas/v3/schemas-v3.ts @@ -1,4 +1,6 @@ -import { EnumValue, FeatureV3, LicenseV3, OrganisationPaymentIntegrationV3, PlanCurrency, PlanFeatureV3, PlanV3, PricingTableV3, ProductCurrency, ProductPricingTableV3, ProductV3, Subscription } from '../../types'; +import { FeatureV3, Invoice, LicenseV3, OrganisationPaymentIntegrationV3, + PaginatedLicenses, PaginatedSubscription, PaginatedSubscriptionInvoice, PlanCurrency, PlanFeatureV3, PlanV3, PricingTableV3, ProductCurrency, ProductPricingTableV3, ProductV3, Subscription } from '../../types'; +import { EnumValueSchema, LicenseSchema, SubscriptionSchema } from '../v2/schemas-v2'; export const ProductSchemaV3: ProductV3 = { uuid: expect.any(String), @@ -54,13 +56,6 @@ export const FeatureSchemaV3: FeatureV3 = { visibility: 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 PlanFeatureSchemaV3: PlanFeatureV3 = { planUuid: expect.any(String), featureUuid: expect.any(String), @@ -70,7 +65,7 @@ export const PlanFeatureSchemaV3: PlanFeatureV3 = { updatedAt: expect.any(String), feature: FeatureSchemaV3, enumValue: EnumValueSchema, -} +}; export const ProductCurrencySchema: ProductCurrency = { productUuid: expect.any(String), @@ -81,7 +76,7 @@ export const ProductCurrencySchema: ProductCurrency = { shortName: expect.any(String), longName: expect.any(String), symbol: expect.any(String), - } + }, }; export const PlanCurrencySchema: PlanCurrency = { @@ -95,18 +90,20 @@ export const PlanCurrencySchema: PlanCurrency = { 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]) - }]), + plans: expect.arrayContaining([ + { + ...PlanSchemaV3, + features: expect.arrayContaining([PlanFeatureSchemaV3]), + currencies: expect.arrayContaining([PlanCurrencySchema]), + }, + ]), status: expect.any(String), updatedAt: expect.any(String), uuid: expect.any(String), @@ -122,7 +119,7 @@ export const OrganisationPaymentIntegrationSchemaV3: OrganisationPaymentIntegrat isTest: expect.any(Boolean), newPaymentEnabled: expect.any(Boolean), status: expect.any(String), -} +}; export const LicenseSchemaV3: LicenseV3 = { uuid: expect.any(String), @@ -140,23 +137,10 @@ export const LicenseSchemaV3: LicenseV3 = { isTest: expect.any(Boolean), }; -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 PaginatedLicensesSchemaV3: PaginatedLicenses = { + first: expect.toBeOneOf([expect.any(String), null]), + last: expect.toBeOneOf([expect.any(String), null]), + data: expect.arrayContaining([LicenseSchemaV3]), }; export const PricingTableSchemaV3: PricingTableV3 = { @@ -171,15 +155,17 @@ export const PricingTableSchemaV3: PricingTableV3 = { 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]), - } - }]), + 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/v2/sessions-v2.test.ts b/src/sessions/v2/sessions-v2.test.ts index 356ece81..8d6a9361 100644 --- a/src/sessions/v2/sessions-v2.test.ts +++ b/src/sessions/v2/sessions-v2.test.ts @@ -1,12 +1,12 @@ import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; -import { Session, SessionScope } from '../../types'; +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 licenseUuid = uuidv4(); +const licenseUuid = randomUUID(); const testGrantee = '123456'; describe('Sessions Tests for v2, v3', () => { @@ -39,7 +39,7 @@ describe('Sessions Tests for v2, v3', () => { productUuid: testUuids.productUuid, }, }); - expect(data).toEqual(sessionSchema); + expect(data).toEqual(SessionSchema); }); it.each(versions)('createSession: Should successfully create a new session with Checkout scope', async ({ version }) => { const data = await salableVersions[version].sessions.create({ @@ -48,7 +48,7 @@ describe('Sessions Tests for v2, v3', () => { planUuid: testUuids.paidPlanUuid, }, }); - expect(data).toEqual(sessionSchema); + expect(data).toEqual(SessionSchema); }); it.each(versions)('Should successfully create a new session with Invoice scope', async ({ version }) => { const data = await salableVersions[version].sessions.create({ @@ -57,14 +57,10 @@ describe('Sessions Tests for v2, v3', () => { 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: { @@ -83,7 +79,7 @@ const generateTestData = async () => { capabilities: [ { name: 'CapabilityOne', - uuid: uuidv4(), + uuid: randomUUID(), status: 'ACTIVE', updatedAt: '2022-10-17T11:41:11.626Z', description: null, @@ -91,7 +87,7 @@ const generateTestData = async () => { }, { name: 'CapabilityTwo', - uuid: uuidv4(), + uuid: randomUUID(), status: 'ACTIVE', updatedAt: '2022-10-17T11:41:11.626Z', description: null, diff --git a/src/subscriptions/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index bc371f2a..6ebbe14d 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -1,9 +1,9 @@ import prismaClient from '../../../test-utils/prisma/prisma-client'; -import { PaginatedSubscription, Invoice, Plan, PaginatedSubscriptionInvoice, PaginatedLicenses, Capability, License, SeatActionType } from '../../types'; +import { SeatActionType } from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; import { initSalable } from '../../index'; -import { SubscriptionSchema } from '../../schemas/v3/schemas-v3'; +import { PaginatedLicensesSchema, PaginationSubscriptionSchema, PlanSchema, StripeInvoiceSchema, StripePaymentMethodSchema, SubscriptionSchema } from '../../schemas/v2/schemas-v2'; import { addMonths } from 'date-fns'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); @@ -42,7 +42,7 @@ describe('Subscriptions V2 Tests', () => { 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 () => { @@ -56,7 +56,7 @@ 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( @@ -65,7 +65,7 @@ describe('Subscriptions V2 Tests', () => { ...SubscriptionSchema, status: 'ACTIVE', email: testEmail, - plan: planSchema, + plan: PlanSchema, }), ]), ); @@ -83,7 +83,7 @@ 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(2); expect(dataWithSearchParams.data).toEqual( @@ -92,7 +92,7 @@ describe('Subscriptions V2 Tests', () => { ...SubscriptionSchema, productUuid: testUuids.productUuid, plan: { - ...planSchema, + ...PlanSchema, uuid: testUuids.paidPlanTwoUuid, }, }), @@ -116,7 +116,7 @@ describe('Subscriptions V2 Tests', () => { 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 () => { @@ -138,26 +138,26 @@ describe('Subscriptions V2 Tests', () => { 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(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(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(testUuids.subscriptionWithInvoicesUuid); - expect(data).toEqual(expect.arrayContaining([planSchema])); + expect(data).toEqual(expect.arrayContaining([PlanSchema])); }); it('getUpdatePaymentLink: Should successfully fetch a subscriptions payment link', async () => { @@ -181,7 +181,7 @@ describe('Subscriptions V2 Tests', () => { it('getPaymentMethod: Should successfully fetch a subscriptions payment method', async () => { 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 () => { @@ -247,223 +247,6 @@ describe('Subscriptions 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), - features: expect.toBeOneOf([expect.anything(), undefined]), - currencies: expect.toBeOneOf([expect.anything(), undefined]), - archivedAt: expect.toBeOneOf([expect.any(String), null]), -}; - -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 generateTestData = async () => { await prismaClient.subscription.create({ data: { diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index b88d851f..9b6e91a3 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -1,22 +1,14 @@ import prismaClient from '../../../test-utils/prisma/prisma-client'; import { - PaginatedSubscription, - Invoice, - PaginatedSubscriptionInvoice, - PaginatedLicenses, 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, - LicenseSchemaV3, - PlanCurrencySchema, - PlanSchemaV3, SubscriptionSchema -} from '../../schemas/v3/schemas-v3'; +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 || ''); @@ -54,7 +46,7 @@ describe('Subscriptions V3 Tests', () => { 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 () => { @@ -118,7 +110,7 @@ describe('Subscriptions V3 Tests', () => { it("getSeats: Should successfully fetch a subscription's seats", async () => { const data = await salable.subscriptions.getSeats(perSeatSubscriptionUuid); - expect(data).toEqual(paginatedLicensesSchema); + expect(data).toEqual(PaginatedLicensesSchemaV3); }); it("getSeatCount: Should successfully fetch a subscription's seat count", async () => { @@ -150,12 +142,12 @@ describe('Subscriptions V3 Tests', () => { it('getInvoices: Should successfully fetch a subscriptions invoices', async () => { 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(testUuids.subscriptionWithInvoicesUuid, { take: 1 }); - expect(data).toEqual(stripeInvoiceSchema); + expect(data).toEqual(StripeInvoiceSchema); expect(data.data.length).toEqual(1); }); @@ -176,7 +168,7 @@ describe('Subscriptions V3 Tests', () => { it('getPaymentMethod: Should successfully fetch a subscriptions payment method', async () => { 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 () => { @@ -232,160 +224,6 @@ describe('Subscriptions V3 Tests', () => { }); }); -const paginatedLicensesSchema: PaginatedLicenses = { - first: expect.toBeOneOf([expect.any(String), null]), - last: expect.toBeOneOf([expect.any(String), null]), - data: expect.arrayContaining([LicenseSchemaV3]), -}; - -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 generateTestData = async () => { await prismaClient.subscription.create({ data: { diff --git a/src/usage/v2/usage-v2.test.ts b/src/usage/v2/usage-v2.test.ts index 64ceac94..4fa5594a 100644 --- a/src/usage/v2/usage-v2.test.ts +++ b/src/usage/v2/usage-v2.test.ts @@ -1,8 +1,8 @@ import { initSalable, TVersion, VersionedMethodsReturn } from '../..'; -import { PaginatedUsageRecords, UsageRecord } from '../../types'; import prismaClient from '../../../test-utils/prisma/prisma-client'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { randomUUID } from 'crypto'; +import { PaginatedUsageRecordsSchema, UsageRecordSchema } from '../../schemas/v2/schemas-v2'; const stripeEnvs = JSON.parse(process.env.stripEnvs || ''); const meteredLicenseUuid = randomUUID(); @@ -37,7 +37,7 @@ describe('Usage Tests for v2, v3', () => { const data = await salableVersions[version].usage.getAllUsageRecords({ granteeId: testGrantee, }); - expect(data).toEqual(paginatedUsageRecordsSchema); + expect(data).toEqual(PaginatedUsageRecordsSchema); }); it.each(versions)('getAllUsageRecords (w/ search params): Should successfully fetch the grantees usage records', async ({ version }) => { const data = await salableVersions[version].usage.getAllUsageRecords({ @@ -50,7 +50,7 @@ describe('Usage Tests for v2, v3', () => { last: expect.toBeOneOf([expect.any(String), null]), data: expect.arrayContaining([ { - ...usageRecordSchema, + ...UsageRecordSchema, type: 'recorded', }, ]), @@ -80,24 +80,6 @@ describe('Usage Tests for v2, v3', () => { }); }); -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: { From 06eae2d4d85662d0c987198a0403ed60999446c9 Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 4 Sep 2025 10:20:55 +0100 Subject: [PATCH 10/28] refactor: updated prisma schema with deprecated fields --- prisma/schema.prisma | 89 ++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fdaf7dfe..50d58ecb 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,32 @@ model Product { organisationPaymentIntegration OrganisationPaymentIntegration? @relation(fields: [organisationPaymentIntegrationUuid], references: [uuid], onUpdate: NoAction) organisationPaymentIntegrationUuid String @default("free") paymentIntegrationProductId String? - appType String @default("custom") + 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 +55,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 +99,6 @@ model Capability { model Feature { uuid String @id @default(uuid()) - name String description String? @db.Text displayName String variableName String? @default("") @@ -105,14 +107,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 +141,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 +169,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]) } @@ -237,8 +243,6 @@ 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 @@ -250,16 +254,19 @@ model License { 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 - features FeaturesOnLicenses[] /// @deprecated Use `usage` instead. usage LicensesUsageOnPlans[] usageRecords LicensesUsage[] isTest Boolean @default(false) - cancelAtPeriodEnd 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. + capabilities Json /// @deprecated + metadata Json? /// @deprecated @@index([status, paymentService]) @@index([productUuid]) @@ -369,13 +376,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 +442,7 @@ model LicensesUsageOnPlans { @@index([planUuid]) } +/// @deprecated model FeaturesOnLicenses { license License @relation(fields: [licenseUuid], references: [uuid], onUpdate: NoAction, onDelete: Cascade) licenseUuid String From a36548f1b6a4d6d0cbe471f358974b55f0ca3c6f Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 4 Sep 2025 11:22:20 +0100 Subject: [PATCH 11/28] refactor: debugging console logs --- docs/docs/entitlements/_category_.json | 8 ++ docs/docs/entitlements/check.md | 40 +++++++ docs/docs/events/get-one.md | 6 +- docs/docs/licenses/_category_.json | 8 -- docs/docs/licenses/cancel-many.md | 29 ----- docs/docs/licenses/cancel.md | 29 ----- docs/docs/licenses/check.md | 36 ------ docs/docs/licenses/create-many.md | 52 --------- docs/docs/licenses/create.md | 43 -------- docs/docs/licenses/get-all.md | 37 ------- docs/docs/licenses/get-count.md | 32 ------ docs/docs/licenses/get-for-granteeId.md | 39 ------- docs/docs/licenses/get-for-purchaser.md | 33 ------ docs/docs/licenses/get-one.md | 39 ------- docs/docs/licenses/update-many.md | 36 ------ docs/docs/licenses/update.md | 40 ------- docs/docs/overview.md | 8 +- docs/docs/plans/get-capabilities.md | 29 ----- docs/docs/plans/get-checkout-link.md | 17 ++- docs/docs/plans/get-currencies.md | 29 ----- docs/docs/plans/get-features.md | 29 ----- docs/docs/plans/get-one.md | 8 +- docs/docs/pricing-tables/get-one.md | 6 +- docs/docs/products/get-all.md | 2 +- docs/docs/products/get-capabilities.md | 29 ----- docs/docs/products/get-currencies.md | 29 ----- docs/docs/products/get-features.md | 29 ----- docs/docs/products/get-one.md | 6 +- docs/docs/products/get-plans.md | 29 ----- docs/docs/products/get-pricing-table.md | 2 +- docs/docs/sessions/create.md | 6 +- docs/docs/subscriptions/add-coupon.md | 4 +- docs/docs/subscriptions/add-seats.md | 38 ------- docs/docs/subscriptions/cancel.md | 4 +- docs/docs/subscriptions/change-plan.md | 6 +- docs/docs/subscriptions/create.md | 6 +- docs/docs/subscriptions/get-all.md | 6 +- .../get-cancel-subscription-link.md | 6 +- docs/docs/subscriptions/get-count.md | 6 +- .../subscriptions/get-customer-portal-link.md | 6 +- docs/docs/subscriptions/get-invoices.md | 6 +- docs/docs/subscriptions/get-one.md | 6 +- docs/docs/subscriptions/get-payment-link.md | 6 +- docs/docs/subscriptions/get-payment-method.md | 6 +- docs/docs/subscriptions/get-seats.md | 6 +- docs/docs/subscriptions/get-user-plans.md | 6 +- docs/docs/subscriptions/manage-seats.md | 6 +- docs/docs/subscriptions/reactivate.md | 4 +- docs/docs/subscriptions/remove-coupon.md | 4 +- docs/docs/subscriptions/remove-seats.md | 38 ------- docs/docs/subscriptions/update-seat-count.md | 76 +++++++++++++ docs/docs/subscriptions/update.md | 6 +- docs/docs/usage/get-record-for-plan.md | 6 +- docs/docs/usage/get-records.md | 6 +- docs/docs/usage/update.md | 4 +- prisma/schema.prisma | 104 +++++++++--------- src/plans/v3/plan-v3.test.ts | 6 +- .../v2/pricing-table-v2.test.ts | 4 +- .../v3/pricing-table-v3.test.ts | 3 +- src/products/v3/product-v3.test.ts | 1 + src/subscriptions/v3/subscriptions-v3.test.ts | 1 + 61 files changed, 272 insertions(+), 880 deletions(-) create mode 100644 docs/docs/entitlements/_category_.json create mode 100644 docs/docs/entitlements/check.md delete mode 100644 docs/docs/licenses/_category_.json delete mode 100644 docs/docs/licenses/cancel-many.md delete mode 100644 docs/docs/licenses/cancel.md delete mode 100644 docs/docs/licenses/check.md delete mode 100644 docs/docs/licenses/create-many.md delete mode 100644 docs/docs/licenses/create.md delete mode 100644 docs/docs/licenses/get-all.md delete mode 100644 docs/docs/licenses/get-count.md delete mode 100644 docs/docs/licenses/get-for-granteeId.md delete mode 100644 docs/docs/licenses/get-for-purchaser.md delete mode 100644 docs/docs/licenses/get-one.md delete mode 100644 docs/docs/licenses/update-many.md delete mode 100644 docs/docs/licenses/update.md delete mode 100644 docs/docs/plans/get-capabilities.md delete mode 100644 docs/docs/plans/get-currencies.md delete mode 100644 docs/docs/plans/get-features.md delete mode 100644 docs/docs/products/get-capabilities.md delete mode 100644 docs/docs/products/get-currencies.md delete mode 100644 docs/docs/products/get-features.md delete mode 100644 docs/docs/products/get-plans.md delete mode 100644 docs/docs/subscriptions/add-seats.md delete mode 100644 docs/docs/subscriptions/remove-seats.md create mode 100644 docs/docs/subscriptions/update-seat-count.md 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..f2408006 --- /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 { Salable } from '@salable/node-sdk'; + +const salable = new Salable('{{API_KEY}}'); + +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 [License Check Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseCheck) 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/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-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..83ea7ba4 100644 --- a/docs/docs/plans/get-checkout-link.md +++ b/docs/docs/plans/get-checkout-link.md @@ -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..91b3a3c8 100644 --- a/docs/docs/plans/get-one.md +++ b/docs/docs/plans/get-one.md @@ -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..2995baa5 100644 --- a/docs/docs/products/get-all.md +++ b/docs/docs/products/get-all.md @@ -18,4 +18,4 @@ 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..4c7ebb77 100644 --- a/docs/docs/products/get-pricing-table.md +++ b/docs/docs/products/get-pricing-table.md @@ -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 index 16b7271b..aaf9848b 100644 --- a/docs/docs/subscriptions/get-user-plans.md +++ b/docs/docs/subscriptions/get-user-plans.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.getSwitchablePlans('e0517f96-1ac0-4631-a52b-56ace9d1168c'); ``` @@ -26,4 +26,4 @@ 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) +For more information about this request see our API documentation on [Subscription Object](https://docs.salable.app/api/v3#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..65a6dce1 --- /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.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/v3#tag/Subscriptions/operation/incrementSubscriptionSeats) + +## 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.addSeats('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/incrementSubscriptionSeats) 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/prisma/schema.prisma b/prisma/schema.prisma index 50d58ecb..6e726c1a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,9 +32,9 @@ model Product { currencies CurrenciesOnProduct[] coupons Coupon[] - name String /// @deprecated - appType String @default("custom") /// @deprecated - capabilities Capability[] /// @deprecated + name String /// @deprecated + appType String @default("custom") /// @deprecated + capabilities Capability[] /// @deprecated @@index([organisation]) @@index([organisationPaymentIntegrationUuid]) @@ -70,17 +70,17 @@ model Plan { coupons CouponsOnPlans[] licensesUsage LicensesUsage[] - 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 + 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 @@ -115,8 +115,8 @@ model Feature { featureEnumOptions FeatureEnumOption[] pricingTables FeaturesOnPricingTables[] - name String /// @deprecated - licenses FeaturesOnLicenses[] /// @deprecated + name String /// @deprecated + licenses FeaturesOnLicenses[] /// @deprecated @@index([productUuid]) } @@ -143,10 +143,10 @@ model FeaturesOnPlans { isUnlimited Boolean @default(false) updatedAt DateTime @default(now()) @updatedAt - isUsage Boolean @default(false) /// @deprecated - pricePerUnit Float? /// @deprecated - minUsage Int? /// @deprecated - maxUsage Int? /// @deprecated + isUsage Boolean @default(false) /// @deprecated + pricePerUnit Float? /// @deprecated + minUsage Int? /// @deprecated + maxUsage Int? /// @deprecated @@id([planUuid, featureUuid]) @@index([planUuid]) @@ -179,11 +179,11 @@ model PricingTable { 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 + name String /// @deprecated + title String? /// @deprecated + text String? @db.Text /// @deprecated + theme String @default("light") /// @deprecated + customTheme Json? /// @deprecated @@index([productUuid]) } @@ -242,31 +242,31 @@ model Session { } model License { - 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. - capabilities Json /// @deprecated - metadata Json? /// @deprecated + 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. + capabilities Json /// @deprecated + metadata Json? /// @deprecated @@index([status, paymentService]) @@index([productUuid]) @@ -382,7 +382,7 @@ model OrganisationPaymentIntegration { isTest Boolean @default(false) newPaymentEnabled Boolean @default(false) - accountData Json /// @deprecated + accountData Json /// @deprecated @@index([accountId]) } @@ -777,4 +777,4 @@ model WebhookEventAttempt { scheduledAt DateTime? organisation String sentCount Int @default(0) -} \ No newline at end of file +} diff --git a/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index 6448ad01..70612d7a 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -1,6 +1,3 @@ -import { - PlanCheckout, -} from '../../types'; import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { PlanFeatureSchemaV3, @@ -10,15 +7,16 @@ import { } from '../../schemas/v3/schemas-v3'; import { initSalable } from '../../index'; import { PlanCheckoutLinkSchema } from '../../schemas/v2/schemas-v2'; +import * as console from 'node:console'; describe('Plans V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; const salable = initSalable(apiKey, 'v3'); - const planUuid = testUuids.paidPlanUuid; it('getOne: should successfully fetch one plan', async () => { const data = await salable.plans.getOne(planUuid); + console.log(data) expect(data).toEqual(PlanSchemaV3); }); diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 8d09d11b..8313dbeb 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -13,9 +13,9 @@ describe('Pricing Table V2 Tests', () => { await generateTestData() }, 10000) - it('getAll: should successfully fetch all products', async () => { + it('getAll: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); - + console.dir({depth: null, data}); expect(data).toEqual(expect.objectContaining(PricingTableSchema)); }); }); diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index d73aa516..cff7c3e1 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -12,8 +12,9 @@ describe('Pricing Table V3 Tests', () => { await generateTestData() }) - it('getOne: should successfully fetch all products', async () => { + it('getOne: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); + console.dir({depth: null, data}); expect(data).toEqual(expect.objectContaining(PricingTableSchemaV3)); }); }); diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index dff23e95..9747e938 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -17,6 +17,7 @@ describe('Products V3 Tests', () => { it('getAll: should successfully fetch all products', async () => { const data = await salable.products.getAll(); + console.log(data) expect(data).toEqual(expect.arrayContaining([ProductSchemaV3])); }); it('getOne: should successfully fetch a product', async () => { diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index 9b6e91a3..a7de08ba 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -56,6 +56,7 @@ describe('Subscriptions V3 Tests', () => { owner, expand: ['plan'], }); + console.log(dataWithSearchParams); expect(dataWithSearchParams.first).toEqual(expect.any(String)) expect(dataWithSearchParams.last).toEqual(expect.any(String)) expect(dataWithSearchParams.data.length).toEqual(3); From 261b92e6c85f9de708c107d4535d6962f7f3db43 Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 4 Sep 2025 11:24:26 +0100 Subject: [PATCH 12/28] refactor: debugging console logs --- src/pricing-tables/v2/pricing-table-v2.test.ts | 2 +- src/pricing-tables/v3/pricing-table-v3.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 8313dbeb..8eb62cde 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -15,7 +15,7 @@ describe('Pricing Table V2 Tests', () => { it('getAll: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); - console.dir({depth: null, data}); + console.dir(data, {depth: null}); expect(data).toEqual(expect.objectContaining(PricingTableSchema)); }); }); diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index cff7c3e1..8cafd40b 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -14,7 +14,7 @@ describe('Pricing Table V3 Tests', () => { it('getOne: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); - console.dir({depth: null, data}); + console.dir(data, {depth: null}); expect(data).toEqual(expect.objectContaining(PricingTableSchemaV3)); }); }); From c12f20d17e8d41b18f64ce3ae6456fd2da58aed8 Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 5 Sep 2025 09:22:16 +0100 Subject: [PATCH 13/28] docs: updated README.md --- README.md | 23 ++++++++++---------- docs/docs/subscriptions/update-seat-count.md | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) 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/docs/docs/subscriptions/update-seat-count.md b/docs/docs/subscriptions/update-seat-count.md index 65a6dce1..075d7ec0 100644 --- a/docs/docs/subscriptions/update-seat-count.md +++ b/docs/docs/subscriptions/update-seat-count.md @@ -16,7 +16,7 @@ import { initSalable } from '@salable/node-sdk'; const salable = initSalable('{{API_KEY}}', 'v3'); -await salable.subscriptions.addSeats('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { increment: 2 }); +await salable.subscriptions.updateSeatCount('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { increment: 2 }); ``` ### Parameters @@ -51,7 +51,7 @@ import { initSalable } from '@salable/node-sdk'; const salable = initSalable('{{API_KEY}}', 'v3'); -await salable.subscriptions.addSeats('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { decrement: 2 }); +await salable.subscriptions.updateSeatCount('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { decrement: 2 }); ``` ### Parameters From 3fcfdc737122f3041fba31cfb1312b45d5246082 Mon Sep 17 00:00:00 2001 From: Perry George Date: Mon, 8 Sep 2025 09:47:43 +0100 Subject: [PATCH 14/28] refactor: updated wrong docs links --- docs/docs/entitlements/check.md | 2 +- docs/docs/products/get-pricing-table.md | 6 ++-- docs/docs/subscriptions/get-user-plans.md | 29 ---------------- docs/docs/subscriptions/update-seat-count.md | 4 +-- src/plans/v3/plan-v3.test.ts | 2 +- src/pricing-tables/index.ts | 2 +- .../v3/pricing-table-v3.test.ts | 2 +- src/products/index.ts | 2 +- src/subscriptions/index.ts | 34 +++++++++---------- src/subscriptions/v3/subscriptions-v3.test.ts | 2 +- 10 files changed, 28 insertions(+), 57 deletions(-) delete mode 100644 docs/docs/subscriptions/get-user-plans.md diff --git a/docs/docs/entitlements/check.md b/docs/docs/entitlements/check.md index f2408006..b6b10aca 100644 --- a/docs/docs/entitlements/check.md +++ b/docs/docs/entitlements/check.md @@ -37,4 +37,4 @@ 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 [License Check Object](https://docs.salable.app/api/v2#tag/Licenses/operation/getLicenseCheck) +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/products/get-pricing-table.md b/docs/docs/products/get-pricing-table.md index 4c7ebb77..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', }); ``` diff --git a/docs/docs/subscriptions/get-user-plans.md b/docs/docs/subscriptions/get-user-plans.md deleted file mode 100644 index aaf9848b..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 { initSalable } from '@salable/node-sdk'; - -const salable = initSalable('{{API_KEY}}', 'v3'); - -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/v3#tag/Subscriptions/operation/getSubscriptionUpdatablePlans) diff --git a/docs/docs/subscriptions/update-seat-count.md b/docs/docs/subscriptions/update-seat-count.md index 075d7ec0..c46d9d24 100644 --- a/docs/docs/subscriptions/update-seat-count.md +++ b/docs/docs/subscriptions/update-seat-count.md @@ -38,7 +38,7 @@ _Type:_ `{ increment: number, proration?: string }` ### 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/incrementSubscriptionSeats) +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 @@ -73,4 +73,4 @@ _Type:_ `{ decrement: number, proration?: string }` ### 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/incrementSubscriptionSeats) +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/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index 70612d7a..2f18c8be 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -10,7 +10,7 @@ import { PlanCheckoutLinkSchema } from '../../schemas/v2/schemas-v2'; import * as console from 'node:console'; describe('Plans V3 Tests', () => { - const apiKey = testUuids.devApiKeyV2; + const apiKey = testUuids.devApiKeyV3; const salable = initSalable(apiKey, 'v3'); const planUuid = testUuids.paidPlanUuid; diff --git a/src/pricing-tables/index.ts b/src/pricing-tables/index.ts index 1b7cc3ea..5e007d84 100644 --- a/src/pricing-tables/index.ts +++ b/src/pricing-tables/index.ts @@ -17,7 +17,7 @@ export type PricingTableVersions = { * 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/v2#tag/Pricing-Tables/operation/getPricingTableByUuid + * @param {{ granteeId?: string; currency?: string;}} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Pricing-Tables/operation/getPricingTableByUuid * * @returns {Promise} */ diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index 8cafd40b..be440055 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -6,7 +6,7 @@ import { randomUUID } from 'crypto'; const pricingTableUuid = randomUUID(); describe('Pricing Table V3 Tests', () => { - const apiKey = testUuids.devApiKeyV2; + const apiKey = testUuids.devApiKeyV3; const salable = initSalable(apiKey, 'v3'); beforeAll(async() => { await generateTestData() diff --git a/src/products/index.ts b/src/products/index.ts index 12a3827b..a81519ed 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -89,7 +89,7 @@ export type ProductVersions = { /** * Retrieves a list of all products * - * Docs - https://docs.salable.app/api/v2#tag/Products/operation/getProducts + * Docs - https://docs.salable.app/api/v3#tag/Products/operation/getProducts * * @returns {Promise} All products present on the account */ diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index 8fdb3c83..bf361569 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -306,7 +306,7 @@ export type SubscriptionVersions = { * * @param {{ status?: SubscriptionStatus; email?: string; cursor?: string; take?: string; expand?: string[] }} options - Filter and pagination options * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptions + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptions * * @returns {Promise} The data of the subscription requested */ @@ -317,7 +317,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionByUuid + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionByUuid * * @returns {Promise} The data of the subscription requested */ @@ -336,7 +336,7 @@ export type SubscriptionVersions = { * @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/v2#tag/Subscriptions/operation/getSubscriptionsSeats + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionsSeats * * @returns {Promise} The seats of the subscription requested */ @@ -345,7 +345,7 @@ export type SubscriptionVersions = { /** * 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/v2#tag/Subscriptions/operation/getSubscriptionsSeatCount + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionsSeatCount * * @returns {Promise} */ @@ -358,7 +358,7 @@ export type SubscriptionVersions = { * @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/v2#tag/Subscriptions/operation/changeSubscriptionsPlan + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/changeSubscriptionsPlan * * @returns {Promise} */ @@ -372,7 +372,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/changeSubscriptionsPlan + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/changeSubscriptionsPlan * * @returns {Promise} */ @@ -389,7 +389,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionInvoices + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionInvoices * * @returns {Promise} */ @@ -400,7 +400,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/cancelSubscription + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/cancelSubscription * * @returns {Promise} */ @@ -416,7 +416,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionUpdatePaymentLink + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionUpdatePaymentLink * * @returns {Promise} */ @@ -427,7 +427,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionCustomerPortalLink + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCustomerPortalLink * * @returns {Promise} */ @@ -438,7 +438,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionCancelLink + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionCancelLink * * @returns {Promise} */ @@ -449,7 +449,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionPaymentMethod + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionPaymentMethod * * @returns {Promise} */ @@ -460,7 +460,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription to cancel * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/getSubscriptionReactivate + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/getSubscriptionReactivate * * @returns {Promise} */ @@ -471,7 +471,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/manageSubscriptionSeats + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/manageSubscriptionSeats * * @returns {Promise} */ @@ -485,7 +485,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/updateSubscriptionSeatCount + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/updateSubscriptionSeatCount * * @returns {Promise} */ @@ -503,7 +503,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/addCoupon + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/addCoupon * * @returns {Promise} */ @@ -519,7 +519,7 @@ export type SubscriptionVersions = { * * @param {string} subscriptionUuid - The UUID of the subscription * - * Docs - https://docs.salable.app/api/v2#tag/Subscriptions/operation/removeCoupon + * Docs - https://docs.salable.app/api/v3#tag/Subscriptions/operation/removeCoupon * * @returns {Promise} */ diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index a7de08ba..c3dc444c 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -26,7 +26,7 @@ const differentOwnerSubscriptionUuid = randomUUID(); describe('Subscriptions V3 Tests', () => { - const apiKey = testUuids.devApiKeyV2; + const apiKey = testUuids.devApiKeyV3; const salable = initSalable(apiKey, 'v3'); beforeAll(async () => { From 22c9a98ea0e68117e979a72d556a63f5cb761427 Mon Sep 17 00:00:00 2001 From: Perry George Date: Mon, 8 Sep 2025 10:20:00 +0100 Subject: [PATCH 15/28] test: added paid tests for per seat add/remove --- src/plans/v3/plan-v3.test.ts | 1 - src/products/v3/product-v3.test.ts | 1 - src/subscriptions/v2/subscriptions-v2.test.ts | 21 ++++++-- src/subscriptions/v3/subscriptions-v3.test.ts | 23 ++++++-- .../scripts/create-salable-test-data.ts | 54 +++++++++++++++---- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/plans/v3/plan-v3.test.ts b/src/plans/v3/plan-v3.test.ts index 2f18c8be..b4bb4c07 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -16,7 +16,6 @@ describe('Plans V3 Tests', () => { it('getOne: should successfully fetch one plan', async () => { const data = await salable.plans.getOne(planUuid); - console.log(data) expect(data).toEqual(PlanSchemaV3); }); diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index 9747e938..dff23e95 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -17,7 +17,6 @@ describe('Products V3 Tests', () => { it('getAll: should successfully fetch all products', async () => { const data = await salable.products.getAll(); - console.log(data) expect(data).toEqual(expect.arrayContaining([ProductSchemaV3])); }); it('getOne: should successfully fetch a product', async () => { diff --git a/src/subscriptions/v2/subscriptions-v2.test.ts b/src/subscriptions/v2/subscriptions-v2.test.ts index 6ebbe14d..98124c12 100644 --- a/src/subscriptions/v2/subscriptions-v2.test.ts +++ b/src/subscriptions/v2/subscriptions-v2.test.ts @@ -202,14 +202,22 @@ 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(); }); + 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 () => { const data = await salable.subscriptions.removeSeats(perSeatSubscriptionUuid, { decrement: 1, @@ -218,7 +226,14 @@ describe('Subscriptions V2 Tests', () => { expect(data).toBeUndefined(); }); - // TODO: paid per seat + 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 () => { const data = await salable.subscriptions.update(perSeatSubscriptionUuid, { diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index c3dc444c..b68537bf 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -56,7 +56,6 @@ describe('Subscriptions V3 Tests', () => { owner, expand: ['plan'], }); - console.log(dataWithSearchParams); expect(dataWithSearchParams.first).toEqual(expect.any(String)) expect(dataWithSearchParams.last).toEqual(expect.any(String)) expect(dataWithSearchParams.data.length).toEqual(3); @@ -188,20 +187,38 @@ describe('Subscriptions V3 Tests', () => { expect(data).toBeUndefined(); }); - it('updateSeatCount: Should successfully add seat to the subscription', async () => { + 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 remove seat from the subscription', async () => { + 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', diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index 9a0427a2..f557706a 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -30,6 +30,7 @@ export type TestDbData = { usageBasicMonthlyPlanUuid: string; usageProMonthlyPlanUuid: string; subscriptionWithInvoicesUuid: string; + perSeatSubscriptionUuid: string; couponSubscriptionUuidV2: string; couponSubscriptionUuidV3: string; currencyUuids: { @@ -64,6 +65,7 @@ export const testUuids: TestDbData = { 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' }; @@ -331,7 +333,7 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) 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', @@ -738,7 +740,7 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) description: 'Per Seat Unlimited Plan description', displayName: 'Per Seat Unlimited Plan', uuid: testUuids.perSeatUnlimitedPlanUuid, - product: { connect: { uuid: testUuids.productTwoUuid } }, + product: { connect: { uuid: testUuids.productUuid } }, status: 'ACTIVE', trialDays: 0, evaluation: false, @@ -753,24 +755,19 @@ export default async function createSalableTestData(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, })), }, }, @@ -1066,7 +1063,42 @@ export default async function createSalableTestData(stripeEnvs: StripeEnvsTypes) } }) - const v2CouponSubscription = await prismaClient.subscription.create({ + 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, From 37e335710ba5f4e21c472b3b1000bfa742642334 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 10 Sep 2025 14:46:01 +0100 Subject: [PATCH 16/28] feat: paginate features and plans endpoints --- __tests__/test-mock-data/api-key.ts | 37 +++ __tests__/test-mock-data/apigateway.ts | 73 +++++ __tests__/test-mock-data/capabilities.ts | 7 + __tests__/test-mock-data/coupons.ts | 17 ++ __tests__/test-mock-data/currencies.ts | 16 ++ __tests__/test-mock-data/data.ts | 30 +++ __tests__/test-mock-data/event.ts | 11 + __tests__/test-mock-data/features.ts | 23 ++ __tests__/test-mock-data/licenses.ts | 37 +++ __tests__/test-mock-data/middleware.ts | 48 ++++ __tests__/test-mock-data/mock-elements.ts | 24 ++ __tests__/test-mock-data/object-builder.ts | 32 +++ __tests__/test-mock-data/optional-type.ts | 1 + __tests__/test-mock-data/plans.ts | 27 ++ __tests__/test-mock-data/pricing-table.ts | 8 + __tests__/test-mock-data/products.ts | 29 ++ __tests__/test-mock-data/rbac.ts | 38 +++ __tests__/test-mock-data/response.ts | 11 + __tests__/test-mock-data/sessions.ts | 10 + __tests__/test-mock-data/subscriptions.ts | 19 ++ docs/docs/changelog.md | 29 +- docs/docs/features/_category_.json | 8 + docs/docs/features/get-all.md | 36 +++ docs/docs/plans/get-all.md | 35 +++ docs/docs/plans/get-checkout-link.md | 2 +- docs/docs/plans/get-one.md | 2 +- src/features/index.ts | 16 ++ src/features/v3/features-v3.test.ts | 175 ++++++++++++ src/features/v3/index.ts | 10 + src/index.ts | 6 +- src/plans/index.ts | 32 ++- src/plans/v3/index.ts | 1 + src/plans/v3/plan-v3.test.ts | 250 +++++++++++++++++- .../v2/pricing-table-v2.test.ts | 2 +- .../v3/pricing-table-v3.test.ts | 1 - src/products/index.ts | 5 +- src/products/v3/product-v3.test.ts | 17 +- src/subscriptions/v3/subscriptions-v3.test.ts | 2 +- src/types.ts | 37 +++ .../scripts/create-salable-test-data.ts | 2 + 40 files changed, 1129 insertions(+), 37 deletions(-) create mode 100644 __tests__/test-mock-data/api-key.ts create mode 100644 __tests__/test-mock-data/apigateway.ts create mode 100644 __tests__/test-mock-data/capabilities.ts create mode 100644 __tests__/test-mock-data/coupons.ts create mode 100644 __tests__/test-mock-data/currencies.ts create mode 100644 __tests__/test-mock-data/data.ts create mode 100644 __tests__/test-mock-data/event.ts create mode 100644 __tests__/test-mock-data/features.ts create mode 100644 __tests__/test-mock-data/licenses.ts create mode 100644 __tests__/test-mock-data/middleware.ts create mode 100644 __tests__/test-mock-data/mock-elements.ts create mode 100644 __tests__/test-mock-data/object-builder.ts create mode 100644 __tests__/test-mock-data/optional-type.ts create mode 100644 __tests__/test-mock-data/plans.ts create mode 100644 __tests__/test-mock-data/pricing-table.ts create mode 100644 __tests__/test-mock-data/products.ts create mode 100644 __tests__/test-mock-data/rbac.ts create mode 100644 __tests__/test-mock-data/response.ts create mode 100644 __tests__/test-mock-data/sessions.ts create mode 100644 __tests__/test-mock-data/subscriptions.ts create mode 100644 docs/docs/features/_category_.json create mode 100644 docs/docs/features/get-all.md create mode 100644 docs/docs/plans/get-all.md create mode 100644 src/features/index.ts create mode 100644 src/features/v3/features-v3.test.ts create mode 100644 src/features/v3/index.ts diff --git a/__tests__/test-mock-data/api-key.ts b/__tests__/test-mock-data/api-key.ts new file mode 100644 index 00000000..4681968b --- /dev/null +++ b/__tests__/test-mock-data/api-key.ts @@ -0,0 +1,37 @@ +import { ApiKeyType } from '@prisma/client'; +import objectBuilder from './object-builder'; + +export const mockApiKey = objectBuilder({ + name: 'Sample API Key 1', + description: 'This is a sample API key for testing purposes', + status: 'ACTIVE', + value: process.env.SEED_API_KEY ?? 'xxxxx', + scopes: JSON.stringify([ + 'read:api-keys', + 'write:api-keys', + 'read:licenses', + 'write:licenses', + 'cancel:licenses', + 'read:products', + 'write:products', + 'read:pricing-tables', + 'write:pricing-tables', + 'read:plans', + 'write:plans', + 'read:capabilities', + 'write:capabilities', + 'read:features', + 'write:features', + 'read:paddle', + 'write:paddle', + 'read:organisations', + 'write:organisations', + 'read:usage', + 'write:usage', + ]), + organisation: 'xxxxx', + isTest: false, + type: ApiKeyType.RESTRICTED, + expiresAt: null as null | Date, + lastUsedAt: null as null | Date, +}); diff --git a/__tests__/test-mock-data/apigateway.ts b/__tests__/test-mock-data/apigateway.ts new file mode 100644 index 00000000..ccfa5953 --- /dev/null +++ b/__tests__/test-mock-data/apigateway.ts @@ -0,0 +1,73 @@ +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import objectBuilder from './object-builder'; + +export const mockApiGatewayContext = objectBuilder({ + awsRequestId: '', + callbackWaitsForEmptyEventLoop: false, + functionName: '', + functionVersion: '', + invokedFunctionArn: '', + logGroupName: '', + logStreamName: '', + memoryLimitInMB: '', + fail(): void { + return; + }, + done(): void { + return; + }, + succeed(): void { + return; + }, + getRemainingTimeInMillis(): number { + return 0; + }, +}); + +export const mockApiGatewayIdentity = objectBuilder({ + accessKey: null, + accountId: null, + apiKey: null, + apiKeyId: null, + caller: null, + clientCert: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: '', + user: null, + userAgent: null, + userArn: null, +}); + +export const mockApiGatewayRequestEventContext = objectBuilder({ + accountId: '', + apiId: '', + authorizer: undefined, + protocol: '', + httpMethod: '', + identity: mockApiGatewayIdentity(), + path: '', + stage: '', + requestId: '', + requestTimeEpoch: 0, + resourceId: '', + resourcePath: '', +}); + +export const mockApiGatewayEvent = objectBuilder({ + body: '', + headers: {}, + httpMethod: '', + isBase64Encoded: false, + multiValueHeaders: {}, + multiValueQueryStringParameters: null, + path: '', + pathParameters: null, + queryStringParameters: null, + requestContext: mockApiGatewayRequestEventContext(), + resource: '', + stageVariables: null, +}); diff --git a/__tests__/test-mock-data/capabilities.ts b/__tests__/test-mock-data/capabilities.ts new file mode 100644 index 00000000..b0a96d1e --- /dev/null +++ b/__tests__/test-mock-data/capabilities.ts @@ -0,0 +1,7 @@ +import objectBuilder from './object-builder'; + +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/data.ts b/__tests__/test-mock-data/data.ts new file mode 100644 index 00000000..4cb1be32 --- /dev/null +++ b/__tests__/test-mock-data/data.ts @@ -0,0 +1,30 @@ +const organisation = 'org_12345'; + +export const productData = { + organisation, + name: 'Product 1', + displayName: 'Product', + description: null, + logoUrl: null, + status: 'ACTIVE', + paid: false, + updatedAt: new Date(), +}; + +export const planData = { + name: 'example-plan', + organisation, + displayName: 'Example Plan', + visibility: 'public', + status: 'ACTIVE', + environment: 'test', + active: true, + planType: 'basic', + currencies: {}, + interval: 'year', + length: 3, + evaluation: false, + pricingType: 'free', + evalDays: 10, + licenseType: 'test', +}; 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..ad308034 --- /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, + pricePerUnit: 10, + minUsage: 1, + maxUsage: 100, +}); diff --git a/__tests__/test-mock-data/licenses.ts b/__tests__/test-mock-data/licenses.ts new file mode 100644 index 00000000..2555aa78 --- /dev/null +++ b/__tests__/test-mock-data/licenses.ts @@ -0,0 +1,37 @@ +import { LicensesUsageRecordType, Prisma } from '@prisma/client'; +import objectBuilder from './object-builder'; + +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[], + startTime: undefined as undefined | Date, + endTime: new Date(), + cancelAtPeriodEnd: false, + isTest: false, +}); diff --git a/__tests__/test-mock-data/middleware.ts b/__tests__/test-mock-data/middleware.ts new file mode 100644 index 00000000..d3c6048f --- /dev/null +++ b/__tests__/test-mock-data/middleware.ts @@ -0,0 +1,48 @@ +import objectBuilder from './object-builder'; +import { + IMiddlewareProxyContext, + IMiddlewareProxyEvent, +} from 'utilities/interfaces/middleware/middleware.interface'; +import { mockApiGatewayRequestEventContext } from './apigateway'; + +export const mockContext = objectBuilder({ + awsRequestId: '', + callbackWaitsForEmptyEventLoop: false, + functionName: '', + functionVersion: '', + invokedFunctionArn: '', + logGroupName: '', + logStreamName: '', + memoryLimitInMB: '', + errors: [], + fail(): void { + return; + }, + done(): void { + return; + }, + succeed(): void { + return; + }, + getRemainingTimeInMillis(): number { + return 0; + }, +}); + +export const mockEvent = objectBuilder({ + Records: [], + body: '', + data: {} as Record, + headers: {}, + httpMethod: '', + isBase64Encoded: false, + multiValueHeaders: {}, + multiValueQueryStringParameters: null, + path: '', + pathParameters: null, + queryStringParameters: null, + requestContext: mockApiGatewayRequestEventContext(), + resource: '', + stageVariables: null, + source: '', +}); diff --git a/__tests__/test-mock-data/mock-elements.ts b/__tests__/test-mock-data/mock-elements.ts new file mode 100644 index 00000000..730e8f77 --- /dev/null +++ b/__tests__/test-mock-data/mock-elements.ts @@ -0,0 +1,24 @@ +import prismaClient from '../prisma/prisma-client'; +import { planData, productData } from './data'; + +const mockElements = async () => { + const mockProduct = await prismaClient.product.create({ + data: productData, + }); + + const mockPlan = await prismaClient.plan.create({ + data: { + productUuid: mockProduct.uuid, + ...planData, + capabilities: {}, + features: {}, + }, + }); + + return { + mockProduct, + mockPlan, + }; +}; + +export default mockElements; 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..10aef1ec --- /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', + description: 'Free Plan description', + displayName: 'Free Plan Display Name', + slug: 'example-slug', + status: 'ACTIVE', + trialDays: 0, + evaluation: false, + evalDays: 0, + organisation: 'xxxxx', + visibility: 'public', + licenseType: 'licensed', + interval: 'month', + perSeatAmount: 1, + maxSeatAmount: -1, + length: 1, + active: true, + planType: 'Standard', + pricingType: 'free', + environment: 'dev', + paddlePlanId: null, // Note: 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..167925a8 --- /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', + isTest: false, + archivedAt: null as null | Date, +}); + +export const mockOrganisationPaymentIntegration = objectBuilder({ + organisation: 'xxxxx', + integrationName: 'stripe_existing' as PaymentIntegration, + accountData: { + key: 'xxxxx', + encryptedData: 'xoxox', + }, + isTest: false, + accountName: 'Account Name', + accountId: 'acc_1234', + status: PaymentIntegrationStatus.active, +}); diff --git a/__tests__/test-mock-data/rbac.ts b/__tests__/test-mock-data/rbac.ts new file mode 100644 index 00000000..2fa4f791 --- /dev/null +++ b/__tests__/test-mock-data/rbac.ts @@ -0,0 +1,38 @@ +import objectBuilder from './object-builder'; +import { randomUUID } from 'crypto'; + +export const mockRbacPermission = objectBuilder({ + value: 'xxxxx', + type: 'xxxxx', + description: 'xxxxx', + dependencies: ['xxxxx'], + organisation: undefined as string | undefined, +}); + +export const mockRbacRole = objectBuilder({ + name: 'xxxxx', + description: 'xxxxx', + permissions: ['xxxxx'], + organisation: undefined as string | undefined, +}); + +export const mockRbacRoleUpdate = objectBuilder({ + name: undefined as string | undefined, + description: undefined as string | undefined, + permissions: { add: [] as string[], remove: [] as string[] }, +}); + +export const mockRbacUser = objectBuilder({ + id: randomUUID(), + name: 'xxxxx', + role: 'xxxxx', + permissions: ['xxxxx'], + organisation: undefined as string | undefined, +}); + +export const mockRbacUserUpdate = objectBuilder({ + id: undefined as string | undefined, + name: undefined as string | undefined, + role: undefined as string | undefined, + permissions: { add: [] as string[], remove: [] as string[] }, +}); diff --git a/__tests__/test-mock-data/response.ts b/__tests__/test-mock-data/response.ts new file mode 100644 index 00000000..e870ef53 --- /dev/null +++ b/__tests__/test-mock-data/response.ts @@ -0,0 +1,11 @@ +import objectBuilder from './object-builder'; +import { IMiddlewareProxyResult } from 'utilities/interfaces/middleware/middleware.interface'; + +export const mockResponse = objectBuilder({ + statusCode: 200, +}); + +export const mockMiddlewareResponse = objectBuilder({ + headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, + statusCode: 200, +}); 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/docs/docs/changelog.md b/docs/docs/changelog.md index 2829c464..b51d649a 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -4,6 +4,33 @@ sidebar_position: 2 # Changelog +## v5.0.0 + +### 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 capabilities deprecated +- `plans.capabilities` +- `product.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. +- `license.check` moved to `entitlements.check` +- `licenses.getAll` moved to `subscriptions.getSeats` +- `licenses.getOne` support removed +- `licenses.getForPurchaser` moved to `subscriptions.getAll` with the owner filter applied. +- `licenses.update` moved to `subscriptions.update` +- `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. + ## v4.0.0 ### Breaking Changes @@ -17,7 +44,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/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..c64ad5b1 --- /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 the features belong to | ✅ | +| 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/plans/get-all.md b/docs/docs/plans/get-all.md new file mode 100644 index 00000000..d69a0423 --- /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 (_required_) + +_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-checkout-link.md b/docs/docs/plans/get-checkout-link.md index 83ea7ba4..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 diff --git a/docs/docs/plans/get-one.md b/docs/docs/plans/get-one.md index 91b3a3c8..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 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..3138504b --- /dev/null +++ b/src/features/v3/index.ts @@ -0,0 +1,10 @@ +import { SALABLE_BASE_URL } from '../../constants'; +import getUrl from '../../utils/get-url'; +import { ApiRequest } from '../../types'; +import { FeatureVersions } from '../index'; + +const baseUrl = `${SALABLE_BASE_URL}/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 1c8615ad..f5d2409d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,8 @@ 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'; @@ -92,6 +94,7 @@ type MethodsV3 = { sessions: SessionVersionedMethods<'v3'> usage: UsageVersionedMethods<'v3'> entitlements: EntitlementVersionedMethods<'v3'> + features: FeatureVersionedMethods<'v3'> } export type VersionedMethodsReturn = @@ -126,7 +129,8 @@ function versionedMethods( products: v3ProductMethods(request), sessions: v2SessionMethods(request), usage: v2UsageMethods(request), - entitlements: v3EntitlementMethods(request) + entitlements: v3EntitlementMethods(request), + features: v3FeatureMethods(request), } as VersionedMethodsReturn; default: throw new Error('Unknown version ' + version); diff --git a/src/plans/index.ts b/src/plans/index.ts index a7a79558..0dac5f7d 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -1,4 +1,18 @@ -import { Plan, PlanCheckout, PlanFeature, PlanCapability, PlanCurrency, TVersion, Version, GetPlanOptions, GetPlanCheckoutOptions, PlanFeatureV3, ProductV3, OrganisationPaymentIntegrationV3, GetPlanOptionsV3, GetPlanCheckoutOptionsV3, PlanV3 } from '../types'; +import { + Plan, + PlanCheckout, + PlanFeature, + PlanCapability, + PlanCurrency, + TVersion, + Version, + GetPlanOptions, + GetPlanCheckoutOptions, + GetPlanOptionsV3, + GetPlanCheckoutOptionsV3, + GetAllPlansOptionsV3, + GetAllPlansV3, +} from '../types'; export type PlanVersions = { [Version.V2]: { @@ -66,10 +80,16 @@ export type PlanVersions = { }; [Version.V3]: { /** - * Retrieves information about a plan 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. + * Get all plans * - * @param {string} planUuid - The UUID of the plan + * @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 Promise; + ) => Promise; /** diff --git a/src/plans/v3/index.ts b/src/plans/v3/index.ts index 2bd7623b..a34cd557 100644 --- a/src/plans/v3/index.ts +++ b/src/plans/v3/index.ts @@ -6,6 +6,7 @@ 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 index b4bb4c07..8d797b78 100644 --- a/src/plans/v3/plan-v3.test.ts +++ b/src/plans/v3/plan-v3.test.ts @@ -5,15 +5,205 @@ import { PlanSchemaV3, ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; -import { initSalable } from '../../index'; +import { initSalable, VersionedMethodsReturn } from '../../index'; import { PlanCheckoutLinkSchema } from '../../schemas/v2/schemas-v2'; -import * as console from 'node:console'; +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); @@ -59,4 +249,58 @@ describe('Plans V3 Tests', () => { expect(data).toEqual(PlanCheckoutLinkSchema); }); -}); \ No newline at end of file +}); + +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/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 8eb62cde..3926e724 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -11,7 +11,7 @@ describe('Pricing Table V2 Tests', () => { beforeAll(async() => { await generateTestData() - }, 10000) + }) it('getAll: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index be440055..8dd9a0b6 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -14,7 +14,6 @@ describe('Pricing Table V3 Tests', () => { it('getOne: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid, {owner: 'xxxxx'}); - console.dir(data, {depth: null}); expect(data).toEqual(expect.objectContaining(PricingTableSchemaV3)); }); }); diff --git a/src/products/index.ts b/src/products/index.ts index a81519ed..7766e2bc 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -103,17 +103,16 @@ export type ProductVersions = { * * @returns {Promise} */ - getOne: (productUuid: string, options?: { expand: ('features' | 'currencies' | 'organisationPaymentIntegration' | 'plans')[] }) => Promise; + getOne: (productUuid: string, options?: { expand: ('organisationPaymentIntegration')[] }) => Promise; /** * 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 {{ owner: string; currency?: string }} options - (Optional) Filter parameters. See https://docs.salable.app/api/v3#tag/Products/operation/getProductPricingTable * * @returns {Promise} */ - getPricingTable: (productUuid: string, options: { owner: string; currency?: 'GBP' | 'USD' | 'EUR' | 'CAD' }) => Promise; + getPricingTable: (productUuid: string, options: { owner: string }) => Promise; }; }; diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index dff23e95..8d3b6f71 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -1,14 +1,10 @@ import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; import { - FeatureSchemaV3, - PlanFeatureSchemaV3, OrganisationPaymentIntegrationSchemaV3, - PlanSchemaV3, ProductPricingTableSchemaV3, ProductSchemaV3 } from '../../schemas/v3/schemas-v3'; import { initSalable } from '../../index'; -import { EnumValueSchema, PlanCurrencySchema, ProductCurrencySchema } from '../../schemas/v2/schemas-v2'; describe('Products V3 Tests', () => { const apiKey = testUuids.devApiKeyV2; @@ -26,20 +22,10 @@ describe('Products V3 Tests', () => { it('getOne (w / search params): should successfully fetch a product', async () => { const data = await salable.products.getOne(productUuid, { - expand: ['features', 'plans', 'currencies', 'organisationPaymentIntegration'], + expand: ['organisationPaymentIntegration'], }); expect(data).toEqual({ ...ProductSchemaV3, - currencies: expect.arrayContaining([ProductCurrencySchema]), - features: expect.arrayContaining([{ - ...FeatureSchemaV3, - featureEnumOptions: expect.arrayContaining([EnumValueSchema]) - }]), - plans: expect.arrayContaining([{ - ...PlanSchemaV3, - features: expect.arrayContaining([PlanFeatureSchemaV3]), - currencies: expect.arrayContaining([PlanCurrencySchema]) - }]), organisationPaymentIntegration: OrganisationPaymentIntegrationSchemaV3 }); }); @@ -52,7 +38,6 @@ describe('Products V3 Tests', () => { it('getPricingTable (w / search params): should successfully fetch a product pricing table', async () => { const data = await salable.products.getPricingTable(productUuid, { owner: 'xxxxx', - currency: 'GBP', }); expect(data).toEqual(ProductPricingTableSchemaV3); }); diff --git a/src/subscriptions/v3/subscriptions-v3.test.ts b/src/subscriptions/v3/subscriptions-v3.test.ts index b68537bf..f55b5a29 100644 --- a/src/subscriptions/v3/subscriptions-v3.test.ts +++ b/src/subscriptions/v3/subscriptions-v3.test.ts @@ -31,7 +31,7 @@ describe('Subscriptions V3 Tests', () => { beforeAll(async () => { await generateTestData(); - }, 10000); + }); it('create: Should successfully create a subscription without a payment integration', async () => { const data = await salable.subscriptions.create({ diff --git a/src/types.ts b/src/types.ts index 9ef6ea3d..d3dffa43 100644 --- a/src/types.ts +++ b/src/types.ts @@ -311,6 +311,28 @@ export type PlanV3 = { 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; @@ -522,6 +544,21 @@ export type FeatureV3 = { 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; diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index f557706a..7504f0a2 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -159,6 +159,8 @@ const apiKeyScopesV2 = [ const apiKeyScopesV3 = [ 'entitlements:check', 'events:read', + 'features:read', + 'currencies:read', 'billing:read', 'billing:write', 'organisations:read', From 7e9af6f81721705882336e1b91226beed26f4c07 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 17 Sep 2025 15:38:08 +0100 Subject: [PATCH 17/28] feat: cursor pagination on products endpoint --- src/products/index.ts | 18 +-- src/products/v3/index.ts | 2 +- src/products/v3/product-v3.test.ts | 178 ++++++++++++++++++++++++++++- src/types.ts | 15 +++ 4 files changed, 192 insertions(+), 21 deletions(-) diff --git a/src/products/index.ts b/src/products/index.ts index 7766e2bc..f473a2bc 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -1,14 +1,4 @@ -import { - Plan, - Product, - ProductCapability, - ProductCurrency, - ProductFeature, - ProductPricingTable, - TVersion, - Version, - ProductV3, ProductPricingTableV3 -} from '../types'; +import { Plan, Product, ProductCapability, ProductCurrency, ProductFeature, ProductPricingTable, TVersion, Version, ProductV3, ProductPricingTableV3, GetAllProductsV3, GetAllProductsOptionsV3 } from '../types'; export type ProductVersions = { [Version.V2]: { @@ -87,13 +77,13 @@ export type ProductVersions = { }; [Version.V3]: { /** - * Retrieves a list of all products + * Retrieves a paginated list of all products * * Docs - https://docs.salable.app/api/v3#tag/Products/operation/getProducts * - * @returns {Promise} All products present on the account + * @returns {Promise} All products present on the account */ - getAll: () => Promise; + getAll: (options?: GetAllProductsOptionsV3) => 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. diff --git a/src/products/v3/index.ts b/src/products/v3/index.ts index 2ed5d75d..6b2ef975 100644 --- a/src/products/v3/index.ts +++ b/src/products/v3/index.ts @@ -6,7 +6,7 @@ import getUrl from '../../utils/get-url'; const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.PRODUCTS}`; export const v3ProductMethods = (request: ApiRequest): ProductVersions['v3'] => ({ - getAll: () => request(baseUrl, { method: 'GET' }), + 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 index 8d3b6f71..1778f24b 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -1,16 +1,161 @@ import { testUuids } from '../../../test-utils/scripts/create-salable-test-data'; -import { - OrganisationPaymentIntegrationSchemaV3, - ProductPricingTableSchemaV3, - ProductSchemaV3 -} from '../../schemas/v3/schemas-v3'; -import { initSalable } from '../../index'; +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 plans', 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 plans 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 plans 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 plans 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', + productUuid: data.productThree.uuid, + archivedAt: expect.any(String) as string, + }, + ], + } + ); + }) + }) + it('getAll: should successfully fetch all products', async () => { const data = await salable.products.getAll(); expect(data).toEqual(expect.arrayContaining([ProductSchemaV3])); @@ -42,3 +187,24 @@ describe('Products V3 Tests', () => { 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/types.ts b/src/types.ts index d3dffa43..346ac6e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -636,6 +636,21 @@ export type ProductV3 = { 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; From 2b143a030bd9f87e0c93dba8ceeb60f661a474f1 Mon Sep 17 00:00:00 2001 From: Perry George Date: Wed, 17 Sep 2025 16:04:10 +0100 Subject: [PATCH 18/28] test: updated feature schema to include featureEnumOptions BREAKING CHANGE: methods will use v3 of the API. New initSalable function to replace Salable class. --- .../v2/pricing-table-v2.test.ts | 1 - src/products/v3/product-v3.test.ts | 5 ---- src/schemas/v3/schemas-v3.ts | 25 ++++++++++++++++--- .../scripts/create-salable-test-data.ts | 1 + 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 3926e724..7efee32a 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -15,7 +15,6 @@ describe('Pricing Table V2 Tests', () => { it('getAll: should successfully fetch all pricing tables', async () => { const data = await salable.pricingTables.getOne(pricingTableUuid); - console.dir(data, {depth: null}); expect(data).toEqual(expect.objectContaining(PricingTableSchema)); }); }); diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index 1778f24b..1243bcc8 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -147,7 +147,6 @@ describe('Products V3 Tests', () => { ...ProductSchemaV3, uuid: data.productThree.uuid, slug: 'c-slug', - productUuid: data.productThree.uuid, archivedAt: expect.any(String) as string, }, ], @@ -156,10 +155,6 @@ describe('Products V3 Tests', () => { }) }) - it('getAll: should successfully fetch all products', async () => { - const data = await salable.products.getAll(); - expect(data).toEqual(expect.arrayContaining([ProductSchemaV3])); - }); it('getOne: should successfully fetch a product', async () => { const data = await salable.products.getOne(productUuid); expect(data).toEqual(ProductSchemaV3); diff --git a/src/schemas/v3/schemas-v3.ts b/src/schemas/v3/schemas-v3.ts index fbfd0ed0..1a9d6438 100644 --- a/src/schemas/v3/schemas-v3.ts +++ b/src/schemas/v3/schemas-v3.ts @@ -1,5 +1,21 @@ -import { FeatureV3, Invoice, LicenseV3, OrganisationPaymentIntegrationV3, - PaginatedLicenses, PaginatedSubscription, PaginatedSubscriptionInvoice, PlanCurrency, PlanFeatureV3, PlanV3, PricingTableV3, ProductCurrency, ProductPricingTableV3, ProductV3, Subscription } from '../../types'; +import { + FeatureEnumOption, + FeatureV3, + Invoice, + LicenseV3, + OrganisationPaymentIntegrationV3, + PaginatedLicenses, + PaginatedSubscription, + PaginatedSubscriptionInvoice, + PlanCurrency, + PlanFeatureV3, + PlanV3, + PricingTableV3, + ProductCurrency, + ProductPricingTableV3, + ProductV3, + Subscription, +} from '../../types'; import { EnumValueSchema, LicenseSchema, SubscriptionSchema } from '../v2/schemas-v2'; export const ProductSchemaV3: ProductV3 = { @@ -41,7 +57,9 @@ export const PlanSchemaV3: PlanV3 = { isSubscribed: expect.toBeOneOf([expect.any(Boolean), undefined]), }; -export const FeatureSchemaV3: FeatureV3 = { +export const FeatureSchemaV3: FeatureV3 & { + featureEnumOptions: FeatureEnumOption[] +} = { defaultValue: expect.any(String), description: expect.toBeOneOf([expect.any(String), null]), displayName: expect.any(String), @@ -54,6 +72,7 @@ export const FeatureSchemaV3: FeatureV3 = { valueType: expect.any(String), variableName: expect.any(String), visibility: expect.any(String), + featureEnumOptions: expect.arrayContaining([EnumValueSchema]) as FeatureEnumOption[] }; export const PlanFeatureSchemaV3: PlanFeatureV3 = { diff --git a/test-utils/scripts/create-salable-test-data.ts b/test-utils/scripts/create-salable-test-data.ts index 7504f0a2..835bb356 100644 --- a/test-utils/scripts/create-salable-test-data.ts +++ b/test-utils/scripts/create-salable-test-data.ts @@ -169,6 +169,7 @@ const apiKeyScopesV3 = [ 'subscriptions:write', 'pricing-tables:read', 'plans:read', + 'features:read', 'products:read', 'sessions:write', 'usage:read', From 93ae5812cac534f5d35be394aab709abce0caa73 Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 18 Sep 2025 09:53:16 +0100 Subject: [PATCH 19/28] test: removed unused mock data files --- __tests__/test-mock-data/api-key.ts | 37 ------------ __tests__/test-mock-data/apigateway.ts | 73 ----------------------- __tests__/test-mock-data/capabilities.ts | 1 + __tests__/test-mock-data/data.ts | 30 ---------- __tests__/test-mock-data/features.ts | 8 +-- __tests__/test-mock-data/licenses.ts | 3 +- __tests__/test-mock-data/middleware.ts | 48 --------------- __tests__/test-mock-data/mock-elements.ts | 24 -------- __tests__/test-mock-data/plans.ts | 14 ++--- __tests__/test-mock-data/products.ts | 4 +- __tests__/test-mock-data/rbac.ts | 38 ------------ __tests__/test-mock-data/response.ts | 11 ---- 12 files changed, 16 insertions(+), 275 deletions(-) delete mode 100644 __tests__/test-mock-data/api-key.ts delete mode 100644 __tests__/test-mock-data/apigateway.ts delete mode 100644 __tests__/test-mock-data/data.ts delete mode 100644 __tests__/test-mock-data/middleware.ts delete mode 100644 __tests__/test-mock-data/mock-elements.ts delete mode 100644 __tests__/test-mock-data/rbac.ts delete mode 100644 __tests__/test-mock-data/response.ts diff --git a/__tests__/test-mock-data/api-key.ts b/__tests__/test-mock-data/api-key.ts deleted file mode 100644 index 4681968b..00000000 --- a/__tests__/test-mock-data/api-key.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ApiKeyType } from '@prisma/client'; -import objectBuilder from './object-builder'; - -export const mockApiKey = objectBuilder({ - name: 'Sample API Key 1', - description: 'This is a sample API key for testing purposes', - status: 'ACTIVE', - value: process.env.SEED_API_KEY ?? 'xxxxx', - scopes: JSON.stringify([ - 'read:api-keys', - 'write:api-keys', - 'read:licenses', - 'write:licenses', - 'cancel:licenses', - 'read:products', - 'write:products', - 'read:pricing-tables', - 'write:pricing-tables', - 'read:plans', - 'write:plans', - 'read:capabilities', - 'write:capabilities', - 'read:features', - 'write:features', - 'read:paddle', - 'write:paddle', - 'read:organisations', - 'write:organisations', - 'read:usage', - 'write:usage', - ]), - organisation: 'xxxxx', - isTest: false, - type: ApiKeyType.RESTRICTED, - expiresAt: null as null | Date, - lastUsedAt: null as null | Date, -}); diff --git a/__tests__/test-mock-data/apigateway.ts b/__tests__/test-mock-data/apigateway.ts deleted file mode 100644 index ccfa5953..00000000 --- a/__tests__/test-mock-data/apigateway.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { APIGatewayProxyEvent, Context } from 'aws-lambda'; -import objectBuilder from './object-builder'; - -export const mockApiGatewayContext = objectBuilder({ - awsRequestId: '', - callbackWaitsForEmptyEventLoop: false, - functionName: '', - functionVersion: '', - invokedFunctionArn: '', - logGroupName: '', - logStreamName: '', - memoryLimitInMB: '', - fail(): void { - return; - }, - done(): void { - return; - }, - succeed(): void { - return; - }, - getRemainingTimeInMillis(): number { - return 0; - }, -}); - -export const mockApiGatewayIdentity = objectBuilder({ - accessKey: null, - accountId: null, - apiKey: null, - apiKeyId: null, - caller: null, - clientCert: null, - cognitoAuthenticationProvider: null, - cognitoAuthenticationType: null, - cognitoIdentityId: null, - cognitoIdentityPoolId: null, - principalOrgId: null, - sourceIp: '', - user: null, - userAgent: null, - userArn: null, -}); - -export const mockApiGatewayRequestEventContext = objectBuilder({ - accountId: '', - apiId: '', - authorizer: undefined, - protocol: '', - httpMethod: '', - identity: mockApiGatewayIdentity(), - path: '', - stage: '', - requestId: '', - requestTimeEpoch: 0, - resourceId: '', - resourcePath: '', -}); - -export const mockApiGatewayEvent = objectBuilder({ - body: '', - headers: {}, - httpMethod: '', - isBase64Encoded: false, - multiValueHeaders: {}, - multiValueQueryStringParameters: null, - path: '', - pathParameters: null, - queryStringParameters: null, - requestContext: mockApiGatewayRequestEventContext(), - resource: '', - stageVariables: null, -}); diff --git a/__tests__/test-mock-data/capabilities.ts b/__tests__/test-mock-data/capabilities.ts index b0a96d1e..3e9c39ec 100644 --- a/__tests__/test-mock-data/capabilities.ts +++ b/__tests__/test-mock-data/capabilities.ts @@ -1,5 +1,6 @@ import objectBuilder from './object-builder'; +// deprecated export const mockCapability = objectBuilder({ name: 'test_capability', status: 'ACTIVE', diff --git a/__tests__/test-mock-data/data.ts b/__tests__/test-mock-data/data.ts deleted file mode 100644 index 4cb1be32..00000000 --- a/__tests__/test-mock-data/data.ts +++ /dev/null @@ -1,30 +0,0 @@ -const organisation = 'org_12345'; - -export const productData = { - organisation, - name: 'Product 1', - displayName: 'Product', - description: null, - logoUrl: null, - status: 'ACTIVE', - paid: false, - updatedAt: new Date(), -}; - -export const planData = { - name: 'example-plan', - organisation, - displayName: 'Example Plan', - visibility: 'public', - status: 'ACTIVE', - environment: 'test', - active: true, - planType: 'basic', - currencies: {}, - interval: 'year', - length: 3, - evaluation: false, - pricingType: 'free', - evalDays: 10, - licenseType: 'test', -}; diff --git a/__tests__/test-mock-data/features.ts b/__tests__/test-mock-data/features.ts index ad308034..f811bd25 100644 --- a/__tests__/test-mock-data/features.ts +++ b/__tests__/test-mock-data/features.ts @@ -16,8 +16,8 @@ export const mockFeature = objectBuilder({ export const mockPlanFeature = objectBuilder({ value: 'xxxxx', isUnlimited: undefined as boolean | undefined, - isUsage: undefined as boolean | undefined, - pricePerUnit: 10, - minUsage: 1, - maxUsage: 100, + 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 index 2555aa78..e5549057 100644 --- a/__tests__/test-mock-data/licenses.ts +++ b/__tests__/test-mock-data/licenses.ts @@ -1,6 +1,7 @@ import { LicensesUsageRecordType, Prisma } from '@prisma/client'; import objectBuilder from './object-builder'; +// deprecated export const mockLicenseCapability = objectBuilder({ name: 'Export', uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285', @@ -29,7 +30,7 @@ export const mockLicense = objectBuilder({ capabilities: [ mockLicenseCapability({ name: 'CapabilityOne' }), mockLicenseCapability({ name: 'CapabilityTwo' }), - ] as Prisma.InputJsonObject[], + ] as Prisma.InputJsonObject[], // deprecated startTime: undefined as undefined | Date, endTime: new Date(), cancelAtPeriodEnd: false, diff --git a/__tests__/test-mock-data/middleware.ts b/__tests__/test-mock-data/middleware.ts deleted file mode 100644 index d3c6048f..00000000 --- a/__tests__/test-mock-data/middleware.ts +++ /dev/null @@ -1,48 +0,0 @@ -import objectBuilder from './object-builder'; -import { - IMiddlewareProxyContext, - IMiddlewareProxyEvent, -} from 'utilities/interfaces/middleware/middleware.interface'; -import { mockApiGatewayRequestEventContext } from './apigateway'; - -export const mockContext = objectBuilder({ - awsRequestId: '', - callbackWaitsForEmptyEventLoop: false, - functionName: '', - functionVersion: '', - invokedFunctionArn: '', - logGroupName: '', - logStreamName: '', - memoryLimitInMB: '', - errors: [], - fail(): void { - return; - }, - done(): void { - return; - }, - succeed(): void { - return; - }, - getRemainingTimeInMillis(): number { - return 0; - }, -}); - -export const mockEvent = objectBuilder({ - Records: [], - body: '', - data: {} as Record, - headers: {}, - httpMethod: '', - isBase64Encoded: false, - multiValueHeaders: {}, - multiValueQueryStringParameters: null, - path: '', - pathParameters: null, - queryStringParameters: null, - requestContext: mockApiGatewayRequestEventContext(), - resource: '', - stageVariables: null, - source: '', -}); diff --git a/__tests__/test-mock-data/mock-elements.ts b/__tests__/test-mock-data/mock-elements.ts deleted file mode 100644 index 730e8f77..00000000 --- a/__tests__/test-mock-data/mock-elements.ts +++ /dev/null @@ -1,24 +0,0 @@ -import prismaClient from '../prisma/prisma-client'; -import { planData, productData } from './data'; - -const mockElements = async () => { - const mockProduct = await prismaClient.product.create({ - data: productData, - }); - - const mockPlan = await prismaClient.plan.create({ - data: { - productUuid: mockProduct.uuid, - ...planData, - capabilities: {}, - features: {}, - }, - }); - - return { - mockProduct, - mockPlan, - }; -}; - -export default mockElements; diff --git a/__tests__/test-mock-data/plans.ts b/__tests__/test-mock-data/plans.ts index 10aef1ec..88799a31 100644 --- a/__tests__/test-mock-data/plans.ts +++ b/__tests__/test-mock-data/plans.ts @@ -1,13 +1,13 @@ import objectBuilder from './object-builder'; export const mockPlan = objectBuilder({ - name: 'Free Plan Name', + name: 'Free Plan Name', // deprecated description: 'Free Plan description', displayName: 'Free Plan Display Name', slug: 'example-slug', status: 'ACTIVE', - trialDays: 0, - evaluation: false, + trialDays: 0, // deprecated + evaluation: false, // deprecated evalDays: 0, organisation: 'xxxxx', visibility: 'public', @@ -16,11 +16,11 @@ export const mockPlan = objectBuilder({ perSeatAmount: 1, maxSeatAmount: -1, length: 1, - active: true, - planType: 'Standard', + active: true, // deprecated + planType: 'Standard', // deprecated pricingType: 'free', - environment: 'dev', - paddlePlanId: null, // Note: deprecated + environment: 'dev', // deprecated + paddlePlanId: null, // deprecated isTest: false, hasAcceptedTransaction: false, archivedAt: null as Date | null, diff --git a/__tests__/test-mock-data/products.ts b/__tests__/test-mock-data/products.ts index 167925a8..6f0e3d03 100644 --- a/__tests__/test-mock-data/products.ts +++ b/__tests__/test-mock-data/products.ts @@ -10,7 +10,7 @@ export const mockProduct = objectBuilder({ slug: 'example-slug', status: 'ACTIVE', paid: false, - appType: 'CUSTOM', + appType: 'CUSTOM', // deprecated isTest: false, archivedAt: null as null | Date, }); @@ -18,7 +18,7 @@ export const mockProduct = objectBuilder({ export const mockOrganisationPaymentIntegration = objectBuilder({ organisation: 'xxxxx', integrationName: 'stripe_existing' as PaymentIntegration, - accountData: { + accountData: { // deprecated key: 'xxxxx', encryptedData: 'xoxox', }, diff --git a/__tests__/test-mock-data/rbac.ts b/__tests__/test-mock-data/rbac.ts deleted file mode 100644 index 2fa4f791..00000000 --- a/__tests__/test-mock-data/rbac.ts +++ /dev/null @@ -1,38 +0,0 @@ -import objectBuilder from './object-builder'; -import { randomUUID } from 'crypto'; - -export const mockRbacPermission = objectBuilder({ - value: 'xxxxx', - type: 'xxxxx', - description: 'xxxxx', - dependencies: ['xxxxx'], - organisation: undefined as string | undefined, -}); - -export const mockRbacRole = objectBuilder({ - name: 'xxxxx', - description: 'xxxxx', - permissions: ['xxxxx'], - organisation: undefined as string | undefined, -}); - -export const mockRbacRoleUpdate = objectBuilder({ - name: undefined as string | undefined, - description: undefined as string | undefined, - permissions: { add: [] as string[], remove: [] as string[] }, -}); - -export const mockRbacUser = objectBuilder({ - id: randomUUID(), - name: 'xxxxx', - role: 'xxxxx', - permissions: ['xxxxx'], - organisation: undefined as string | undefined, -}); - -export const mockRbacUserUpdate = objectBuilder({ - id: undefined as string | undefined, - name: undefined as string | undefined, - role: undefined as string | undefined, - permissions: { add: [] as string[], remove: [] as string[] }, -}); diff --git a/__tests__/test-mock-data/response.ts b/__tests__/test-mock-data/response.ts deleted file mode 100644 index e870ef53..00000000 --- a/__tests__/test-mock-data/response.ts +++ /dev/null @@ -1,11 +0,0 @@ -import objectBuilder from './object-builder'; -import { IMiddlewareProxyResult } from 'utilities/interfaces/middleware/middleware.interface'; - -export const mockResponse = objectBuilder({ - statusCode: 200, -}); - -export const mockMiddlewareResponse = objectBuilder({ - headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, - statusCode: 200, -}); From d74d2fe4dd692eb80267671b85aebc4ac342c1cc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Sep 2025 09:08:38 +0000 Subject: [PATCH 20/28] chore(release): 5.0.0-beta.1 [skip ci] # [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. --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf21a23..804d1991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# [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/package.json b/package.json index 03f902a5..016184cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salable/node-sdk", - "version": "4.11.0", + "version": "5.0.0-beta.1", "description": "Node.js SDK to interact with Salable APIs", "main": "dist/index.js", "module": "dist/index.es.js", From 273b14dcb3269fe4ad987499804065f2253e0051 Mon Sep 17 00:00:00 2001 From: Perry George Date: Thu, 18 Sep 2025 10:23:45 +0100 Subject: [PATCH 21/28] fix: updated EntitlementsCheck return type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 346ac6e8..2d6804ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -792,7 +792,7 @@ export type CheckLicensesCapabilitiesResponse = { }; export type EntitlementCheck = { - feature: { + features: { feature: string; expiry: Date; }[]; From 7126730f66947d519660263b234efc9176c76704 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Sep 2025 09:39:25 +0000 Subject: [PATCH 22/28] chore(release): 5.0.0-beta.2 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 804d1991..c42c7adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) diff --git a/package.json b/package.json index 016184cd..5455d227 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salable/node-sdk", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "description": "Node.js SDK to interact with Salable APIs", "main": "dist/index.js", "module": "dist/index.es.js", From 9a88a47204e42389bd5775d8551136fdd12621a1 Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 08:18:10 +0100 Subject: [PATCH 23/28] docs: updated changelog for v5.0.0 --- docs/docs/changelog.md | 62 ++++++++++++++++++++++++++++++++---- src/schemas/v3/schemas-v3.ts | 6 +--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index b51d649a..c5a14deb 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -8,29 +8,79 @@ sidebar_position: 2 ### Breaking Changes -### Capabilities deprecated +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') // 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 capabilities deprecated + +##### Deprecated methods that use capabilities - `plans.capabilities` -- `product.capabilities` +- `products.capabilities` - `licenses.check` -### Licenses deprecated +#### 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. -- `license.check` moved to `entitlements.check` +- `licenses.check` moved to `entitlements.check` - `licenses.getAll` moved to `subscriptions.getSeats` -- `licenses.getOne` support removed +- `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. + +#### 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. +- `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 diff --git a/src/schemas/v3/schemas-v3.ts b/src/schemas/v3/schemas-v3.ts index 1a9d6438..2362d424 100644 --- a/src/schemas/v3/schemas-v3.ts +++ b/src/schemas/v3/schemas-v3.ts @@ -1,12 +1,9 @@ import { FeatureEnumOption, FeatureV3, - Invoice, LicenseV3, OrganisationPaymentIntegrationV3, PaginatedLicenses, - PaginatedSubscription, - PaginatedSubscriptionInvoice, PlanCurrency, PlanFeatureV3, PlanV3, @@ -14,9 +11,8 @@ import { ProductCurrency, ProductPricingTableV3, ProductV3, - Subscription, } from '../../types'; -import { EnumValueSchema, LicenseSchema, SubscriptionSchema } from '../v2/schemas-v2'; +import { EnumValueSchema} from '../v2/schemas-v2'; export const ProductSchemaV3: ProductV3 = { uuid: expect.any(String), From 8aae55ea61c54a8d02b4c2bc24963918eb253a8f Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 14:33:02 +0100 Subject: [PATCH 24/28] docs: added notice for add and remove seats in changelog --- docs/docs/changelog.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index c5a14deb..b59dd2a1 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -13,12 +13,12 @@ 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') +const salable = new Salable('your-api-key', 'v2'); ``` **v5.0.0 implementation** ```typescript -const salableV2 = initSalable('your-api-key', 'v2') // still supported -const salableV3 = initSalable('your-api-key', 'v3') +const salableV2 = initSalable('your-api-key', 'v2'); // v2 still supported +const salableV3 = initSalable('your-api-key', 'v3'); ``` ### V3 Breaking changes @@ -51,6 +51,8 @@ consistent implementation across all types of subscriptions. #### 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. @@ -59,6 +61,7 @@ consistent implementation across all types of subscriptions. #### 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** From e495ae43b3901ed32c11cbbc5e0fc29fdf1b41c2 Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 15:02:53 +0100 Subject: [PATCH 25/28] test: updated pricing table tests to create the UUID in the test itself --- src/pricing-tables/v2/pricing-table-v2.test.ts | 6 +++--- src/pricing-tables/v3/pricing-table-v3.test.ts | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pricing-tables/v2/pricing-table-v2.test.ts b/src/pricing-tables/v2/pricing-table-v2.test.ts index 7efee32a..37a9cbae 100644 --- a/src/pricing-tables/v2/pricing-table-v2.test.ts +++ b/src/pricing-tables/v2/pricing-table-v2.test.ts @@ -4,13 +4,13 @@ import { testUuids } from '../../../test-utils/scripts/create-salable-test-data' import { randomUUID } from 'crypto'; import { PricingTableSchema } from '../../schemas/v2/schemas-v2'; -const pricingTableUuid = randomUUID(); describe('Pricing Table V2 Tests', () => { const apiKey = testUuids.devApiKeyV2; const salable = initSalable(apiKey, 'v2'); + const pricingTableUuid = randomUUID(); beforeAll(async() => { - await generateTestData() + await generateTestData(pricingTableUuid) }) it('getAll: should successfully fetch all pricing tables', async () => { @@ -20,7 +20,7 @@ describe('Pricing Table V2 Tests', () => { }); -const generateTestData = async () => { +const generateTestData = async (pricingTableUuid: string) => { const product = await prismaClient.product.findUnique({ where: { uuid: testUuids.productUuid }, diff --git a/src/pricing-tables/v3/pricing-table-v3.test.ts b/src/pricing-tables/v3/pricing-table-v3.test.ts index 8dd9a0b6..cebee3f1 100644 --- a/src/pricing-tables/v3/pricing-table-v3.test.ts +++ b/src/pricing-tables/v3/pricing-table-v3.test.ts @@ -4,12 +4,13 @@ import { PricingTableSchemaV3 } from '../../schemas/v3/schemas-v3'; import { initSalable } from '../../index'; import { randomUUID } from 'crypto'; -const pricingTableUuid = randomUUID(); describe('Pricing Table V3 Tests', () => { const apiKey = testUuids.devApiKeyV3; const salable = initSalable(apiKey, 'v3'); + const pricingTableUuid = randomUUID(); + beforeAll(async() => { - await generateTestData() + await generateTestData(pricingTableUuid) }) it('getOne: should successfully fetch all pricing tables', async () => { @@ -19,7 +20,7 @@ describe('Pricing Table V3 Tests', () => { }); -const generateTestData = async () => { +const generateTestData = async (pricingTableUuid: string) => { const product = await prismaClient.product.findUnique({ where: { uuid: testUuids.productUuid }, From d9c606fce03e1295257c15cd59cd5357fe4aad2e Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 15:43:04 +0100 Subject: [PATCH 26/28] docs: updates to inconsistencies in the documentation --- docs/docs/changelog.md | 6 ++++++ docs/docs/entitlements/check.md | 4 ++-- docs/docs/features/get-all.md | 2 +- docs/docs/plans/get-all.md | 2 +- docs/docs/products/get-all.md | 6 +++--- src/constants.ts | 24 ++---------------------- src/entitlements/index.ts | 4 ++-- src/entitlements/v3/index.ts | 4 ++-- src/features/v3/index.ts | 4 ++-- src/plans/index.ts | 2 +- src/products/index.ts | 14 +++++++------- src/products/v3/product-v3.test.ts | 8 ++++---- src/subscriptions/index.ts | 4 ++-- 13 files changed, 35 insertions(+), 49 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index b59dd2a1..7d42b7d5 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -8,6 +8,7 @@ sidebar_position: 2 ### Breaking Changes +<<<<<<< Updated upstream 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. @@ -29,6 +30,11 @@ too rigid, for flexibility we have deprecated capabilities in favour of using th 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` diff --git a/docs/docs/entitlements/check.md b/docs/docs/entitlements/check.md index b6b10aca..4190e8a1 100644 --- a/docs/docs/entitlements/check.md +++ b/docs/docs/entitlements/check.md @@ -9,9 +9,9 @@ Retrieves the features the grantee(s) have access to. ## 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 check = await salable.entitlements.check({ productUuid: '{{PRODUCT_UUID}}', diff --git a/docs/docs/features/get-all.md b/docs/docs/features/get-all.md index c64ad5b1..dfc0fddb 100644 --- a/docs/docs/features/get-all.md +++ b/docs/docs/features/get-all.md @@ -26,7 +26,7 @@ _Type:_ `GetAllFeaturesOptionsV3` | **Parameter** | **Type** | **Description** | **Required** | |:--------------|:----------------|:----------------------------------------------------|:------------:| -| productUuid | string | The product the features belong to | ✅ | +| 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` | ❌ | diff --git a/docs/docs/plans/get-all.md b/docs/docs/plans/get-all.md index d69a0423..f6071892 100644 --- a/docs/docs/plans/get-all.md +++ b/docs/docs/plans/get-all.md @@ -18,7 +18,7 @@ const plans = await salable.plans.getAll(); ## Parameters -#### options (_required_) +#### options _Type:_ `GetAllPlansOptionsV3` diff --git a/docs/docs/products/get-all.md b/docs/docs/products/get-all.md index 2995baa5..2e774406 100644 --- a/docs/docs/products/get-all.md +++ b/docs/docs/products/get-all.md @@ -4,14 +4,14 @@ 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(); ``` 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 index e96249e2..6593d783 100644 --- a/src/entitlements/index.ts +++ b/src/entitlements/index.ts @@ -5,8 +5,8 @@ export type EntitlementVersions = { /** * Check entitlements * - * @param {string[]} granteeIds - The IDs of the grantee to be checked - * @param {string} productUuid - The ID of the product to be checked + * @param {string[]} granteeIds - The UUIDs of the grantee to be checked + * @param {string} productUuid - The UUID of the product to be checked * * @returns { Promise} */ diff --git a/src/entitlements/v3/index.ts b/src/entitlements/v3/index.ts index 5a76d4ea..569a8ac4 100644 --- a/src/entitlements/v3/index.ts +++ b/src/entitlements/v3/index.ts @@ -1,9 +1,9 @@ -import { SALABLE_BASE_URL } from '../../constants'; +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}/entitlements`; +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.ENTITLEMENTS}`; export const v3EntitlementMethods = (request: ApiRequest): EntitlementVersions['v3'] => ({ check: (options) => request(getUrl(`${baseUrl}/check`, options), { method: 'GET' }), diff --git a/src/features/v3/index.ts b/src/features/v3/index.ts index 3138504b..71cb0637 100644 --- a/src/features/v3/index.ts +++ b/src/features/v3/index.ts @@ -1,9 +1,9 @@ -import { SALABLE_BASE_URL } from '../../constants'; +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}/features`; +const baseUrl = `${SALABLE_BASE_URL}/${RESOURCE_NAMES.FEATURES}`; export const v3FeatureMethods = (request: ApiRequest): FeatureVersions['v3'] => ({ getAll: (options) => request(getUrl(`${baseUrl}`, options), { method: 'GET' }), diff --git a/src/plans/index.ts b/src/plans/index.ts index 0dac5f7d..b608140c 100644 --- a/src/plans/index.ts +++ b/src/plans/index.ts @@ -108,7 +108,7 @@ export type PlanVersions = { /** * 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 {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;}>} diff --git a/src/products/index.ts b/src/products/index.ts index f473a2bc..fc576aae 100644 --- a/src/products/index.ts +++ b/src/products/index.ts @@ -7,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} @@ -24,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} @@ -81,14 +81,14 @@ export type ProductVersions = { * * Docs - https://docs.salable.app/api/v3#tag/Products/operation/getProducts * - * @returns {Promise} All products present on the account + * @returns {Promise} All products for an organisation */ getAll: (options?: GetAllProductsOptionsV3) => 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/v3#tag/Products/operation/getProductByUuid * * @returns {Promise} @@ -96,9 +96,9 @@ export type ProductVersions = { getOne: (productUuid: string, options?: { expand: ('organisationPaymentIntegration')[] }) => Promise; /** - * 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. + * 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 pricingTable + * @param {string} productUuid - The UUID for the product * * @returns {Promise} */ diff --git a/src/products/v3/product-v3.test.ts b/src/products/v3/product-v3.test.ts index 1243bcc8..d86411f4 100644 --- a/src/products/v3/product-v3.test.ts +++ b/src/products/v3/product-v3.test.ts @@ -34,7 +34,7 @@ describe('Products V3 Tests', () => { }); differentOrgSalable = initSalable(value, 'v3'); }) - it('getAll: return correct plans', async () => { + it('getAll: return correct products', async () => { const plans = await differentOrgSalable.products.getAll() expect(plans).toEqual( { @@ -70,7 +70,7 @@ describe('Products V3 Tests', () => { ); }) - it('getAll: return correct plans with sort desc', async () => { + it('getAll: return correct products with sort desc', async () => { const plans = await differentOrgSalable.products.getAll({ sort: 'desc' }) @@ -107,7 +107,7 @@ describe('Products V3 Tests', () => { } ); }) - it('getAll: return correct plans with take set', async () => { + it('getAll: return correct products with take set', async () => { const plans = await differentOrgSalable.products.getAll({ take: 2 }) @@ -134,7 +134,7 @@ describe('Products V3 Tests', () => { } ); }) - it('getAll: return correct plans with archived set', async () => { + it('getAll: return correct products with archived set', async () => { const plans = await differentOrgSalable.products.getAll({ archived: true }) diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts index bf361569..8cb3e616 100644 --- a/src/subscriptions/index.ts +++ b/src/subscriptions/index.ts @@ -163,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 * @@ -423,7 +423,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 * From bb9aec168c49ef934ec66e0b978afa8095b19dfa Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 16:05:27 +0100 Subject: [PATCH 27/28] refactor: commitlint remove body-max-line-length rule --- commitlint.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/commitlint.config.js b/commitlint.config.js index 422b1944..f052fed1 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 + } +}; From 49563a4427650f18a4b2a97b67865e0ddb73026f Mon Sep 17 00:00:00 2001 From: Perry George Date: Fri, 19 Sep 2025 16:08:59 +0100 Subject: [PATCH 28/28] refactor: added array value to commitlint rule --- commitlint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitlint.config.js b/commitlint.config.js index f052fed1..a93a3976 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { - "body-max-line-length": 0 + "body-max-line-length": [0] } };