Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions docs/docs/subscriptions/add-coupon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
sidebar_position: 14
---

# Add a coupon to a subscription

Adds the specified coupon to the subscription. Adding coupons do not trigger immediate adjustments and are applied in the following billing cycle.

## Code Sample

```typescript
import { Salable } from '@salable/node-sdk';

const salable = new Salable('{{API_KEY}}', 'v2');

await salable.subscriptions.addCoupon('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { couponUuid: '4c064ace-57c4-4618-bd79-a0e8029f9904' });
```

## Parameters

#### subscriptionUuid (_required_)

_Type:_ `string`

The UUID of the Subscription

#### Options (_required_)

_Type:_ `{ couponUuid: string }`

| Option | Type | Description | Required |
| ---------- | ------ | ---------------------- | -------- |
| couponUuid | string | The UUID of the coupon | ✅ |
33 changes: 33 additions & 0 deletions docs/docs/subscriptions/remove-coupon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
sidebar_position: 15
---

# Remove a coupon from a subscription

Removes the specified coupon from the subscription. Removing coupons do not trigger immediate adjustments and are applied in the following billing cycle.

## Code Sample

```typescript
import { Salable } from '@salable/node-sdk';

const salable = new Salable('{{API_KEY}}', 'v2');

await salable.subscriptions.removeCoupon('d18642b3-6dc0-40c4-aaa5-6315ed37c744', { couponUuid: '4c064ace-57c4-4618-bd79-a0e8029f9904' });
```

## Parameters

#### subscriptionUuid (_required_)

_Type:_ `string`

The UUID of the Subscription

#### Options (_required_)

_Type:_ `{ couponUuid: string }`

| Option | Type | Description | Required |
| ---------- | ------ | ---------------------- | -------- |
| couponUuid | string | The UUID of the coupon | ✅ |
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 21 additions & 19 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -648,24 +648,26 @@ enum CouponStatus {
}

model Coupon {
uuid String @id @default(uuid())
name String
productUuid String
product Product @relation(fields: [productUuid], references: [uuid], onDelete: Cascade)
appliesTo CouponsOnPlans[]
promotionCodes PromotionCodes[]
discountType DiscountType
currencies CouponCurrency[]
percentOff Float?
duration CouponDuration
durationInMonths Int?
expiresAt DateTime?
maxRedemptions Int?
status CouponStatus @default(ACTIVE)
isTest Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
couponsOnSubscriptions CouponsOnSubscriptions[]
uuid String @id @default(uuid())
name String
productUuid String
product Product @relation(fields: [productUuid], references: [uuid], onDelete: Cascade)
appliesTo CouponsOnPlans[]
promotionCodes PromotionCodes[]
discountType DiscountType
currencies CouponCurrency[]
percentOff Float?
duration CouponDuration
durationInMonths Int?
expiresAt DateTime?
maxRedemptions Int?
paymentIntegrationCouponId String?
isRestricted Boolean @default(false)
status CouponStatus @default(ACTIVE)
isTest Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
couponsOnSubscriptions CouponsOnSubscriptions[]
}

model CouponsOnPlans {
Expand Down Expand Up @@ -698,4 +700,4 @@ model CouponsOnSubscriptions {
@@id([couponUuid, subscriptionUuid])
@@index([couponUuid])
@@index([subscriptionUuid])
}
}
3 changes: 2 additions & 1 deletion src/licenses/v2/licenses-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const noSubLicenseThreeUuid = uuidv4();
const subscriptionUuid = uuidv4();
const testPurchaser = 'tester@testing.com';
const testGrantee = '123456';
const owner = 'subscription-owner'

