Skip to content

Commit

Permalink
Merge pull request #16792 from mozilla/FXA-9454
Browse files Browse the repository at this point in the history
feat(payments-stripe): Add applyPromoCodeToSubscription to StripeService
  • Loading branch information
xlisachan authored Apr 30, 2024
2 parents 395290f + 4e46a8a commit c06042e
Show file tree
Hide file tree
Showing 13 changed files with 917 additions and 23 deletions.
5 changes: 3 additions & 2 deletions libs/payments/stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export * from './lib/accountCustomer/accountCustomer.factories';
export * from './lib/accountCustomer/accountCustomer.manager';
export {
StripeApiListFactory,
StripeResponseFactory,
} from './lib/factories/api-list.factory';
export { StripeCardFactory } from './lib/factories/card.factory';
export { StripeCouponFactory } from './lib/factories/coupon.factory';
export { StripeCustomerFactory } from './lib/factories/customer.factory';
export { StripeInvoiceLineItemFactory } from './lib/factories/invoice-line-item.factory';
export { StripeInvoiceFactory } from './lib/factories/invoice.factory';
Expand All @@ -27,6 +29,5 @@ export * from './lib/stripe.config';
export * from './lib/stripe.constants';
export * from './lib/stripe.error';
export * from './lib/stripe.manager';
export * from './lib/stripe.service';
export * from './lib/stripe.util';
export * from './lib/accountCustomer/accountCustomer.manager';
export * from './lib/accountCustomer/accountCustomer.factories';
27 changes: 27 additions & 0 deletions libs/payments/stripe/src/lib/factories/coupon.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { faker } from '@faker-js/faker';
import { StripeCoupon } from '../stripe.client.types';

export const StripeCouponFactory = (
override?: Partial<StripeCoupon>
): StripeCoupon => ({
id: faker.string.alphanumeric({ length: 8 }),
object: 'coupon',
amount_off: null,
created: faker.number.int(),
currency: null,
duration: 'repeating',
duration_in_months: faker.number.int({ min: 1, max: 6 }),
livemode: false,
max_redemptions: null,
metadata: {},
name: null,
percent_off: faker.number.float({ min: 1, max: 100 }),
redeem_by: null,
times_redeemed: faker.number.int(),
valid: true,
...override,
});
39 changes: 39 additions & 0 deletions libs/payments/stripe/src/lib/factories/discount.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { faker } from '@faker-js/faker';
import { StripeDiscount } from '../stripe.client.types';

export const StripeDiscountFactory = (
override?: Partial<StripeDiscount>
): StripeDiscount => ({
id: `di_${faker.string.alphanumeric({ length: 14 })}`,
object: 'discount',
checkout_session: `cs_test_${faker.string.alphanumeric({ length: 60 })}`,
coupon: {
id: 'wsd',
object: 'coupon',
amount_off: null,
created: faker.number.int(),
currency: null,
duration: 'forever',
duration_in_months: null,
livemode: false,
max_redemptions: null,
metadata: {},
name: null,
percent_off: faker.number.int({ min: 1, max: 100 }),
redeem_by: null,
times_redeemed: faker.number.int(),
valid: true,
},
customer: `cus_${faker.string.alphanumeric({ length: 14 })}`,
end: null,
invoice: null,
invoice_item: null,
promotion_code: null,
start: faker.number.int(),
subscription: null,
...override,
});
71 changes: 71 additions & 0 deletions libs/payments/stripe/src/lib/stripe.client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripePlanFactory } from './factories/plan.factory';
import { StripeProductFactory } from './factories/product.factory';
import { StripePromotionCodeFactory } from './factories/promotion-code.factory';
import { StripeSubscriptionFactory } from './factories/subscription.factory';
import { StripeUpcomingInvoiceFactory } from './factories/upcoming-invoice.factory';
import { StripeClient } from './stripe.client';
Expand All @@ -33,12 +35,20 @@ const mockStripeInvoicesRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.invoices.retrieve>();
const mockStripePlansRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.plans.retrieve>();
const mockStripeProductsRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.products.retrieve>();
const mockStripePromotionCodesList =
mockJestFnGenerator<typeof Stripe.prototype.promotionCodes.list>();
const mockStripePromotionCodesRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.promotionCodes.retrieve>();
const mockStripeSubscriptionsList =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.list>();
const mockStripeSubscriptionsCreate =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.create>();
const mockStripeSubscriptionsCancel =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.cancel>();
const mockStripeSubscriptionsRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.retrieve>();
const mockStripeSubscriptionsUpdate =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.update>();

Expand All @@ -58,10 +68,18 @@ jest.mock('stripe', () => ({
plans: {
retrieve: mockStripePlansRetrieve,
},
products: {
retrieve: mockStripeProductsRetrieve,
},
promotionCodes: {
list: mockStripePromotionCodesList,
retrieve: mockStripePromotionCodesRetrieve,
},
subscriptions: {
create: mockStripeSubscriptionsCreate,
cancel: mockStripeSubscriptionsCancel,
list: mockStripeSubscriptionsList,
retrieve: mockStripeSubscriptionsRetrieve,
update: mockStripeSubscriptionsUpdate,
},
};
Expand Down Expand Up @@ -166,6 +184,20 @@ describe('StripeClient', () => {
});
});

