Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,33 @@ 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

#### Salable API versioning and Types
- 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
Expand Down
6 changes: 3 additions & 3 deletions __tests__/_setup/setup-test-envs.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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');
};

Expand Down
8 changes: 8 additions & 0 deletions __tests__/test-mock-data/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import objectBuilder from './object-builder';

// deprecated
export const mockCapability = objectBuilder({
name: 'test_capability',
status: 'ACTIVE',
description: 'Capability description',
});
17 changes: 17 additions & 0 deletions __tests__/test-mock-data/coupons.ts
Original file line number Diff line number Diff line change
@@ -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,
});
16 changes: 16 additions & 0 deletions __tests__/test-mock-data/currencies.ts
Original file line number Diff line number Diff line change
@@ -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',
});
11 changes: 11 additions & 0 deletions __tests__/test-mock-data/event.ts
Original file line number Diff line number Diff line change
@@ -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,
});
23 changes: 23 additions & 0 deletions __tests__/test-mock-data/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import objectBuilder from './object-builder';

export const mockFeature = objectBuilder({
name: 'Boolean Feature Name',
description: 'Feature description',
displayName: 'Boolean Feature Display Name',
variableName: 'boolean_feature',
status: 'ACTIVE',
visibility: 'public',
valueType: 'boolean',
defaultValue: 'false',
showUnlimited: false,
sortOrder: 0,
});

export const mockPlanFeature = objectBuilder({
value: 'xxxxx',
isUnlimited: undefined as boolean | undefined,
isUsage: undefined as boolean | undefined, // deprecated
pricePerUnit: 10, // deprecated
minUsage: 1, // deprecated
maxUsage: 100, // deprecated
});
38 changes: 38 additions & 0 deletions __tests__/test-mock-data/licenses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LicensesUsageRecordType, Prisma } from '@prisma/client';
import objectBuilder from './object-builder';

// deprecated
export const mockLicenseCapability = objectBuilder({
name: 'Export',
uuid: '38e63e2a-1269-4e9d-b712-28cfbf087285',
status: 'ACTIVE',
updatedAt: '2022-10-17T11:41:11.626Z',
description: null as string | null,
productUuid: 'c32f26e4-21d9-4456-a1f0-7e76039af518',
});

export const mockLicenseUsageRecord = objectBuilder({
unitCount: 0,
type: 'current' as LicensesUsageRecordType,
resetAt: null as Date | null,
recordedAt: null as Date | null,
});

export const mockLicense = objectBuilder({
name: null as string | null,
email: null as string | null,
status: 'ACTIVE',
granteeId: '123456' as string | null,
paymentService: 'ad-hoc',
purchaser: 'tester@testing.com',
type: 'licensed',
metadata: undefined as undefined | { member: string; granteeId: string },
capabilities: [
mockLicenseCapability({ name: 'CapabilityOne' }),
mockLicenseCapability({ name: 'CapabilityTwo' }),
] as Prisma.InputJsonObject[], // deprecated
startTime: undefined as undefined | Date,
endTime: new Date(),
cancelAtPeriodEnd: false,
isTest: false,
});
32 changes: 32 additions & 0 deletions __tests__/test-mock-data/object-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;

function isObject(obj?: unknown): obj is object {
return Boolean(obj) && (obj as object)?.constructor === Object;
}

function merge(target: Record<string, unknown>, source?: Record<string, unknown>) {
const clone = { ...target } as Record<string, unknown>;
if (!source) return clone;
for (const key of Object.keys(source)) {
if (isObject(source[key])) {
clone[key] = merge(
clone[key] as Record<string, unknown>,
source[key] as Record<string, unknown>
);
} else {
clone[key] = source[key];
}
}

return clone;
}

export default function objectBuilder<T>(defaultParameters: T) {
return (overrideParameters?: DeepPartial<T> | null): T => {
if (!overrideParameters) overrideParameters = {} as DeepPartial<T>;
return merge(
defaultParameters as Record<string, unknown>,
overrideParameters as Record<string, unknown>
) as T;
};
}
1 change: 1 addition & 0 deletions __tests__/test-mock-data/optional-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
27 changes: 27 additions & 0 deletions __tests__/test-mock-data/plans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import objectBuilder from './object-builder';

export const mockPlan = objectBuilder({
name: 'Free Plan Name', // deprecated
description: 'Free Plan description',
displayName: 'Free Plan Display Name',
slug: 'example-slug',
status: 'ACTIVE',
trialDays: 0, // deprecated
evaluation: false, // deprecated
evalDays: 0,
organisation: 'xxxxx',
visibility: 'public',
licenseType: 'licensed',
interval: 'month',
perSeatAmount: 1,
maxSeatAmount: -1,
length: 1,
active: true, // deprecated
planType: 'Standard', // deprecated
pricingType: 'free',
environment: 'dev', // deprecated
paddlePlanId: null, // deprecated
isTest: false,
hasAcceptedTransaction: false,
archivedAt: null as Date | null,
});
8 changes: 8 additions & 0 deletions __tests__/test-mock-data/pricing-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import objectBuilder from './object-builder';

export const mockPricingTable = objectBuilder({
name: 'Sample Pricing Table',
status: 'ACTIVE',
productUuid: 'xxxxxx',
featuredPlanUuid: 'xxxxxx',
});
29 changes: 29 additions & 0 deletions __tests__/test-mock-data/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import objectBuilder from './object-builder';
import { PaymentIntegration, PaymentIntegrationStatus } from '@prisma/client';

export const mockProduct = objectBuilder({
name: 'Sample Product',
description: 'This is a sample product for testing purposes',
logoUrl: 'https://example.com/logo.png',
displayName: 'Sample Product',
organisation: 'xxxxx',
slug: 'example-slug',
status: 'ACTIVE',
paid: false,
appType: 'CUSTOM', // deprecated
isTest: false,
archivedAt: null as null | Date,
});

export const mockOrganisationPaymentIntegration = objectBuilder({
organisation: 'xxxxx',
integrationName: 'stripe_existing' as PaymentIntegration,
accountData: { // deprecated
key: 'xxxxx',
encryptedData: 'xoxox',
},
isTest: false,
accountName: 'Account Name',
accountId: 'acc_1234',
status: PaymentIntegrationStatus.active,
});
10 changes: 10 additions & 0 deletions __tests__/test-mock-data/sessions.ts
Original file line number Diff line number Diff line change
@@ -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: {},
});
19 changes: 19 additions & 0 deletions __tests__/test-mock-data/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -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,
});
29 changes: 28 additions & 1 deletion docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/entitlements/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"label": "Entitlements",
"position": 2,
"link": {
"type": "generated-index",
"description": "Contains methods for the Entitlements resource"
}
}
Loading
Loading