chore(libs): restructure stripe lib into responsibility-driven structure

This commit is contained in:
julianpoyourow 2024-06-04 16:53:26 +00:00
Родитель 915fcd55ec
Коммит de2ad977c9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: EA0570ABC73D47D3
35 изменённых файлов: 1344 добавлений и 1087 удалений

Просмотреть файл

@ -24,11 +24,15 @@ import {
ResultAccountCustomerFactory,
StripeClient,
StripeCustomerFactory,
StripeManager,
CustomerManager,
StripePriceFactory,
StripeResponseFactory,
SubplatInterval,
TaxAddressFactory,
InvoiceManager,
PriceManager,
SubscriptionManager,
PromotionCodeManager,
} from '@fxa/payments/stripe';
import {
ContentfulClient,
@ -69,7 +73,8 @@ describe('CartService', () => {
let accountCustomerManager: AccountCustomerManager;
let eligibilityService: EligibilityService;
let geodbManager: GeoDBManager;
let stripeManager: StripeManager;
let customerManager: CustomerManager;
let invoiceManager: InvoiceManager;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
@ -90,7 +95,7 @@ describe('CartService', () => {
EligibilityService,
MockStripeConfigProvider,
StripeClient,
StripeManager,
CustomerManager,
MockPaypalClientConfigProvider,
PayPalClient,
PayPalManager,
@ -99,8 +104,12 @@ describe('CartService', () => {
GeoDBManager,
GeoDBManagerConfig,
MockGeoDBNestFactory,
StripeManager,
CustomerManager,
InvoiceManager,
AccountManager,
PriceManager,
SubscriptionManager,
PromotionCodeManager,
],
}).compile();
@ -111,7 +120,8 @@ describe('CartService', () => {
eligibilityService = moduleRef.get(EligibilityService);
geodbManager = moduleRef.get(GeoDBManager);
contentfulService = moduleRef.get(ContentfulService);
stripeManager = moduleRef.get(StripeManager);
customerManager = moduleRef.get(CustomerManager);
invoiceManager = moduleRef.get(InvoiceManager);
});
describe('setupCart', () => {
@ -143,11 +153,9 @@ describe('CartService', () => {
jest
.spyOn(contentfulService, 'retrieveStripePlanId')
.mockResolvedValue(mockPrice.id);
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
jest
.spyOn(stripeManager, 'fetchActiveCustomer')
.mockResolvedValue(mockCustomer);
jest
.spyOn(stripeManager, 'previewInvoice')
.spyOn(invoiceManager, 'preview')
.mockResolvedValue(mockInvoicePreview);
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockResultCart);
@ -403,11 +411,9 @@ describe('CartService', () => {
jest
.spyOn(contentfulService, 'retrieveStripePlanId')
.mockResolvedValue(mockPrice.id);
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
jest
.spyOn(stripeManager, 'fetchActiveCustomer')
.mockResolvedValue(mockCustomer);
jest
.spyOn(stripeManager, 'previewInvoice')
.spyOn(invoiceManager, 'preview')
.mockResolvedValue(mockInvoicePreview);
const result = await cartService.getCart(mockCart.id);
@ -421,10 +427,10 @@ describe('CartService', () => {
mockCart.offeringConfigId,
mockCart.interval
);
expect(stripeManager.fetchActiveCustomer).toHaveBeenCalledWith(
expect(customerManager.retrieve).toHaveBeenCalledWith(
mockCart.stripeCustomerId
);
expect(stripeManager.previewInvoice).toHaveBeenCalledWith({
expect(invoiceManager.preview).toHaveBeenCalledWith({
priceId: mockPrice.id,
customer: mockCustomer,
taxAddress: mockCart.taxAddress,
@ -442,9 +448,9 @@ describe('CartService', () => {
jest
.spyOn(contentfulService, 'retrieveStripePlanId')
.mockResolvedValue(mockPrice.id);
jest.spyOn(stripeManager, 'fetchActiveCustomer');
jest.spyOn(customerManager, 'retrieve');
jest
.spyOn(stripeManager, 'previewInvoice')
.spyOn(invoiceManager, 'preview')
.mockResolvedValue(mockInvoicePreview);
const result = await cartService.getCart(mockCart.id);
@ -458,8 +464,8 @@ describe('CartService', () => {
mockCart.offeringConfigId,
mockCart.interval
);
expect(stripeManager.fetchActiveCustomer).not.toHaveBeenCalled();
expect(stripeManager.previewInvoice).toHaveBeenCalledWith({
expect(customerManager.retrieve).not.toHaveBeenCalled();
expect(invoiceManager.preview).toHaveBeenCalledWith({
priceId: mockPrice.id,
customer: undefined,
taxAddress: mockCart.taxAddress,

Просмотреть файл

@ -8,8 +8,9 @@ import { EligibilityService } from '@fxa/payments/eligibility';
import {
AccountCustomerManager,
AccountCustomerNotFoundError,
CustomerManager,
InvoiceManager,
StripeCustomer,
StripeManager,
SubplatInterval,
TaxAddress,
} from '@fxa/payments/stripe';
@ -36,7 +37,8 @@ export class CartService {
private geodbManager: GeoDBManager,
private checkoutService: CheckoutService,
private contentfulService: ContentfulService,
private stripeManager: StripeManager
private customerManager: CustomerManager,
private invoiceManager: InvoiceManager
) {}
/**
@ -70,7 +72,7 @@ export class CartService {
const stripeCustomerId = accountCustomer?.stripeCustomerId;
const stripeCustomer = stripeCustomerId
? await this.stripeManager.fetchActiveCustomer(stripeCustomerId)
? await this.customerManager.retrieve(stripeCustomerId)
: undefined;
const taxAddress = args.ip
@ -82,7 +84,7 @@ export class CartService {
args.interval
);
const upcomingInvoice = await this.stripeManager.previewInvoice({
const upcomingInvoice = await this.invoiceManager.preview({
priceId: priceId,
customer: stripeCustomer,
taxAddress: taxAddress,
@ -223,12 +225,10 @@ export class CartService {
let customer: StripeCustomer | undefined;
if (cart.stripeCustomerId) {
customer = await this.stripeManager.fetchActiveCustomer(
cart.stripeCustomerId
);
customer = await this.customerManager.retrieve(cart.stripeCustomerId);
}
const invoicePreview = await this.stripeManager.previewInvoice({
const invoicePreview = await this.invoiceManager.preview({
priceId,
customer,
taxAddress: cart.taxAddress as unknown as TaxAddress, // TODO: Fix the typings for taxAddress

Просмотреть файл

@ -11,7 +11,6 @@ import {
StripeConfig,
StripeCustomerFactory,
StripeInvoiceFactory,
StripeManager,
StripePaymentIntentFactory,
StripePaymentMethodFactory,
StripePriceFactory,
@ -22,6 +21,11 @@ import {
InvoicePreviewFactory,
AccountCustomerManager,
ResultAccountCustomerFactory,
CustomerManager,
SubscriptionManager,
InvoiceManager,
PromotionCodeManager,
PriceManager,
} from '@fxa/payments/stripe';
import {
PayPalClient,
@ -64,7 +68,10 @@ import { AccountManager } from '@fxa/shared/account/account';
describe('CheckoutService', () => {
let checkoutService: CheckoutService;
let stripeClient: StripeClient;
let stripeManager: StripeManager;
let customerManager: CustomerManager;
let subscriptionManager: SubscriptionManager;
let invoiceManager: InvoiceManager;
let promotionCodeManager: PromotionCodeManager;
let paypalCustomerManager: PaypalCustomerManager;
let paypalManager: PayPalManager;
let cartManager: CartManager;
@ -77,7 +84,11 @@ describe('CheckoutService', () => {
const moduleRef = await Test.createTestingModule({
providers: [
StripeClient,
StripeManager,
CustomerManager,
SubscriptionManager,
InvoiceManager,
PromotionCodeManager,
PriceManager,
PaypalCustomerManager,
PayPalManager,
CheckoutService,
@ -106,7 +117,10 @@ describe('CheckoutService', () => {
paypalCustomerManager = moduleRef.get(PaypalCustomerManager);
paypalManager = moduleRef.get(PayPalManager);
stripeClient = moduleRef.get(StripeClient);
stripeManager = moduleRef.get(StripeManager);
customerManager = moduleRef.get(CustomerManager);
subscriptionManager = moduleRef.get(SubscriptionManager);
invoiceManager = moduleRef.get(InvoiceManager);
promotionCodeManager = moduleRef.get(PromotionCodeManager);
contentfulService = moduleRef.get(ContentfulService);
accountManager = moduleRef.get(AccountManager);
accountCustomerManager = moduleRef.get(AccountCustomerManager);
@ -160,12 +174,8 @@ describe('CheckoutService', () => {
beforeEach(async () => {
jest.spyOn(accountManager, 'createAccountStub').mockResolvedValue(uid);
jest
.spyOn(stripeManager, 'createPlainCustomer')
.mockResolvedValue(mockCustomer);
jest
.spyOn(stripeManager, 'fetchActiveCustomer')
.mockResolvedValue(mockCustomer);
jest.spyOn(customerManager, 'create').mockResolvedValue(mockCustomer);
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
jest
.spyOn(accountCustomerManager, 'createAccountCustomer')
.mockResolvedValue(mockAccountCustomer);
@ -177,13 +187,13 @@ describe('CheckoutService', () => {
.spyOn(contentfulService, 'retrieveStripePlanId')
.mockResolvedValue(mockPrice.id);
jest
.spyOn(stripeManager, 'previewInvoice')
.spyOn(invoiceManager, 'preview')
.mockResolvedValue(mockInvoicePreview);
jest
.spyOn(stripeManager, 'cancelIncompleteSubscriptionsToPrice')
.spyOn(subscriptionManager, 'cancelIncompleteSubscriptionsToPrice')
.mockResolvedValue();
jest
.spyOn(stripeManager, 'getPromotionCodeByName')
.spyOn(promotionCodeManager, 'retrieveByName')
.mockResolvedValue(mockPromotionCode);
});
@ -193,7 +203,7 @@ describe('CheckoutService', () => {
});
it('fetches the customer', () => {
expect(stripeManager.fetchActiveCustomer).toHaveBeenCalledWith(
expect(customerManager.retrieve).toHaveBeenCalledWith(
mockCart.stripeCustomerId
);
});
@ -216,16 +226,16 @@ describe('CheckoutService', () => {
it('checks that customer does not have existing subscription to price', () => {
expect(
stripeManager.cancelIncompleteSubscriptionsToPrice
subscriptionManager.cancelIncompleteSubscriptionsToPrice
).toHaveBeenCalledWith(mockCustomer.id, mockPrice.id);
});
it('checks if customer is stripe tax eligible', async () => {
expect(stripeManager.isCustomerStripeTaxEligible).toBeTruthy();
expect(customerManager.isTaxEligible).toBeTruthy();
});
it('fetches promotion code by name', async () => {
expect(stripeManager.getPromotionCodeByName).toHaveBeenCalledWith(
expect(promotionCodeManager.retrieveByName).toHaveBeenCalledWith(
mockCart.couponCode,
true
);
@ -291,7 +301,7 @@ describe('CheckoutService', () => {
});
it('creates a new stripe customer stub account', () => {
expect(stripeManager.createPlainCustomer).toHaveBeenCalledWith({
expect(customerManager.create).toHaveBeenCalledWith({
uid: uid,
email: mockCart.email,
displayName: mockCustomerData.displayName,
@ -403,7 +413,7 @@ describe('CheckoutService', () => {
.spyOn(stripeClient, 'paymentIntentRetrieve')
.mockResolvedValue(mockPaymentIntent);
jest
.spyOn(stripeManager, 'cancelSubscription')
.spyOn(subscriptionManager, 'cancel')
.mockResolvedValue(mockSubscription);
});
@ -471,7 +481,7 @@ describe('CheckoutService', () => {
});
it('does not cancel the subscription', () => {
expect(stripeManager.cancelSubscription).not.toHaveBeenCalled();
expect(subscriptionManager.cancel).not.toHaveBeenCalled();
});
});
});
@ -524,7 +534,7 @@ describe('CheckoutService', () => {
.mockResolvedValue(mockInvoice);
jest.spyOn(paypalManager, 'processInvoice').mockResolvedValue();
jest
.spyOn(stripeManager, 'cancelSubscription')
.spyOn(subscriptionManager, 'cancel')
.mockResolvedValue(mockSubscription);
jest.spyOn(paypalManager, 'cancelBillingAgreement').mockResolvedValue();
});
@ -595,7 +605,7 @@ describe('CheckoutService', () => {
});
it('does not cancel the subscription', () => {
expect(stripeManager.cancelSubscription).not.toHaveBeenCalled();
expect(subscriptionManager.cancel).not.toHaveBeenCalled();
});
it('does not cancel the billing agreement', () => {

Просмотреть файл

@ -7,10 +7,13 @@ import { EligibilityService } from '@fxa/payments/eligibility';
import { PayPalManager, PaypalCustomerManager } from '@fxa/payments/paypal';
import {
AccountCustomerManager,
CustomerManager,
InvoiceManager,
PromotionCodeManager,
StripeClient,
StripeManager,
StripeSubscription,
SubplatInterval,
SubscriptionManager,
TaxAddress,
} from '@fxa/payments/stripe';
import { CheckoutError, CheckoutPaymentError } from './checkout.error';
@ -29,7 +32,10 @@ import { AccountManager } from '@fxa/shared/account/account';
export class CheckoutService {
constructor(
private stripeClient: StripeClient,
private stripeManager: StripeManager,
private customerManager: CustomerManager,
private subscriptionManager: SubscriptionManager,
private invoiceManager: InvoiceManager,
private promotionCodeManager: PromotionCodeManager,
private paypalCustomerManager: PaypalCustomerManager,
private paypalManager: PayPalManager,
private cartManager: CartManager,
@ -62,7 +68,7 @@ export class CheckoutService {
// if stripeCustomerId not found, create plain stripe account
if (!cart.stripeCustomerId) {
customer = await this.stripeManager.createPlainCustomer({
customer = await this.customerManager.create({
uid,
email: cart.email,
displayName: customerData.displayName,
@ -72,7 +78,7 @@ export class CheckoutService {
stripeCustomerId = customer.id;
} else {
stripeCustomerId = cart.stripeCustomerId;
customer = await this.stripeManager.fetchActiveCustomer(stripeCustomerId);
customer = await this.customerManager.retrieve(stripeCustomerId);
}
// create accountCustomer if it does not exist
@ -119,7 +125,7 @@ export class CheckoutService {
cart.interval as SubplatInterval
);
const upcomingInvoice = await this.stripeManager.previewInvoice({
const upcomingInvoice = await this.invoiceManager.preview({
priceId: priceId,
customer: customer,
taxAddress: taxAddress,
@ -134,16 +140,15 @@ export class CheckoutService {
}
// check if customer already has subscription to price and cancel if they do
await this.stripeManager.cancelIncompleteSubscriptionsToPrice(
await this.subscriptionManager.cancelIncompleteSubscriptionsToPrice(
stripeCustomerId,
priceId
);
const enableAutomaticTax =
this.stripeManager.isCustomerStripeTaxEligible(customer);
const enableAutomaticTax = this.customerManager.isTaxEligible(customer);
const promotionCode = cart.couponCode
? await this.stripeManager.getPromotionCodeByName(cart.couponCode, true)
? await this.promotionCodeManager.retrieveByName(cart.couponCode, true)
: undefined;
return {
@ -197,7 +202,7 @@ export class CheckoutService {
// TODO: Generate and use idempotency key using util
});
const paymentIntent = await this.stripeManager.getLatestPaymentIntent(
const paymentIntent = await this.subscriptionManager.getLatestPaymentIntent(
subscription
);
if (!paymentIntent) {
@ -212,7 +217,7 @@ export class CheckoutService {
}
if (paymentIntent.last_payment_error) {
await this.stripeManager.cancelSubscription(subscription.id);
await this.subscriptionManager.cancel(subscription.id);
throw new CheckoutPaymentError(
'Checkout payment intent has error on payment attempt',
@ -281,7 +286,7 @@ export class CheckoutService {
try {
this.paypalManager.processInvoice(latestInvoice);
} catch (e) {
await this.stripeManager.cancelSubscription(subscription.id);
await this.subscriptionManager.cancel(subscription.id);
await this.paypalManager.cancelBillingAgreement(billingAgreementId);
}

Просмотреть файл

@ -0,0 +1,25 @@
/* 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 { Transform } from 'class-transformer';
import { IsObject } from 'class-validator';
import { Provider } from '@nestjs/common';
export class CurrencyConfig {
@Transform(
({ value }) => (value instanceof Object ? value : JSON.parse(value)),
{ toClassOnly: true }
)
@IsObject()
public readonly taxIds!: { [key: string]: string };
}
export const MockCurrencyConfig = {
taxIds: { EUR: 'EU1234' },
} satisfies CurrencyConfig;
export const MockCurrencyConfigProvider = {
provide: CurrencyConfig,
useValue: MockCurrencyConfig,
} satisfies Provider<CurrencyConfig>;

Просмотреть файл

@ -11,16 +11,19 @@ import {
CurrencyCountryMismatchError,
} from './currency.error';
import { CURRENCIES_TO_COUNTRIES } from './currency.constants';
import { CurrencyConfig, MockCurrencyConfigProvider } from './currency.config';
describe('CurrencyManager', () => {
let currencyManager: CurrencyManager;
let mockCurrencyConfig: CurrencyConfig;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [CurrencyManager],
providers: [MockCurrencyConfigProvider, CurrencyManager],
}).compile();
currencyManager = module.get(CurrencyManager);
mockCurrencyConfig = module.get(CurrencyConfig);
});
describe('assertCurrencyCompatibleWithCountry', () => {
@ -64,4 +67,18 @@ describe('CurrencyManager', () => {
).toThrow(CurrencyCountryMismatchError);
});
});
describe('getTaxId', () => {
it('returns the correct tax id for currency', async () => {
const mockCurrency = Object.entries(mockCurrencyConfig.taxIds)[0];
const result = currencyManager.getTaxId(mockCurrency[0]);
expect(result).toEqual(mockCurrency[1]);
});
it('returns empty string when no tax id found', async () => {
const result = currencyManager.getTaxId('DOES NOT EXIST');
expect(result).toEqual(undefined);
});
});
});

Просмотреть файл

@ -14,10 +14,14 @@ import {
CountryCodeInvalidError,
CurrencyCountryMismatchError,
} from './currency.error';
import { CurrencyConfig } from './currency.config';
@Injectable()
export class CurrencyManager {
constructor() {}
private taxIds: { [key: string]: string };
constructor(private config: CurrencyConfig) {
this.taxIds = this.config.taxIds;
}
/**
* Verify that provided source country and plan currency are compatible with
@ -52,4 +56,8 @@ export class CurrencyManager {
throw new CurrencyCountryMismatchError(currency, country);
}
}
getTaxId(currency: string) {
return this.taxIds[currency.toUpperCase()];
}
}

Просмотреть файл

@ -5,7 +5,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import {
StripeManager,
PriceManager,
StripePlan,
StripePlanFactory,
SubplatInterval,
@ -34,7 +34,7 @@ describe('EligibilityManager', () => {
let mockContentfulManager: ContentfulManager;
let mockOfferingResult: EligibilityContentByOfferingResultUtil;
let mockResult: EligibilityContentByPlanIdsResultUtil;
let mockStripeManager: StripeManager;
let mockPriceManager: PriceManager;
beforeEach(async () => {
mockOfferingResult = {} as EligibilityContentByOfferingResultUtil;
@ -47,14 +47,14 @@ describe('EligibilityManager', () => {
.fn()
.mockResolvedValueOnce(mockResult),
} as any;
mockStripeManager = {
getPlanByInterval: jest.fn(),
mockPriceManager = {
retrieveByInterval: jest.fn(),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: ContentfulManager, useValue: mockContentfulManager },
{ provide: StripeManager, useValue: mockStripeManager },
{ provide: PriceManager, useValue: mockPriceManager },
EligibilityManager,
],
}).compile();
@ -624,7 +624,7 @@ describe('EligibilityManager', () => {
});
const interval = SubplatInterval.Monthly;
mockStripeManager.getPlanByInterval = jest
mockPriceManager.retrieveByInterval = jest
.fn()
.mockResolvedValueOnce(mockPlan2);
@ -658,7 +658,7 @@ describe('EligibilityManager', () => {
},
] as OfferingOverlapProductResult[];
mockStripeManager.getPlanByInterval = jest
mockPriceManager.retrieveByInterval = jest
.fn()
.mockResolvedValueOnce(mockPlan2);
@ -692,7 +692,7 @@ describe('EligibilityManager', () => {
},
] as OfferingOverlapProductResult[];
mockStripeManager.getPlanByInterval = jest
mockPriceManager.retrieveByInterval = jest
.fn()
.mockResolvedValueOnce(mockPlan2);

Просмотреть файл

@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import {
StripeManager,
PriceManager,
StripePlan,
SubplatInterval,
} from '@fxa/payments/stripe';
@ -27,7 +27,7 @@ import { intervalComparison, offeringComparison } from './utils';
export class EligibilityManager {
constructor(
private contentfulManager: ContentfulManager,
private stripeManager: StripeManager
private priceManager: PriceManager
) {}
/**
@ -116,7 +116,7 @@ export class EligibilityManager {
return EligibilityStatus.DOWNGRADE;
const targetPlanIds = targetOffering.defaultPurchase.stripePlanChoices;
const targetPlan = await this.stripeManager.getPlanByInterval(
const targetPlan = await this.priceManager.retrieveByInterval(
targetPlanIds,
interval
);

Просмотреть файл

@ -15,9 +15,10 @@ import {
StripeClient,
StripeConfig,
StripeCustomerFactory,
StripeManager,
SubscriptionManager,
StripeSubscriptionFactory,
SubplatInterval,
PriceManager,
} from '@fxa/payments/stripe';
import {
ContentfulClient,
@ -41,7 +42,7 @@ describe('EligibilityService', () => {
let contentfulManager: ContentfulManager;
let eligibilityManager: EligibilityManager;
let eligibilityService: EligibilityService;
let stripeManager: StripeManager;
let subscriptionManager: SubscriptionManager;
let mockOfferingResult: EligibilityContentByOfferingResultUtil;
@ -55,10 +56,11 @@ describe('EligibilityService', () => {
ContentfulClient,
MockStatsDProvider,
ContentfulManager,
EligibilityManager,
StripeConfig,
StripeClient,
StripeManager,
SubscriptionManager,
PriceManager,
EligibilityManager,
EligibilityService,
],
}).compile();
@ -66,7 +68,7 @@ describe('EligibilityService', () => {
contentfulManager = module.get<ContentfulManager>(ContentfulManager);
eligibilityManager = module.get<EligibilityManager>(EligibilityManager);
eligibilityService = module.get<EligibilityService>(EligibilityService);
stripeManager = module.get<StripeManager>(StripeManager);
subscriptionManager = module.get<SubscriptionManager>(SubscriptionManager);
});
describe('checkEligibility', () => {
@ -124,7 +126,7 @@ describe('EligibilityService', () => {
mockOfferingResult.getOffering = jest.fn().mockReturnValue(mockOffering);
jest
.spyOn(stripeManager, 'getSubscriptions')
.spyOn(subscriptionManager, 'listForCustomer')
.mockResolvedValue([mockSubscription]);
mockStripeUtil.getSubscribedPlans.mockReturnValue([]);
@ -147,7 +149,7 @@ describe('EligibilityService', () => {
expect(
contentfulManager.getEligibilityContentByOffering
).toHaveBeenCalledWith(mockOffering.apiIdentifier);
expect(stripeManager.getSubscriptions).toHaveBeenCalledWith(
expect(subscriptionManager.listForCustomer).toHaveBeenCalledWith(
mockCustomer.id
);
expect(eligibilityManager.getProductIdOverlap).toHaveBeenCalledWith(

Просмотреть файл

@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common';
import {
getSubscribedPlans,
getSubscribedProductIds,
StripeManager,
SubscriptionManager,
SubplatInterval,
} from '@fxa/payments/stripe';
import { ContentfulManager } from '@fxa/shared/contentful';
@ -18,7 +18,7 @@ export class EligibilityService {
constructor(
private contentfulManager: ContentfulManager,
private eligibilityManager: EligibilityManager,
private stripeManager: StripeManager
private subscriptionManager: SubscriptionManager
) {}
/**
@ -40,7 +40,7 @@ export class EligibilityService {
const targetOffering = targetOfferingResult.getOffering();
const subscriptions = await this.stripeManager.getSubscriptions(
const subscriptions = await this.subscriptionManager.listForCustomer(
stripeCustomerId
);

Просмотреть файл

@ -5,13 +5,15 @@ import { faker } from '@faker-js/faker';
import { Test } from '@nestjs/testing';
import {
CustomerManager,
InvoiceManager,
MockStripeConfigProvider,
StripeClient,
StripeCustomerFactory,
StripeInvoiceFactory,
StripeManager,
StripeResponseFactory,
StripeSubscriptionFactory,
SubscriptionManager,
} from '@fxa/payments/stripe';
import { MockAccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
@ -35,8 +37,10 @@ import { MockPaypalClientConfigProvider } from './paypal.client.config';
describe('PayPalManager', () => {
let paypalManager: PayPalManager;
let paypalClient: PayPalClient;
let stripeManager: StripeManager;
let paypalCustomerManager: PaypalCustomerManager;
let customerManager: CustomerManager;
let subscriptionManager: SubscriptionManager;
let invoiceManager: InvoiceManager;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
@ -47,14 +51,18 @@ describe('PayPalManager', () => {
PayPalManager,
PayPalClient,
StripeClient,
StripeManager,
CustomerManager,
SubscriptionManager,
InvoiceManager,
PaypalCustomerManager,
],
}).compile();
paypalManager = moduleRef.get(PayPalManager);
paypalClient = moduleRef.get(PayPalClient);
stripeManager = moduleRef.get(StripeManager);
customerManager = moduleRef.get(CustomerManager);
subscriptionManager = moduleRef.get(SubscriptionManager);
invoiceManager = moduleRef.get(InvoiceManager);
paypalCustomerManager = moduleRef.get(PaypalCustomerManager);
});
@ -320,7 +328,7 @@ describe('PayPalManager', () => {
const expected = [mockPayPalSubscription];
jest
.spyOn(stripeManager, 'getSubscriptions')
.spyOn(subscriptionManager, 'listForCustomer')
.mockResolvedValue([mockPayPalSubscription]);
const result = await paypalManager.getCustomerPayPalSubscriptions(
@ -333,7 +341,9 @@ describe('PayPalManager', () => {
it('returns empty array when no subscriptions', async () => {
const mockCustomer = StripeCustomerFactory();
jest.spyOn(stripeManager, 'getSubscriptions').mockResolvedValueOnce([]);
jest
.spyOn(subscriptionManager, 'listForCustomer')
.mockResolvedValueOnce([]);
const result = await paypalManager.getCustomerPayPalSubscriptions(
mockCustomer.id
@ -367,13 +377,13 @@ describe('PayPalManager', () => {
const mockInvoice = StripeResponseFactory(StripeInvoiceFactory());
jest
.spyOn(stripeManager, 'finalizeInvoiceWithoutAutoAdvance')
.spyOn(invoiceManager, 'finalizeWithoutAutoAdvance')
.mockResolvedValue(mockInvoice);
const result = await paypalManager.processZeroInvoice(mockInvoice.id);
expect(result).toEqual(mockInvoice);
expect(stripeManager.finalizeInvoiceWithoutAutoAdvance).toBeCalledWith(
expect(invoiceManager.finalizeWithoutAutoAdvance).toBeCalledWith(
mockInvoice.id
);
});
@ -388,7 +398,7 @@ describe('PayPalManager', () => {
})
);
jest.spyOn(stripeManager, 'getMinimumAmount').mockReturnValue(10);
jest.spyOn(subscriptionManager, 'getMinimumAmount').mockReturnValue(10);
jest
.spyOn(paypalManager, 'processZeroInvoice')
.mockResolvedValue(mockInvoice);
@ -406,10 +416,8 @@ describe('PayPalManager', () => {
currency: 'usd',
});
jest.spyOn(stripeManager, 'getMinimumAmount').mockReturnValue(10);
jest
.spyOn(stripeManager, 'fetchActiveCustomer')
.mockResolvedValue(mockCustomer);
jest.spyOn(subscriptionManager, 'getMinimumAmount').mockReturnValue(10);
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
jest
.spyOn(paypalManager, 'processZeroInvoice')
.mockResolvedValue(StripeResponseFactory(mockInvoice));

Просмотреть файл

@ -6,9 +6,11 @@ import { Injectable } from '@nestjs/common';
import {
ACTIVE_SUBSCRIPTION_STATUSES,
CustomerManager,
InvoiceManager,
StripeCustomer,
StripeInvoice,
StripeManager,
SubscriptionManager,
} from '@fxa/payments/stripe';
import { PayPalClient } from './paypal.client';
import { BillingAgreement, BillingAgreementStatus } from './paypal.types';
@ -21,7 +23,9 @@ import { PaypalManagerError } from './paypal.error';
export class PayPalManager {
constructor(
private client: PayPalClient,
private stripeManager: StripeManager,
private customerManager: CustomerManager,
private subscriptionManager: SubscriptionManager,
private invoiceManager: InvoiceManager,
private paypalCustomerManager: PaypalCustomerManager
) {}
@ -122,11 +126,11 @@ export class PayPalManager {
return firstRecord.billingAgreementId;
}
/**
* Retrieves PayPal subscriptions
*/
// TODO: This should be moved to the subscription manager
async getCustomerPayPalSubscriptions(customerId: string) {
const subscriptions = await this.stripeManager.getSubscriptions(customerId);
const subscriptions = await this.subscriptionManager.listForCustomer(
customerId
);
if (!subscriptions) return [];
return subscriptions.filter(
(sub) =>
@ -164,7 +168,7 @@ export class PayPalManager {
// It appears for subscriptions that do not require payment, the invoice
// transitions to paid automatially.
// https://stripe.com/docs/billing/invoices/subscription#sub-invoice-lifecycle
return this.stripeManager.finalizeInvoiceWithoutAutoAdvance(invoiceId);
return this.invoiceManager.finalizeWithoutAutoAdvance(invoiceId);
}
/**
@ -176,14 +180,15 @@ export class PayPalManager {
if (!invoice.customer) throw new Error('Customer not present on invoice');
const amountInCents = invoice.amount_due;
if (amountInCents < this.stripeManager.getMinimumAmount(invoice.currency)) {
if (
amountInCents <
this.subscriptionManager.getMinimumAmount(invoice.currency)
) {
await this.processZeroInvoice(invoice.id);
return;
}
const customer = await this.stripeManager.fetchActiveCustomer(
invoice.customer
);
const customer = await this.customerManager.retrieve(invoice.customer);
await this.processNonZeroInvoice(customer, invoice);
}

Просмотреть файл

@ -36,7 +36,12 @@ export * from './lib/stripe.config';
export * from './lib/stripe.constants';
export * from './lib/stripe.error';
export * from './lib/stripe.factories';
export * from './lib/stripe.manager';
export * from './lib/customer.manager';
export * from './lib/invoice.manager';
export * from './lib/price.manager';
export * from './lib/product.manager';
export * from './lib/promotionCode.manager';
export * from './lib/subscription.manager';
export * from './lib/stripe.service';
export * from './lib/stripe.types';
export * from './lib/stripe.util';

Просмотреть файл

@ -0,0 +1,232 @@
/* 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 { Test } from '@nestjs/testing';
import { StripeResponseFactory } from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { TaxAddressFactory } from './factories/tax-address.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { CustomerManager } from './customer.manager';
import { MOZILLA_TAX_ID } from './stripe.constants';
describe('CustomerManager', () => {
let customerManager: CustomerManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, CustomerManager],
}).compile();
customerManager = module.get(CustomerManager);
stripeClient = module.get(StripeClient);
});
describe('retrieve', () => {
it('returns an existing, non-deleted customer from Stripe', async () => {
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValueOnce(mockCustomer);
const result = await customerManager.retrieve(mockCustomer.id);
expect(result).toEqual(mockCustomer);
});
});
describe('update', () => {
it('should update an existing customer from Stripe', async () => {
const mockCustomer = StripeResponseFactory(
StripeCustomerFactory({
address: {
city: faker.location.city(),
country: faker.location.countryCode(),
line1: faker.location.streetAddress(),
line2: '',
postal_code: faker.location.zipCode(),
state: faker.location.state(),
},
})
);
jest
.spyOn(stripeClient, 'customersUpdate')
.mockResolvedValue(mockCustomer);
const result = await customerManager.update(mockCustomer.id, {
address: {
city: faker.location.city(),
country: faker.location.countryCode(),
line1: faker.location.streetAddress(),
line2: '',
postal_code: faker.location.zipCode(),
state: faker.location.state(),
},
});
expect(result).toEqual(mockCustomer);
});
});
describe('create', () => {
it('creates a customer within Stripe', async () => {
const taxAddress = TaxAddressFactory();
const mockCustomer = StripeResponseFactory(
StripeCustomerFactory({
name: faker.person.fullName(),
shipping: {
name: '',
address: {
city: faker.location.city(),
country: taxAddress.countryCode,
line1: faker.location.streetAddress(),
line2: '',
postal_code: taxAddress.postalCode,
state: faker.location.state(),
},
},
})
);
jest
.spyOn(stripeClient, 'customersCreate')
.mockResolvedValue(mockCustomer);
const result = await customerManager.create({
uid: faker.string.uuid(),
email: faker.internet.email(),
displayName: faker.person.fullName(),
taxAddress: taxAddress,
});
expect(result).toEqual(mockCustomer);
});
});
describe('isTaxEligible', () => {
it('should return true for a taxable customer', async () => {
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'supported',
ip_address: null,
location: { country: 'US', state: 'CA', source: 'billing_address' },
},
});
const result = customerManager.isTaxEligible(mockCustomer);
expect(result).toEqual(true);
});
it('should return true for a customer in a not-collecting location', async () => {
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'not_collecting',
ip_address: null,
location: null,
},
});
const result = customerManager.isTaxEligible(mockCustomer);
expect(result).toEqual(true);
});
});
describe('getTaxId', () => {
it('returns customer tax id if found', async () => {
const mockTaxIdValue = faker.string.uuid();
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: mockTaxIdValue }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
});
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
const result = await customerManager.getTaxId(mockCustomer.id);
expect(result).toEqual(mockTaxIdValue);
});
it('returns undefined when customer tax id not found', async () => {
const mockCustomer = StripeCustomerFactory();
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
const result = await customerManager.getTaxId(mockCustomer.id);
expect(result).toBeUndefined();
});
});
describe('setTaxId', () => {
it('updates customer object with incoming tax id when match is not found', async () => {
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
const mockTaxId = 'EU1234';
const mockUpdatedCustomer = StripeResponseFactory(
StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: mockTaxId }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
})
);
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(mockCustomer);
jest
.spyOn(stripeClient, 'customersUpdate')
.mockResolvedValue(mockUpdatedCustomer);
await customerManager.setTaxId(mockCustomer.id, mockTaxId);
expect(stripeClient.customersUpdate).toHaveBeenCalledWith(
mockCustomer.id,
{
invoice_settings: {
custom_fields: [{ name: MOZILLA_TAX_ID, value: mockTaxId }],
},
}
);
});
it('does not update customer object when incoming tax id matches existing tax id', async () => {
const mockTaxId = 'T43CAK315A713';
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: mockTaxId }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
});
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
jest
.spyOn(stripeClient, 'customersUpdate')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
await customerManager.setTaxId(mockCustomer.id, mockTaxId);
expect(stripeClient.customersUpdate).not.toHaveBeenCalled();
});
});
});

Просмотреть файл

@ -0,0 +1,99 @@
/* 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 { Injectable } from '@nestjs/common';
import { Stripe } from 'stripe';
import { StripeClient } from './stripe.client';
import { MOZILLA_TAX_ID } from './stripe.constants';
import { CustomerDeletedError } from './stripe.error';
import { TaxAddress } from './stripe.types';
import { StripeCustomer } from './stripe.client.types';
import { isCustomerTaxEligible } from './util/isCustomerTaxEligible';
@Injectable()
export class CustomerManager {
constructor(private client: StripeClient) {}
/**
* Retrieves a customer record
*/
async retrieve(customerId: string) {
const customer = await this.client.customersRetrieve(customerId);
if (customer.deleted) throw new CustomerDeletedError();
return customer;
}
/**
* Updates a customer record
*/
update(customerId: string, params?: Stripe.CustomerUpdateParams) {
return this.client.customersUpdate(customerId, params);
}
/**
* Create a customer
*/
async create(args: {
uid: string;
email: string;
displayName: string;
taxAddress?: TaxAddress;
}) {
const { uid, email, displayName, taxAddress } = args;
const shipping = taxAddress
? {
name: email,
address: {
country: taxAddress.countryCode,
postal_code: taxAddress.postalCode,
},
}
: undefined;
const customer = await this.client.customersCreate({
email,
name: displayName || '',
description: uid,
metadata: {
userid: uid,
geoip_date: taxAddress ? new Date().toString() : null,
},
shipping,
});
return customer;
}
async setTaxId(customerId: string, taxId: string) {
const customerTaxId = await this.getTaxId(customerId);
if (!customerTaxId || customerTaxId !== taxId) {
await this.client.customersUpdate(customerId, {
invoice_settings: {
custom_fields: [{ name: MOZILLA_TAX_ID, value: taxId }],
},
});
}
return;
}
async getTaxId(customerId: string) {
const customer = await this.retrieve(customerId);
const customFields = customer.invoice_settings.custom_fields || [];
const taxIdFields = customFields.filter((customField: any) => {
return customField.name === MOZILLA_TAX_ID;
});
return taxIdFields.at(0)?.value;
}
isTaxEligible(customer: StripeCustomer) {
return isCustomerTaxEligible(customer);
}
}

Просмотреть файл

@ -0,0 +1,86 @@
/* 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/. */
const mockStripeUtil = {
stripeInvoiceToFirstInvoicePreviewDTO: jest.fn(),
};
jest.mock(
'../../../stripe/src/lib/util/stripeInvoiceToFirstInvoicePreviewDTO',
() => mockStripeUtil
);
import { Test } from '@nestjs/testing';
import { StripeResponseFactory } from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripePriceFactory } from './factories/price.factory';
import { StripeUpcomingInvoiceFactory } from './factories/upcoming-invoice.factory';
import { TaxAddressFactory } from './factories/tax-address.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { InvoicePreviewFactory } from './stripe.factories';
import { InvoiceManager } from './invoice.manager';
describe('InvoiceManager', () => {
let invoiceManager: InvoiceManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, InvoiceManager],
}).compile();
invoiceManager = module.get(InvoiceManager);
stripeClient = module.get(StripeClient);
});
describe('finalizeWithoutAutoAdvance', () => {
it('works successfully', async () => {
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
auto_advance: false,
})
);
jest
.spyOn(stripeClient, 'invoicesFinalizeInvoice')
.mockResolvedValue(mockInvoice);
const result = await invoiceManager.finalizeWithoutAutoAdvance(
mockInvoice.id
);
expect(result).toEqual(mockInvoice);
});
});
describe('preview', () => {
it('returns upcoming invoice', async () => {
const mockCustomer = StripeCustomerFactory();
const mockPrice = StripePriceFactory();
const mockUpcomingInvoice = StripeResponseFactory(
StripeUpcomingInvoiceFactory()
);
const mockTaxAddress = TaxAddressFactory();
const mockInvoicePreview = InvoicePreviewFactory();
jest
.spyOn(stripeClient, 'invoicesRetrieveUpcoming')
.mockResolvedValue(mockUpcomingInvoice);
mockStripeUtil.stripeInvoiceToFirstInvoicePreviewDTO.mockReturnValue(
mockInvoicePreview
);
const result = await invoiceManager.preview({
priceId: mockPrice.id,
customer: mockCustomer,
taxAddress: mockTaxAddress,
});
expect(result).toEqual(mockInvoicePreview);
});
});
});

Просмотреть файл

@ -0,0 +1,67 @@
/* 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 { Injectable } from '@nestjs/common';
import { Stripe } from 'stripe';
import { StripeClient } from './stripe.client';
import { StripeCustomer } from './stripe.client.types';
import { TaxAddress } from './stripe.types';
import { stripeInvoiceToFirstInvoicePreviewDTO } from './util/stripeInvoiceToFirstInvoicePreviewDTO';
import { isCustomerTaxEligible } from './util/isCustomerTaxEligible';
@Injectable()
export class InvoiceManager {
constructor(private client: StripeClient) {}
async finalizeWithoutAutoAdvance(invoiceId: string) {
return this.client.invoicesFinalizeInvoice(invoiceId, {
auto_advance: false,
});
}
async preview({
priceId,
customer,
taxAddress,
}: {
priceId: string;
customer?: StripeCustomer;
taxAddress?: TaxAddress;
}) {
const automaticTax = !!(
(customer && isCustomerTaxEligible(customer)) ||
(!customer && taxAddress)
);
const shipping =
!customer && taxAddress
? {
name: '',
address: {
country: taxAddress.countryCode,
postal_code: taxAddress.postalCode,
},
}
: undefined;
const requestObject: Stripe.InvoiceRetrieveUpcomingParams = {
customer: customer?.id,
automatic_tax: {
enabled: automaticTax,
},
customer_details: {
tax_exempt: 'none', // Param required when shipping address not present
shipping,
},
subscription_items: [{ price: priceId }],
};
const upcomingInvoice = await this.client.invoicesRetrieveUpcoming(
requestObject
);
return stripeInvoiceToFirstInvoicePreviewDTO(upcomingInvoice);
}
}

Просмотреть файл

@ -0,0 +1,82 @@
/* 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 { Test } from '@nestjs/testing';
import { StripeResponseFactory } from './factories/api-list.factory';
import { StripePlanFactory } from './factories/plan.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { PlanIntervalMultiplePlansError } from './stripe.error';
import { PriceManager } from './price.manager';
import { SubplatInterval } from './stripe.types';
describe('PriceManager', () => {
let priceManager: PriceManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, PriceManager],
}).compile();
priceManager = module.get(PriceManager);
stripeClient = module.get(StripeClient);
});
describe('retrieve', () => {
it('returns plan', async () => {
const mockPlan = StripeResponseFactory(StripePlanFactory());
jest.spyOn(stripeClient, 'plansRetrieve').mockResolvedValue(mockPlan);
const result = await priceManager.retrieve(mockPlan.id);
expect(result).toEqual(mockPlan);
});
});
describe('getPlanByInterval', () => {
it('returns plan that matches interval', async () => {
const mockPlan = StripeResponseFactory(
StripePlanFactory({
interval: 'month',
interval_count: 1,
})
);
const subplatInterval = SubplatInterval.Monthly;
jest.spyOn(priceManager, 'retrieve').mockResolvedValue(mockPlan);
const result = await priceManager.retrieveByInterval(
[mockPlan.id],
subplatInterval
);
expect(result).toEqual(mockPlan);
});
it('throw error if interval returns multiple plans', async () => {
const mockPlan1 = StripePlanFactory({
interval: 'month',
});
const mockPlan2 = StripePlanFactory({
interval: 'month',
});
const subplatInterval = SubplatInterval.Monthly;
jest
.spyOn(priceManager, 'retrieve')
.mockResolvedValue(StripeResponseFactory(mockPlan1));
jest
.spyOn(priceManager, 'retrieve')
.mockResolvedValue(StripeResponseFactory(mockPlan2));
await expect(
priceManager.retrieveByInterval(
[mockPlan1.id, mockPlan2.id],
subplatInterval
)
).rejects.toBeInstanceOf(PlanIntervalMultiplePlansError);
});
});
});

Просмотреть файл

@ -0,0 +1,37 @@
/* 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 { Injectable } from '@nestjs/common';
import { StripeClient } from './stripe.client';
import { StripePlan } from './stripe.client.types';
import {
PlanIntervalMultiplePlansError,
PlanNotFoundError,
} from './stripe.error';
import { SubplatInterval } from './stripe.types';
import { doesPlanMatchSubplatInterval } from './util/doesPlanMatchSubplatInterval';
@Injectable()
export class PriceManager {
constructor(private client: StripeClient) {}
async retrieve(planId: string) {
const plan = await this.client.plansRetrieve(planId);
if (!plan) throw new PlanNotFoundError();
return plan;
}
async retrieveByInterval(planIds: string[], interval: SubplatInterval) {
const plans: StripePlan[] = [];
for (const planId of planIds) {
const plan = await this.retrieve(planId);
if (doesPlanMatchSubplatInterval(plan, interval)) {
plans.push(plan);
}
}
if (plans.length > 1) throw new PlanIntervalMultiplePlansError();
return plans.at(0);
}
}

Просмотреть файл

@ -0,0 +1,38 @@
/* 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 { Test } from '@nestjs/testing';
import { StripeResponseFactory } from './factories/api-list.factory';
import { StripeProductFactory } from './factories/product.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { ProductManager } from './product.manager';
describe('ProductManager', () => {
let productManager: ProductManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, ProductManager],
}).compile();
productManager = module.get(ProductManager);
stripeClient = module.get(StripeClient);
});
describe('retrieve', () => {
it('returns product', async () => {
const mockProduct = StripeResponseFactory(StripeProductFactory());
jest
.spyOn(stripeClient, 'productsRetrieve')
.mockResolvedValue(mockProduct);
const result = await productManager.retrieve(mockProduct.id);
expect(result).toEqual(mockProduct);
});
});
});

Просмотреть файл

@ -0,0 +1,19 @@
/* 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 { Injectable } from '@nestjs/common';
import { StripeClient } from './stripe.client';
import { ProductNotFoundError } from './stripe.error';
@Injectable()
export class ProductManager {
constructor(private client: StripeClient) {}
async retrieve(productId: string) {
const product = await this.client.productsRetrieve(productId);
if (!product) throw new ProductNotFoundError();
return product;
}
}

Просмотреть файл

@ -0,0 +1,62 @@
/* 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 { Test } from '@nestjs/testing';
import {
StripeApiListFactory,
StripeResponseFactory,
} from './factories/api-list.factory';
import { StripePromotionCodeFactory } from './factories/promotion-code.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { PromotionCodeManager } from './promotionCode.manager';
describe('PromotionCodeManager', () => {
let promotionCodeManager: PromotionCodeManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, PromotionCodeManager],
}).compile();
promotionCodeManager = module.get(PromotionCodeManager);
stripeClient = module.get(StripeClient);
});
describe('retrieve', () => {
it('retrieves promotion code', async () => {
const mockPromotionCode = StripePromotionCodeFactory();
const mockResponse = StripeResponseFactory(mockPromotionCode);
jest
.spyOn(stripeClient, 'promotionCodesRetrieve')
.mockResolvedValue(mockResponse);
const result = await promotionCodeManager.retrieve(mockPromotionCode.id);
expect(result).toEqual(mockResponse);
});
});
describe('retrieveByName', () => {
it('queries for promotionCodes from stripe and returns first', async () => {
const mockPromotionCode = StripePromotionCodeFactory();
const mockPromotionCode2 = StripePromotionCodeFactory();
const mockPromotionCodesResponse = StripeApiListFactory([
mockPromotionCode,
mockPromotionCode2,
]);
jest
.spyOn(stripeClient, 'promotionCodesList')
.mockResolvedValue(StripeResponseFactory(mockPromotionCodesResponse));
const result = await promotionCodeManager.retrieveByName(
mockPromotionCode.code
);
expect(result).toEqual(mockPromotionCode);
});
});
});

Просмотреть файл

@ -0,0 +1,25 @@
/* 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 { Injectable } from '@nestjs/common';
import { StripeClient } from './stripe.client';
@Injectable()
export class PromotionCodeManager {
constructor(private client: StripeClient) {}
async retrieve(id: string) {
return this.client.promotionCodesRetrieve(id);
}
async retrieveByName(code: string, active?: boolean) {
const promotionCodes = await this.client.promotionCodesList({
active,
code,
});
return promotionCodes.data.at(0);
}
}

Просмотреть файл

@ -1,604 +0,0 @@
/* 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/. */
const mockStripeUtil = {
stripeInvoiceToFirstInvoicePreviewDTO: jest.fn(),
};
jest.mock(
'../../../stripe/src/lib/util/stripeInvoiceToFirstInvoicePreviewDTO',
() => mockStripeUtil
);
import { faker } from '@faker-js/faker';
import { Test } from '@nestjs/testing';
import {
StripeApiListFactory,
StripeResponseFactory,
} from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripePaymentIntentFactory } from './factories/payment-intent.factory';
import { StripePlanFactory } from './factories/plan.factory';
import { StripePriceFactory } from './factories/price.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 { TaxAddressFactory } from './factories/tax-address.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { PlanIntervalMultiplePlansError } from './stripe.error';
import { InvoicePreviewFactory } from './stripe.factories';
import { StripeManager } from './stripe.manager';
import { SubplatInterval } from './stripe.types';
describe('StripeManager', () => {
let stripeManager: StripeManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, StripeManager],
}).compile();
stripeManager = module.get(StripeManager);
stripeClient = module.get(StripeClient);
});
describe('fetchActiveCustomer', () => {
it('returns an existing customer from Stripe', async () => {
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValueOnce(mockCustomer);
const result = await stripeManager.fetchActiveCustomer(mockCustomer.id);
expect(result).toEqual(mockCustomer);
});
});
describe('updateCustomer', () => {
it('should update an existing customer from Stripe', async () => {
const mockCustomer = StripeResponseFactory(
StripeCustomerFactory({
address: {
city: faker.location.city(),
country: faker.location.countryCode(),
line1: faker.location.streetAddress(),
line2: '',
postal_code: faker.location.zipCode(),
state: faker.location.state(),
},
})
);
jest
.spyOn(stripeClient, 'customersUpdate')
.mockResolvedValue(mockCustomer);
const result = await stripeManager.updateCustomer(mockCustomer.id, {
address: {
city: faker.location.city(),
country: faker.location.countryCode(),
line1: faker.location.streetAddress(),
line2: '',
postal_code: faker.location.zipCode(),
state: faker.location.state(),
},
});
expect(result).toEqual(mockCustomer);
});
});
describe('createPlainCustomer', () => {
it('creates a plain customer from Stripe', async () => {
const taxAddress = TaxAddressFactory();
const mockCustomer = StripeResponseFactory(
StripeCustomerFactory({
name: faker.person.fullName(),
shipping: {
name: '',
address: {
city: faker.location.city(),
country: taxAddress.countryCode,
line1: faker.location.streetAddress(),
line2: '',
postal_code: taxAddress.postalCode,
state: faker.location.state(),
},
},
})
);
jest
.spyOn(stripeClient, 'customersCreate')
.mockResolvedValue(mockCustomer);
const result = await stripeManager.createPlainCustomer({
uid: faker.string.uuid(),
email: faker.internet.email(),
displayName: faker.person.fullName(),
taxAddress: taxAddress,
});
expect(result).toEqual(mockCustomer);
});
});
describe('finalizeInvoiceWithoutAutoAdvance', () => {
it('works successfully', async () => {
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
auto_advance: false,
})
);
jest
.spyOn(stripeClient, 'invoicesFinalizeInvoice')
.mockResolvedValue(mockInvoice);
const result = await stripeManager.finalizeInvoiceWithoutAutoAdvance(
mockInvoice.id
);
expect(result).toEqual(mockInvoice);
});
});
describe('previewInvoice', () => {
it('returns upcoming invoice', async () => {
const mockCustomer = StripeCustomerFactory();
const mockPrice = StripePriceFactory();
const mockUpcomingInvoice = StripeResponseFactory(
StripeUpcomingInvoiceFactory()
);
const mockTaxAddress = TaxAddressFactory();
const mockInvoicePreview = InvoicePreviewFactory();
jest
.spyOn(stripeClient, 'invoicesRetrieveUpcoming')
.mockResolvedValue(mockUpcomingInvoice);
mockStripeUtil.stripeInvoiceToFirstInvoicePreviewDTO.mockReturnValue(
mockInvoicePreview
);
const result = await stripeManager.previewInvoice({
priceId: mockPrice.id,
customer: mockCustomer,
taxAddress: mockTaxAddress,
});
expect(result).toEqual(mockInvoicePreview);
});
});
describe('getMinimumAmount', () => {
it('returns minimum amout for valid currency', () => {
const expected = 50;
const result = stripeManager.getMinimumAmount('usd');
expect(result).toEqual(expected);
});
it('should throw an error if currency is invalid', () => {
expect(() => stripeManager.getMinimumAmount('fake')).toThrow(
'Currency does not have a minimum charge amount available.'
);
});
});
describe('getTaxIdForCurrency', () => {
it('returns the correct tax id for currency', async () => {
const mockCurrency = 'eur';
const result = stripeManager.getTaxIdForCurrency(mockCurrency);
expect(result).toEqual('EU1234');
});
it('returns empty string when no tax id found', async () => {
const mockCurrency = faker.finance.currencyCode();
const result = stripeManager.getTaxIdForCurrency(mockCurrency);
expect(result).toEqual(undefined);
});
});
describe('cancelIncompleteSubscriptionsToPrice', () => {
it('cancels incomplete subscriptions', async () => {
const mockCustomer = StripeCustomerFactory();
const mockSubscription = StripeSubscriptionFactory({
status: 'incomplete',
});
const mockSubscriptionList = [mockSubscription];
const mockPrice = mockSubscription.items.data[0].price;
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeManager, 'getSubscriptions')
.mockResolvedValue(mockSubscriptionList);
jest
.spyOn(stripeClient, 'subscriptionsCancel')
.mockResolvedValue(mockResponse);
await stripeManager.cancelIncompleteSubscriptionsToPrice(
mockCustomer.id,
mockPrice.id
);
expect(stripeClient.subscriptionsCancel).toBeCalledWith(
mockSubscription.id
);
});
});
describe('getSubscriptions', () => {
it('returns subscriptions', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockSubscriptionList = StripeApiListFactory([mockSubscription]);
const mockCustomer = StripeCustomerFactory();
const expected = mockSubscriptionList.data;
jest
.spyOn(stripeClient, 'subscriptionsList')
.mockResolvedValue(mockSubscriptionList);
const result = await stripeManager.getSubscriptions(mockCustomer.id);
expect(result).toEqual(expected);
});
it('returns empty array if no subscriptions exist', async () => {
const mockCustomer = StripeCustomerFactory();
jest
.spyOn(stripeClient, 'subscriptionsList')
.mockResolvedValue(StripeApiListFactory([]));
const result = await stripeManager.getSubscriptions(mockCustomer.id);
expect(result).toEqual([]);
});
});
describe('cancelSubscription', () => {
it('calls stripeclient', async () => {
const mockSubscription = StripeSubscriptionFactory();
jest
.spyOn(stripeClient, 'subscriptionsCancel')
.mockResolvedValue(StripeResponseFactory(mockSubscription));
await stripeManager.cancelSubscription(mockSubscription.id);
expect(stripeClient.subscriptionsCancel).toBeCalledWith(
mockSubscription.id
);
});
});
describe('retrieveSubscription', () => {
it('calls stripeclient', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeClient, 'subscriptionsRetrieve')
.mockResolvedValue(mockResponse);
const result = await stripeManager.retrieveSubscription(
mockSubscription.id
);
expect(stripeClient.subscriptionsRetrieve).toBeCalledWith(
mockSubscription.id
);
expect(result).toEqual(mockResponse);
});
});
describe('updateSubscription', () => {
it('calls stripeclient', async () => {
const mockParams = {
description: 'This is an updated subscription',
};
const mockSubscription = StripeSubscriptionFactory(mockParams);
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeClient, 'subscriptionsUpdate')
.mockResolvedValue(mockResponse);
const result = await stripeManager.updateSubscription(
mockSubscription.id,
mockParams
);
expect(stripeClient.subscriptionsUpdate).toBeCalledWith(
mockSubscription.id,
mockParams
);
expect(result).toEqual(mockResponse);
});
});
describe('isCustomerStripeTaxEligible', () => {
it('should return true for a taxable customer', async () => {
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'supported',
ip_address: null,
location: { country: 'US', state: 'CA', source: 'billing_address' },
},
});
const result = stripeManager.isCustomerStripeTaxEligible(mockCustomer);
expect(result).toEqual(true);
});
it('should return true for a customer in a not-collecting location', async () => {
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'not_collecting',
ip_address: null,
location: null,
},
});
const result = stripeManager.isCustomerStripeTaxEligible(mockCustomer);
expect(result).toEqual(true);
});
});
describe('getPromotionCodeByName', () => {
it('queries for promotionCodes from stripe and returns first', async () => {
const mockPromotionCode = StripePromotionCodeFactory();
const mockPromotionCode2 = StripePromotionCodeFactory();
const mockPromotionCodesResponse = StripeApiListFactory([
mockPromotionCode,
mockPromotionCode2,
]);
jest
.spyOn(stripeClient, 'promotionCodesList')
.mockResolvedValue(StripeResponseFactory(mockPromotionCodesResponse));
const result = await stripeManager.getPromotionCodeByName(
mockPromotionCode.code
);
expect(result).toEqual(mockPromotionCode);
});
});
describe('retrievePromotionCode', () => {
it('retrieves promotion code', async () => {
const mockPromotionCode = StripePromotionCodeFactory();
const mockResponse = StripeResponseFactory(mockPromotionCode);
jest
.spyOn(stripeClient, 'promotionCodesRetrieve')
.mockResolvedValue(mockResponse);
const result = await stripeManager.retrievePromotionCode(
mockPromotionCode.id
);
expect(result).toEqual(mockResponse);
});
});
describe('getCustomerTaxId', () => {
it('returns customer tax id if found', async () => {
const mockTaxIdValue = faker.string.uuid();
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: mockTaxIdValue }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
});
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
const result = await stripeManager.getCustomerTaxId(mockCustomer.id);
expect(result).toEqual(mockTaxIdValue);
});
it('returns undefined when customer tax id not found', async () => {
const mockCustomer = StripeCustomerFactory();
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
const result = await stripeManager.getCustomerTaxId(mockCustomer.id);
expect(result).toBeUndefined();
});
});
describe('setCustomerTaxId', () => {
it('updates customer object with incoming tax id when match is not found', async () => {
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
const mockUpdatedCustomer = StripeResponseFactory(
StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: 'EU1234' }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
})
);
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(mockCustomer);
jest
.spyOn(stripeClient, 'customersUpdate')
.mockResolvedValue(mockUpdatedCustomer);
const result = await stripeManager.setCustomerTaxId(
mockCustomer.id,
'EU1234'
);
expect(result).toEqual(mockUpdatedCustomer);
});
it('does not update customer object when incoming tax id matches existing tax id', async () => {
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: 'T43CAK315A713' }],
default_payment_method: null,
footer: null,
rendering_options: null,
},
});
jest
.spyOn(stripeClient, 'customersRetrieve')
.mockResolvedValue(StripeResponseFactory(mockCustomer));
const result = await stripeManager.setCustomerTaxId(
mockCustomer.id,
'T43CAK315A713'
);
expect(result).toBeUndefined();
});
});
describe('getPlan', () => {
it('returns plan', async () => {
const mockPlan = StripeResponseFactory(StripePlanFactory());
jest.spyOn(stripeClient, 'plansRetrieve').mockResolvedValue(mockPlan);
const result = await stripeManager.getPlan(mockPlan.id);
expect(result).toEqual(mockPlan);
});
});
describe('getPlanByInterval', () => {
it('returns plan that matches interval', async () => {
const mockPlan = StripeResponseFactory(
StripePlanFactory({
interval: 'month',
interval_count: 1,
})
);
const subplatInterval = SubplatInterval.Monthly;
jest.spyOn(stripeManager, 'getPlan').mockResolvedValue(mockPlan);
const result = await stripeManager.getPlanByInterval(
[mockPlan.id],
subplatInterval
);
expect(result).toEqual(mockPlan);
});
it('throw error if interval returns multiple plans', async () => {
const mockPlan1 = StripePlanFactory({
interval: 'month',
});
const mockPlan2 = StripePlanFactory({
interval: 'month',
});
const subplatInterval = SubplatInterval.Monthly;
jest
.spyOn(stripeManager, 'getPlan')
.mockResolvedValue(StripeResponseFactory(mockPlan1));
jest
.spyOn(stripeManager, 'getPlan')
.mockResolvedValue(StripeResponseFactory(mockPlan2));
await expect(
stripeManager.getPlanByInterval(
[mockPlan1.id, mockPlan2.id],
subplatInterval
)
).rejects.toBeInstanceOf(PlanIntervalMultiplePlansError);
});
});
describe('retrieveProduct', () => {
it('returns product', async () => {
const mockProduct = StripeResponseFactory(StripeProductFactory());
jest
.spyOn(stripeClient, 'productsRetrieve')
.mockResolvedValue(mockProduct);
const result = await stripeManager.retrieveProduct(mockProduct.id);
expect(result).toEqual(mockProduct);
});
});
describe('getLatestPaymentIntent', () => {
it('fetches the latest payment intent for the subscription', async () => {
const mockSubscription = StripeResponseFactory(
StripeSubscriptionFactory()
);
const mockInvoice = StripeResponseFactory(StripeInvoiceFactory());
const mockPaymentIntent = StripeResponseFactory(
StripePaymentIntentFactory()
);
jest
.spyOn(stripeClient, 'invoicesRetrieve')
.mockResolvedValue(mockInvoice);
jest
.spyOn(stripeClient, 'paymentIntentRetrieve')
.mockResolvedValue(mockPaymentIntent);
const result = await stripeManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(mockPaymentIntent);
});
it('returns undefined if no invoice on subscription', async () => {
const mockSubscription = StripeSubscriptionFactory({
latest_invoice: null,
});
const result = await stripeManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(undefined);
});
it('returns undefined if the invoice has no payment intent', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
})
);
jest
.spyOn(stripeClient, 'invoicesRetrieve')
.mockResolvedValue(mockInvoice);
const result = await stripeManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(undefined);
});
});
});