describe('subscriptionsRetrieve', () => {
it('retrieves a subscription within Stripe', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockResponse = StripeResponseFactory(mockSubscription);

mockStripeSubscriptionsRetrieve.mockResolvedValue(mockResponse);

const result = await mockClient.subscriptionsRetrieve(
mockSubscription.id
);
expect(result).toEqual(mockResponse);
});
});

describe('subscriptionsUpdate', () => {
it('updates a subscription within Stripe', async () => {
const mockSubscription = StripeSubscriptionFactory();
Expand Down Expand Up @@ -238,4 +270,43 @@ describe('StripeClient', () => {
expect(result).toEqual(mockResponse);
});
});

describe('productsRetrieve', () => {
it('retrieves product successfully', async () => {
const mockProduct = StripeProductFactory();
const mockResponse = StripeResponseFactory(mockProduct);

mockStripeProductsRetrieve.mockResolvedValue(mockResponse);

const result = await mockClient.productsRetrieve(mockProduct.id);
expect(result).toEqual(mockResponse);
});
});

describe('promotionCodesList', () => {
it('returns promotion codes from Stripe', async () => {
const mockPromoCode = StripePromotionCodeFactory();
const mockPromoCodeList = StripeApiListFactory([mockPromoCode]);
const mockResponse = StripeResponseFactory(mockPromoCodeList);

mockStripePromotionCodesList.mockResolvedValue(mockResponse);

const result = await mockClient.promotionCodesList({
code: mockPromoCode.code,
});
expect(result).toEqual(mockResponse);
});
});

describe('promotionCodesRetrieve', () => {
it('retrieves promotion code successfully', async () => {
const mockPromoCode = StripePromotionCodeFactory();
const mockResponse = StripeResponseFactory(mockPromoCode);

mockStripePromotionCodesRetrieve.mockResolvedValue(mockResponse);

const result = await mockClient.promotionCodesRetrieve(mockPromoCode.id);
expect(result).toEqual(mockResponse);
});
});
});
36 changes: 34 additions & 2 deletions libs/payments/stripe/src/lib/stripe.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
StripePaymentIntent,
StripePaymentMethod,
StripePlan,
StripeProduct,
StripePromotionCode,
StripeResponse,
StripeSubscription,
Expand Down Expand Up @@ -103,6 +104,18 @@ export class StripeClient {
return result as StripeResponse<StripeSubscription>;
}

async subscriptionsRetrieve(
id: string,
params?: Stripe.SubscriptionRetrieveParams
) {
const result = await this.stripe.subscriptions.retrieve(id, {
...params,
expand: undefined,
});

return result as StripeResponse<StripeSubscription>;
}

async subscriptionsUpdate(
id: string,
params?: Stripe.SubscriptionUpdateParams
Expand Down Expand Up @@ -177,11 +190,30 @@ export class StripeClient {
return result as StripeResponse<StripePlan>;
}

async promotionCodeList(params: Stripe.PromotionCodeListParams) {
async productsRetrieve(id: string, params?: Stripe.ProductRetrieveParams) {
const result = await this.stripe.products.retrieve(id, {
...params,
expand: undefined,
});
return result as StripeResponse<StripeProduct>;
}

async promotionCodesList(params: Stripe.PromotionCodeListParams) {
const result = await this.stripe.promotionCodes.list({
...params,
expand: undefined,
});
return result as StripeApiList<StripePromotionCode>;
return result as StripeResponse<StripeApiList<StripePromotionCode>>;
}

async promotionCodesRetrieve(
id: string,
params?: Stripe.PromotionCodeRetrieveParams
) {
const result = await this.stripe.promotionCodes.retrieve(id, {
...params,
expand: undefined,
});
return result as StripeResponse<StripePromotionCode>;
}
}
44 changes: 44 additions & 0 deletions libs/payments/stripe/src/lib/stripe.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,50 @@ export class PlanNotFoundError extends StripeError {
}
}

export class ProductNotFoundError extends StripeError {
constructor() {
super('Product not found');
}
}

export class PromotionCodeCouldNotBeAttachedError extends StripeError {
constructor(cause: Error) {
super('Promotion code could not be attached to subscription', cause);
}
}

export class PromotionCodeInvalidError extends StripeError {
constructor() {
super('Invalid promotion code');
}
}

export class PromotionCodeNotForSubscriptionError extends StripeError {
constructor() {
super(
"Promotion code restricted to a product that doesn't match the product on this subscription"
);
}
}

export class SubscriptionCustomerIdDoesNotMatchCustomerIdError extends StripeError {
constructor() {
super('subscription.customerId does not match passed in customerId');
}
}

export class SubscriptionNotActiveError extends StripeError {
constructor() {
super('Subscription is not active');
}
}

export class SubscriptionPriceUnknownError extends StripeError {
constructor() {
super('Unknown subscription price');
}
}

export class StripeNoMinimumChargeAmountAvailableError extends StripeError {
constructor() {
super('Currency does not have a minimum charge amount available.');
Expand Down
Loading

0 comments on commit c06042e

Please sign in to comment.