describe('Licenses V2 Tests', () => {
const salable = new Salable(testUuids.devApiKeyV2, version);
Expand Down Expand Up @@ -574,7 +575,7 @@ const generateTestData = async () => {
email: 'tester@testing.com',
type: 'salable',
status: 'ACTIVE',
owner: 'owner_12345',
owner,
organisation: testUuids.organisationId,
license: { connect: [{ uuid: licenseUuid }, { uuid: licenseTwoUuid }, { uuid: licenseThreeUuid }] },
product: { connect: { uuid: testUuids.productUuid } },
Expand Down
3 changes: 2 additions & 1 deletion src/sessions/v2/sessions-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const stripeEnvs = JSON.parse(process.env.stripEnvs || '');
const licenseUuid = uuidv4();
const subscriptionUuid = uuidv4();
const testGrantee = '123456';
const owner = 'subscription-owner'

describe('Sessions V2 Tests', () => {
const apiKey = testUuids.devApiKeyV2;
Expand Down Expand Up @@ -102,7 +103,6 @@ const generateTestData = async () => {
paymentIntegrationSubscriptionId: stripeEnvs.basicSubscriptionFourId,
uuid: subscriptionUuid,
email: 'tester@testing.com',
owner: 'owner_12345',
type: 'salable',
status: 'ACTIVE',
organisation: testUuids.organisationId,
Expand All @@ -112,6 +112,7 @@ const generateTestData = async () => {
createdAt: new Date(),
updatedAt: new Date(),
expiryDate: new Date(Date.now() + 31536000000),
owner,
},
});
};
32 changes: 32 additions & 0 deletions src/subscriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,38 @@ export type SubscriptionVersions = {
proration?: string;
},
) => Promise<SubscriptionSeat>;

/**
* 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<void>}
*/
addCoupon: (
subscriptionUuid: string,
options: {
couponUuid: string
},
) => Promise<void>;

/**
* 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<void>}
*/
removeCoupon: (
subscriptionUuid: string,
options: {
couponUuid: string
},
) => Promise<void>;
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/subscriptions/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ export const v2SubscriptionMethods = (request: ApiRequest): SubscriptionVersions
method: 'PUT',
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) }),
});
46 changes: 44 additions & 2 deletions src/subscriptions/v2/subscriptions-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const perSeatSubscriptionUuid = uuidv4();
const licenseUuid = uuidv4();
const licenseTwoUuid = uuidv4();
const licenseThreeUuid = uuidv4();
const couponUuid = uuidv4();
const perSeatBasicLicenseUuids = [uuidv4(), uuidv4(), uuidv4(), uuidv4(), uuidv4(), uuidv4()];
const testGrantee = '123456';
const testEmail = 'tester@domain.com';
Expand Down Expand Up @@ -231,6 +232,18 @@ describe('Subscriptions V2 Tests', () => {
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' });

Expand Down Expand Up @@ -340,8 +353,8 @@ const invoiceSchema: Invoice = {
account_tax_ids: expect.toBeOneOf([expect.toBeArray(), null]),
amount_due: expect.any(Number),
amount_paid: expect.any(Number),
amount_remaining: 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]),
Expand Down Expand Up @@ -388,6 +401,7 @@ const invoiceSchema: Invoice = {
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),
Expand All @@ -410,7 +424,6 @@ const invoiceSchema: Invoice = {
subtotal_excluding_tax: expect.any(Number),
tax: expect.toBeOneOf([expect.any(Number), null]),
test_clock: expect.toBeOneOf([expect.any(String), null]),
parent: expect.toBeObject(),
total: expect.any(Number),
total_discount_amounts: expect.toBeOneOf([expect.toBeArray(), null]),
total_excluding_tax: expect.any(Number),
Expand Down Expand Up @@ -476,6 +489,7 @@ const stripePaymentMethodSchema = {

const deleteTestData = async () => {
await prismaClient.license.deleteMany({});
await prismaClient.couponsOnSubscriptions.deleteMany({});
await prismaClient.subscription.deleteMany({});
};

Expand Down Expand Up @@ -700,4 +714,32 @@ const generateTestData = async () => {
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 },
},
},
},
},
});
};
3 changes: 2 additions & 1 deletion src/usage/v2/usage-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const stripeEnvs = JSON.parse(process.env.stripEnvs || '');
const meteredLicenseUuid = uuidv4();
const usageSubscriptionUuid = uuidv4();
const testGrantee = 'userId_metered';
const owner = 'subscription-owner'

describe('Usage V2 Tests', () => {
const salable = new Salable(testUuids.devApiKeyV2, version);
Expand Down Expand Up @@ -98,7 +99,6 @@ const generateTestData = async () => {
paymentIntegrationSubscriptionId: stripeEnvs.usageBasicSubscriptionId,
uuid: usageSubscriptionUuid,
email: 'tester@testing.com',
owner: 'owner_12345',
type: 'salable',
status: 'ACTIVE',
organisation: testUuids.organisationId,
Expand Down Expand Up @@ -143,6 +143,7 @@ const generateTestData = async () => {
endTime: new Date(),
},
},
owner,
product: { connect: { uuid: testUuids.productTwoUuid } },
plan: { connect: { uuid: testUuids.usageBasicMonthlyPlanUuid } },
createdAt: new Date(),
Expand Down
9 changes: 8 additions & 1 deletion test-utils/stripe/create-stripe-test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface StripeEnvsTypes {
planPerSeatMaximumMonthlyGbpId: string;
planPerSeatMinimumMonthlyGbpId: string;
planPerSeatRangeMonthlyGbpId: string;
couponId: string;
}

export default async function createStripeData(): Promise<StripeEnvsTypes> {
Expand All @@ -51,7 +52,7 @@ export default async function createStripeData(): Promise<StripeEnvsTypes> {
}

const stripeConnect = new Stripe(STRIPE_KEY, {
apiVersion: '2023-10-16',
apiVersion: '2024-06-20',
stripeAccount: process.env.STRIPE_ACCOUNT_ID,
});
const stripePaymentMethod = await stripeConnect.paymentMethods.create({
Expand Down Expand Up @@ -213,6 +214,11 @@ export default async function createStripeData(): Promise<StripeEnvsTypes> {
product: stripeProductWidgetOne.id,
amount: 2500,
});
const coupon = 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,
Expand Down Expand Up @@ -266,5 +272,6 @@ export default async function createStripeData(): Promise<StripeEnvsTypes> {
proSubscriptionId: stripeProSubscription.id,
proSubscriptionLineItemId: stripeProSubscription.items.data[0].id,
basicSubscriptionFourLineItemId: stripeBasicSubscriptionFour.items.data[0].id,
couponId: coupon.id,
};
}