Просмотреть файл

@ -1,321 +0,0 @@
/* 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 { Injectable } from '@nestjs/common';
import { Stripe } from 'stripe';
import { StripeClient } from './stripe.client';
import {
StripeCustomer,
StripePlan,
StripeSubscription,
} from './stripe.client.types';
import { StripeConfig } from './stripe.config';
import {
MOZILLA_TAX_ID,
STRIPE_MINIMUM_CHARGE_AMOUNTS,
} from './stripe.constants';
import {
CustomerDeletedError,
CustomerNotFoundError,
PlanIntervalMultiplePlansError,
PlanNotFoundError,
ProductNotFoundError,
StripeNoMinimumChargeAmountAvailableError,
} from './stripe.error';
import { SubplatInterval, TaxAddress } from './stripe.types';
import { doesPlanMatchSubplatInterval } from './util/doesPlanMatchSubplatInterval';
import { stripeInvoiceToFirstInvoicePreviewDTO } from './util/stripeInvoiceToFirstInvoicePreviewDTO';
@Injectable()
export class StripeManager {
private taxIds: { [key: string]: string };
constructor(private client: StripeClient, private config: StripeConfig) {
this.taxIds = this.config.taxIds;
}
/**
* Returns minimum amount for valid currency
* Throws error for invalid currency
*/
getMinimumAmount(currency: string): number {
if (STRIPE_MINIMUM_CHARGE_AMOUNTS[currency]) {
return STRIPE_MINIMUM_CHARGE_AMOUNTS[currency];
}
throw new StripeNoMinimumChargeAmountAvailableError();
}
/**
* Returns the correct tax id for currency
*/
getTaxIdForCurrency(currency: string) {
return this.taxIds[currency.toUpperCase()];
}
/**
* Retrieves a customer record
*/
async fetchActiveCustomer(customerId: string) {
const customer = await this.client.customersRetrieve(customerId);
if (customer.deleted) throw new CustomerDeletedError();
return customer;
}
/**
* Updates a customer record
*/
async updateCustomer(
customerId: string,
params?: Stripe.CustomerUpdateParams
) {
return await this.client.customersUpdate(customerId, params);
}
/**
* Create customer stub account
*/
async createPlainCustomer(args: {
uid: string;
email: string;
displayName: string;
taxAddress?: TaxAddress;
}) {
const { uid, email, displayName, taxAddress } = args;
const shipping = taxAddress
? {
name: email,
address: {
country: taxAddress.countryCode,
postal_code: taxAddress.postalCode,
},
}
: undefined;
const stripeCustomer = await this.client.customersCreate({
email,
name: displayName || '',
description: uid,
metadata: {
userid: uid,
geoip_date: new Date().toString(),
},
shipping,
});
return stripeCustomer;
}
/**
* Finalizes an invoice and marks auto_advance as false.
*/
async finalizeInvoiceWithoutAutoAdvance(invoiceId: string) {
return this.client.invoicesFinalizeInvoice(invoiceId, {
auto_advance: false,
});
}
/**
* Returns upcoming invoice
*/
async previewInvoice({
priceId,
customer,
taxAddress,
}: {
priceId: string;
customer?: StripeCustomer;
taxAddress?: TaxAddress;
}) {
const automaticTax = !!(
(customer && this.isCustomerStripeTaxEligible(customer)) ||
(!customer && taxAddress)
);
const shipping =
!customer && taxAddress
? {
name: '',
address: {
country: taxAddress.countryCode,
postal_code: taxAddress.postalCode,
},
}
: undefined;
const requestObject: Stripe.InvoiceRetrieveUpcomingParams = {
customer: customer?.id,
automatic_tax: {
enabled: automaticTax,
},
customer_details: {
tax_exempt: 'none', // Param required when shipping address not present
shipping,
},
subscription_items: [{ price: priceId }],
};
const upcomingInvoice = await this.client.invoicesRetrieveUpcoming(
requestObject
);
return stripeInvoiceToFirstInvoicePreviewDTO(upcomingInvoice);
}
/**
* Cancels incomplete subscription
*/
async cancelIncompleteSubscriptionsToPrice(
customerId: string,
priceId: string
) {
const subscriptions = await this.getSubscriptions(customerId);
const targetSubs = subscriptions.filter((sub) =>
sub.items.data.find((item) => item.price.id === priceId)
);
for (const sub of targetSubs) {
if (sub && sub.status === 'incomplete') {
await this.client.subscriptionsCancel(sub.id);
}
}
}
/**
* Retrieves subscriptions
*/
async getSubscriptions(customerId: string) {
const result = await this.client.subscriptionsList({
customer: customerId,
});
return result.data;
}
async cancelSubscription(subscriptionId: string) {
return this.client.subscriptionsCancel(subscriptionId);
}
async retrieveSubscription(subscriptionId: string) {
return this.client.subscriptionsRetrieve(subscriptionId);
}
async updateSubscription(
subscriptionId: string,
params?: Stripe.SubscriptionUpdateParams
) {
return this.client.subscriptionsUpdate(subscriptionId, params);
}
/**
* Check if customer's automatic tax status indicates that they're eligible for automatic tax.
* Creating a subscription with automatic_tax enabled requires a customer with an address
* that is in a recognized location with an active tax registration.
*/
isCustomerStripeTaxEligible(customer: StripeCustomer) {
return (
customer.tax.automatic_tax === 'supported' ||
customer.tax.automatic_tax === 'not_collecting'
);
}
async getPromotionCodeByName(code: string, active?: boolean) {
const promotionCodes = await this.client.promotionCodesList({
active,
code,
});
return promotionCodes.data.at(0);
}
async retrievePromotionCode(id: string) {
return this.client.promotionCodesRetrieve(id);
}
/**
* Updates customer object with incoming tax ID if existing tax ID does not match
*
* @param customerId Customer ID of customer to be updated
* @param taxId Customer tax ID to be updated if different from existing tax ID
* @returns True if the customer was updated, false otherwise
*/
async setCustomerTaxId(customerId: string, taxId: string) {
const customerTaxId = await this.getCustomerTaxId(customerId);
if (!customerTaxId || customerTaxId !== taxId) {
return await this.client.customersUpdate(customerId, {
invoice_settings: {
custom_fields: [{ name: MOZILLA_TAX_ID, value: taxId }],
},
});
}
return;
}
/**
* Returns tax ID of customer
*
* @param customerId ID of customer to fetch and check existing tax ID
* @returns The tax ID of customer or undefined if not found
*/
async getCustomerTaxId(customerId: string) {
const customer = await this.client.customersRetrieve(customerId);
if (!customer) throw new CustomerNotFoundError();
if (customer.deleted) throw new CustomerDeletedError();
const customFields = customer.invoice_settings.custom_fields || [];
const taxIdFields = customFields.filter((customField: any) => {
return customField.name === MOZILLA_TAX_ID;
});
return taxIdFields.at(0)?.value;
}
async getPlan(planId: string) {
const plan = await this.client.plansRetrieve(planId);
if (!plan) throw new PlanNotFoundError();
return plan;
}
async getPlanByInterval(planIds: string[], interval: SubplatInterval) {
const plans: StripePlan[] = [];
for (const planId of planIds) {
const plan = await this.getPlan(planId);
if (doesPlanMatchSubplatInterval(plan, interval)) {
plans.push(plan);
}
}
if (plans.length > 1) throw new PlanIntervalMultiplePlansError();
return plans.at(0);
}
async retrieveProduct(productId: string) {
const product = await this.client.productsRetrieve(productId);
if (!product) throw new ProductNotFoundError();
return product;
}
async getLatestPaymentIntent(subscription: StripeSubscription) {
if (!subscription.latest_invoice) {
return;
}
const latestInvoice = await this.client.invoicesRetrieve(
subscription.latest_invoice
);
if (!latestInvoice.payment_intent) {
return;
}
const paymentIntent = await this.client.paymentIntentRetrieve(
latestInvoice.payment_intent
);
return paymentIntent;
}
}

Просмотреть файл

@ -30,25 +30,33 @@ import {
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { PromotionCodeCouldNotBeAttachedError } from './stripe.error';
import { StripeManager } from './stripe.manager';
import { ProductManager } from './product.manager';
import { StripeService } from './stripe.service';
import { STRIPE_PRICE_METADATA } from './stripe.types';
import { SubscriptionManager } from './subscription.manager';
import { PromotionCodeManager } from './promotionCode.manager';
describe('StripeService', () => {
let stripeService: StripeService;
let stripeManager: StripeManager;
let productManager: ProductManager;
let subscriptionManager: SubscriptionManager;
let promotionCodeManager: PromotionCodeManager;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
MockStripeConfigProvider,
StripeClient,
StripeManager,
ProductManager,
SubscriptionManager,
PromotionCodeManager,
StripeService,
],
}).compile();
stripeManager = module.get(StripeManager);
productManager = module.get(ProductManager);
subscriptionManager = module.get(SubscriptionManager);
promotionCodeManager = module.get(PromotionCodeManager);
stripeService = module.get(StripeService);
});
@ -62,7 +70,7 @@ describe('StripeService', () => {
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockResponse);
await expect(
@ -83,7 +91,7 @@ describe('StripeService', () => {
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockResponse);
await expect(
@ -106,11 +114,11 @@ describe('StripeService', () => {
const mockSubResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockSubResponse);
jest
.spyOn(stripeManager, 'retrievePromotionCode')
.spyOn(promotionCodeManager, 'retrieve')
.mockResolvedValue(mockPromoResponse);
mockStripeUtil.checkValidPromotionCode.mockImplementation(() => {
@ -139,11 +147,11 @@ describe('StripeService', () => {
const mockSubResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockSubResponse);
jest
.spyOn(stripeManager, 'retrievePromotionCode')
.spyOn(promotionCodeManager, 'retrieve')
.mockResolvedValue(mockPromoResponse);
mockStripeUtil.checkValidPromotionCode.mockReturnValue(true);
@ -177,18 +185,18 @@ describe('StripeService', () => {
const mockPromoResponse = StripeResponseFactory(mockPromotionCode);
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockSubResponse);
jest
.spyOn(stripeManager, 'retrievePromotionCode')
.spyOn(promotionCodeManager, 'retrieve')
.mockResolvedValue(mockPromoResponse);
mockStripeUtil.checkValidPromotionCode.mockReturnValue(true);
mockStripeUtil.getSubscribedPrice.mockReturnValue(mockPrice);
jest
.spyOn(stripeManager, 'retrieveProduct')
.spyOn(productManager, 'retrieve')
.mockResolvedValue(StripeResponseFactory(mockProduct));
mockStripeUtil.checkSubscriptionPromotionCodes.mockImplementation(() => {
@ -215,11 +223,9 @@ describe('StripeService', () => {
type: 'invalid_request_error',
});
jest
.spyOn(stripeManager, 'retrieveSubscription')
.mockImplementation(() => {
throw stripeError;
});
jest.spyOn(subscriptionManager, 'retrieve').mockImplementation(() => {
throw stripeError;
});
await expect(
stripeService.applyPromoCodeToSubscription(
@ -270,11 +276,11 @@ describe('StripeService', () => {
const mockProduct = StripeProductFactory();
jest
.spyOn(stripeManager, 'retrieveSubscription')
.spyOn(subscriptionManager, 'retrieve')
.mockResolvedValue(mockSubResponse1);
jest
.spyOn(stripeManager, 'retrievePromotionCode')
.spyOn(promotionCodeManager, 'retrieve')
.mockResolvedValue(mockPromoCodeResponse);
mockStripeUtil.checkValidPromotionCode.mockReturnValue(true);
@ -282,11 +288,11 @@ describe('StripeService', () => {
mockStripeUtil.checkSubscriptionPromotionCodes.mockReturnValue(true);
jest
.spyOn(stripeManager, 'retrieveProduct')
.spyOn(productManager, 'retrieve')
.mockResolvedValue(StripeResponseFactory(mockProduct));
jest
.spyOn(stripeManager, 'updateSubscription')
.spyOn(subscriptionManager, 'update')
.mockResolvedValue(mockSubResponse2);
const result = await stripeService.applyPromoCodeToSubscription(

Просмотреть файл

@ -4,24 +4,31 @@
import { Injectable } from '@nestjs/common';
import { PromotionCodeCouldNotBeAttachedError } from './stripe.error';
import { StripeManager } from './stripe.manager';
import {
checkSubscriptionPromotionCodes,
checkValidPromotionCode,
getSubscribedPrice,
} from './stripe.util';
import { SubscriptionManager } from './subscription.manager';
import { PromotionCodeManager } from './promotionCode.manager';
import { ProductManager } from './product.manager';
@Injectable()
export class StripeService {
constructor(private stripeManager: StripeManager) {}
constructor(
private productManager: ProductManager,
private subscriptionManager: SubscriptionManager,
private promotionCodeManager: PromotionCodeManager
) {}
// TODO: Remove this method & this service
async applyPromoCodeToSubscription(
customerId: string,
subscriptionId: string,
promotionId: string
) {
try {
const subscription = await this.stripeManager.retrieveSubscription(
const subscription = await this.subscriptionManager.retrieve(
subscriptionId
);
if (subscription?.status !== 'active')
@ -40,7 +47,7 @@ export class StripeService {
}
);
const promotionCode = await this.stripeManager.retrievePromotionCode(
const promotionCode = await this.promotionCodeManager.retrieve(
promotionId
);
@ -48,11 +55,11 @@ export class StripeService {
const price = getSubscribedPrice(subscription);
const productId = price.product;
const product = await this.stripeManager.retrieveProduct(productId);
const product = await this.productManager.retrieve(productId);
checkSubscriptionPromotionCodes(promotionCode, price, product);
const updatedSubscription = await this.stripeManager.updateSubscription(
const updatedSubscription = await this.subscriptionManager.update(
subscriptionId,
{
discounts: [

Просмотреть файл

@ -0,0 +1,219 @@
/* 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 { Test } from '@nestjs/testing';
import {
StripeApiListFactory,
StripeResponseFactory,
} from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripePaymentIntentFactory } from './factories/payment-intent.factory';
import { StripeSubscriptionFactory } from './factories/subscription.factory';
import { StripeClient } from './stripe.client';
import { MockStripeConfigProvider } from './stripe.config';
import { SubscriptionManager } from './subscription.manager';
describe('SubscriptionManager', () => {
let subscriptionManager: SubscriptionManager;
let stripeClient: StripeClient;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [MockStripeConfigProvider, StripeClient, SubscriptionManager],
}).compile();
subscriptionManager = module.get(SubscriptionManager);
stripeClient = module.get(StripeClient);
});
describe('getMinimumAmount', () => {
it('returns minimum amout for valid currency', () => {
const expected = 50;
const result = subscriptionManager.getMinimumAmount('usd');
expect(result).toEqual(expected);
});
it('should throw an error if currency is invalid', () => {
expect(() => subscriptionManager.getMinimumAmount('fake')).toThrow(
'Currency does not have a minimum charge amount available.'
);
});
});
describe('cancelIncompleteSubscriptionsToPrice', () => {
it('cancels incomplete subscriptions', async () => {
const mockCustomer = StripeCustomerFactory();
const mockSubscription = StripeSubscriptionFactory({
status: 'incomplete',
});
const mockSubscriptionList = [mockSubscription];
const mockPrice = mockSubscription.items.data[0].price;
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(subscriptionManager, 'listForCustomer')
.mockResolvedValue(mockSubscriptionList);
jest
.spyOn(stripeClient, 'subscriptionsCancel')
.mockResolvedValue(mockResponse);
await subscriptionManager.cancelIncompleteSubscriptionsToPrice(
mockCustomer.id,
mockPrice.id
);
expect(stripeClient.subscriptionsCancel).toBeCalledWith(
mockSubscription.id
);
});
});
describe('listForCustomer', () => {
it('returns subscriptions', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockSubscriptionList = StripeApiListFactory([mockSubscription]);
const mockCustomer = StripeCustomerFactory();
const expected = mockSubscriptionList.data;
jest
.spyOn(stripeClient, 'subscriptionsList')
.mockResolvedValue(mockSubscriptionList);
const result = await subscriptionManager.listForCustomer(mockCustomer.id);
expect(result).toEqual(expected);
});
it('returns empty array if no subscriptions exist', async () => {
const mockCustomer = StripeCustomerFactory();
jest
.spyOn(stripeClient, 'subscriptionsList')
.mockResolvedValue(StripeApiListFactory([]));
const result = await subscriptionManager.listForCustomer(mockCustomer.id);
expect(result).toEqual([]);
});
});
describe('cancel', () => {
it('calls stripeclient', async () => {
const mockSubscription = StripeSubscriptionFactory();
jest
.spyOn(stripeClient, 'subscriptionsCancel')
.mockResolvedValue(StripeResponseFactory(mockSubscription));
await subscriptionManager.cancel(mockSubscription.id);
expect(stripeClient.subscriptionsCancel).toBeCalledWith(
mockSubscription.id
);
});
});
describe('retrieve', () => {
it('calls stripeclient', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeClient, 'subscriptionsRetrieve')
.mockResolvedValue(mockResponse);
const result = await subscriptionManager.retrieve(mockSubscription.id);
expect(stripeClient.subscriptionsRetrieve).toBeCalledWith(
mockSubscription.id
);
expect(result).toEqual(mockResponse);
});
});
describe('update', () => {
it('calls stripeclient', async () => {
const mockParams = {
description: 'This is an updated subscription',
};
const mockSubscription = StripeSubscriptionFactory(mockParams);
const mockResponse = StripeResponseFactory(mockSubscription);
jest
.spyOn(stripeClient, 'subscriptionsUpdate')
.mockResolvedValue(mockResponse);
const result = await subscriptionManager.update(
mockSubscription.id,
mockParams
);
expect(stripeClient.subscriptionsUpdate).toBeCalledWith(
mockSubscription.id,
mockParams
);
expect(result).toEqual(mockResponse);
});
});
describe('getLatestPaymentIntent', () => {
it('fetches the latest payment intent for the subscription', async () => {
const mockSubscription = StripeResponseFactory(
StripeSubscriptionFactory()
);
const mockInvoice = StripeResponseFactory(StripeInvoiceFactory());
const mockPaymentIntent = StripeResponseFactory(
StripePaymentIntentFactory()
);
jest
.spyOn(stripeClient, 'invoicesRetrieve')
.mockResolvedValue(mockInvoice);
jest
.spyOn(stripeClient, 'paymentIntentRetrieve')
.mockResolvedValue(mockPaymentIntent);
const result = await subscriptionManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(mockPaymentIntent);
});
it('returns undefined if no invoice on subscription', async () => {
const mockSubscription = StripeSubscriptionFactory({
latest_invoice: null,
});
const result = await subscriptionManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(undefined);
});
it('returns undefined if the invoice has no payment intent', async () => {
const mockSubscription = StripeSubscriptionFactory();
const mockInvoice = StripeResponseFactory(
StripeInvoiceFactory({
payment_intent: null,
})
);
jest
.spyOn(stripeClient, 'invoicesRetrieve')
.mockResolvedValue(mockInvoice);
const result = await subscriptionManager.getLatestPaymentIntent(
mockSubscription
);
expect(result).toEqual(undefined);
});
});
});

Просмотреть файл

@ -0,0 +1,86 @@
/* 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 { Injectable } from '@nestjs/common';
import { Stripe } from 'stripe';
import { StripeClient } from './stripe.client';
import { StripeSubscription } from './stripe.client.types';
import { STRIPE_MINIMUM_CHARGE_AMOUNTS } from './stripe.constants';
import { StripeNoMinimumChargeAmountAvailableError } from './stripe.error';
@Injectable()
export class SubscriptionManager {
constructor(private client: StripeClient) {}
async cancel(subscriptionId: string) {
return this.client.subscriptionsCancel(subscriptionId);
}
async retrieve(subscriptionId: string) {
return this.client.subscriptionsRetrieve(subscriptionId);
}
async update(
subscriptionId: string,
params?: Stripe.SubscriptionUpdateParams
) {
return this.client.subscriptionsUpdate(subscriptionId, params);
}
async listForCustomer(customerId: string) {
const result = await this.client.subscriptionsList({
customer: customerId,
});
return result.data;
}
/**
* Returns minimum charge amount for currency
* Throws error for invalid currency
*/
getMinimumAmount(currency: string): number {
if (STRIPE_MINIMUM_CHARGE_AMOUNTS[currency]) {
return STRIPE_MINIMUM_CHARGE_AMOUNTS[currency];
}
throw new StripeNoMinimumChargeAmountAvailableError();
}
async cancelIncompleteSubscriptionsToPrice(
customerId: string,
priceId: string
) {
const subscriptions = await this.listForCustomer(customerId);
const targetSubs = subscriptions.filter((sub) =>
sub.items.data.find((item) => item.price.id === priceId)
);
for (const sub of targetSubs) {
if (sub && sub.status === 'incomplete') {
await this.client.subscriptionsCancel(sub.id);
}
}
}
async getLatestPaymentIntent(subscription: StripeSubscription) {
if (!subscription.latest_invoice) {
return;
}
const latestInvoice = await this.client.invoicesRetrieve(
subscription.latest_invoice
);
if (!latestInvoice.payment_intent) {
return;
}
const paymentIntent = await this.client.paymentIntentRetrieve(
latestInvoice.payment_intent
);
return paymentIntent;
}
}

Просмотреть файл

@ -0,0 +1,8 @@
import { StripeCustomer } from '../stripe.client.types';
export const isCustomerTaxEligible = (customer: StripeCustomer) => {
return (
customer.tax.automatic_tax === 'supported' ||
customer.tax.automatic_tax === 'not_collecting'
);
};

Просмотреть файл

@ -18,7 +18,12 @@ import {
import {
AccountCustomerManager,
StripeClient,
StripeManager,
CustomerManager,
InvoiceManager,
ProductManager,
PriceManager,
SubscriptionManager,
PromotionCodeManager,
} from '@fxa/payments/stripe';
import {
ContentfulClient,
@ -65,7 +70,12 @@ import { AccountManager } from '@fxa/shared/account/account';
ContentfulClient,
ContentfulManager,
ContentfulService,
StripeManager,
CustomerManager,
InvoiceManager,
ProductManager,
PriceManager,
SubscriptionManager,
PromotionCodeManager,
StripeClient,
PayPalClient,
PaypalCustomerManager,

Просмотреть файл

@ -7,7 +7,7 @@ import { Test } from '@nestjs/testing';
import {
StripeClient,
StripeConfig,
StripeManager,
PriceManager,
StripePlanFactory,
StripeResponseFactory,
SubplatInterval,
@ -32,7 +32,7 @@ import {
describe('ContentfulService', () => {
let contentfulManager: ContentfulManager;
let contentfulService: ContentfulService;
let stripeManager: StripeManager;
let priceManager: PriceManager;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
@ -47,13 +47,13 @@ describe('ContentfulService', () => {
MockStatsDProvider,
StripeClient,
StripeConfig,
StripeManager,
PriceManager,
],
}).compile();
contentfulService = moduleRef.get(ContentfulService);
contentfulManager = moduleRef.get(ContentfulManager);
stripeManager = moduleRef.get(StripeManager);
priceManager = moduleRef.get(PriceManager);
});
describe('fetchContentfulData', () => {
@ -88,7 +88,7 @@ describe('ContentfulService', () => {
.mockResolvedValue([mockPlan.id]);
jest
.spyOn(stripeManager, 'getPlanByInterval')
.spyOn(priceManager, 'retrieveByInterval')
.mockResolvedValue(mockPlan);
const result = await contentfulService.retrieveStripePlanId(
@ -108,7 +108,7 @@ describe('ContentfulService', () => {
.mockResolvedValue([mockPlan.id]);
jest
.spyOn(stripeManager, 'getPlanByInterval')
.spyOn(priceManager, 'retrieveByInterval')
.mockResolvedValue(undefined);
await expect(

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Injectable } from '@nestjs/common';
import { StripeManager, SubplatInterval } from '@fxa/payments/stripe';
import { PriceManager, SubplatInterval } from '@fxa/payments/stripe';
import { ContentfulServiceError } from './contentful.error';
import { ContentfulManager } from './contentful.manager';
import { ContentfulServiceConfig } from './contentful.service.config';
@ -12,7 +12,7 @@ import { ContentfulServiceConfig } from './contentful.service.config';
export class ContentfulService {
constructor(
private contentfulManager: ContentfulManager,
private stripeManager: StripeManager,
private priceManager: PriceManager,
private contentfulServiceConfig: ContentfulServiceConfig
) {}
@ -42,7 +42,7 @@ export class ContentfulService {
const filteredPlanIds = planIds.filter((priceId) =>
supportedListOfPriceIds.includes(priceId)
);
const plan = await this.stripeManager.getPlanByInterval(
const plan = await this.priceManager.retrieveByInterval(
filteredPlanIds,
interval
);

Просмотреть файл

@ -13,7 +13,10 @@ const { CapabilityManager } = require('@fxa/payments/capability');
const { EligibilityManager } = require('@fxa/payments/eligibility');
const {
StripeClient,
StripeManager,
ProductManager,
PriceManager,
SubscriptionManager,
PromotionCodeManager,
StripeService,
} = require('@fxa/payments/stripe');
const {
@ -127,6 +130,20 @@ async function run(config) {
/** @type {undefined | import('../lib/payments/stripe').StripeHelper} */
let stripeHelper = undefined;
if (config.subscriptions && config.subscriptions.stripeApiKey) {
const stripeClient = new StripeClient({
apiKey: config.subscriptions.stripeApiKey,
});
const productManager = new ProductManager(stripeClient);
const priceManager = new PriceManager(stripeClient);
const subscriptionManager = new SubscriptionManager(stripeClient);
const promotionCodeManager = new PromotionCodeManager(stripeClient);
const stripeService = new StripeService(
productManager,
subscriptionManager,
promotionCodeManager
);
Container.set(StripeService, stripeService);
if (
config.contentful &&
config.contentful.cdnUrl &&
@ -152,23 +169,14 @@ async function run(config) {
const contentfulManager = new ContentfulManager(contentfulClient, statsd);
Container.set(ContentfulManager, contentfulManager);
const capabilityManager = new CapabilityManager(contentfulManager);
const eligibilityManager = new EligibilityManager(contentfulManager);
const eligibilityManager = new EligibilityManager(
contentfulManager,
priceManager
);
Container.set(CapabilityManager, capabilityManager);
Container.set(EligibilityManager, eligibilityManager);
}
if (config.subscriptions.stripeApiKey && config.subscriptions.taxIds) {
const stripeClient = new StripeClient({
apiKey: config.subscriptions.stripeApiKey,
});
const stripeManager = new StripeManager(stripeClient, {
apiKey: config.subscriptions.stripeApiKey,
taxIds: config.subscriptions.taxIds,
});
const stripeService = new StripeService(stripeManager);
Container.set(StripeService, stripeService);
}
const { createStripeHelper } = require('../lib/payments/stripe');
stripeHelper = createStripeHelper(log, config, statsd);
Container.set(StripeHelper, stripeHelper);