Merge pull request #16625 from mozilla/FXA-8946

feat(payments-stripe): Create StripeManager getTaxIdForCurrency
This commit is contained in:
Lisa Chan 2024-04-03 13:49:31 -04:00 коммит произвёл GitHub
Родитель 1cf03d60ad ce1c451752
Коммит 3354f25b44
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
19 изменённых файлов: 250 добавлений и 153 удалений

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

@ -1,4 +1,5 @@
import { Stripe } from 'stripe';
import {
ContentfulClient,
PurchaseWithDetailsOfferingContentUtil,
@ -6,8 +7,9 @@ import {
PurchaseWithDetailsOfferingContentTransformedFactory,
PurchaseDetailsTransformedFactory,
} from '@fxa/shared/contentful';
import { StripePlanFactory, StripeProductFactory } from '@fxa/payments/stripe';
import { StripeMapperService } from './stripe-mapper.service';
import { ProductFactory, PlanFactory } from '@fxa/payments/stripe';
import { StripeMetadataWithContentfulFactory } from './factories';
describe('StripeMapperService', () => {
@ -37,7 +39,7 @@ describe('StripeMapperService', () => {
const validMetadata = StripeMetadataWithContentfulFactory({
webIconURL: 'https://example.com/webIconURL',
});
const stripePlan = PlanFactory({
const stripePlan = StripePlanFactory({
product: 'stringvalue',
metadata: validMetadata,
});
@ -61,8 +63,8 @@ describe('StripeMapperService', () => {
const validMetadata = StripeMetadataWithContentfulFactory({
webIconURL: 'https://example.com/webIconURL',
});
const stripePlan = PlanFactory() as Stripe.Plan;
stripePlan.product = ProductFactory({
const stripePlan = StripePlanFactory() as Stripe.Plan;
stripePlan.product = StripeProductFactory({
metadata: validMetadata,
});
const { mappedPlans, nonMatchingPlans } =
@ -88,8 +90,8 @@ describe('StripeMapperService', () => {
const validMetadata = StripeMetadataWithContentfulFactory({
webIconURL: 'https://example.com/webIconURL',
});
const stripePlan = PlanFactory() as Stripe.Plan;
stripePlan.product = ProductFactory({
const stripePlan = StripePlanFactory() as Stripe.Plan;
stripePlan.product = StripeProductFactory({
metadata: validMetadata,
});
const { mappedPlans, nonMatchingPlans } =
@ -118,10 +120,10 @@ describe('StripeMapperService', () => {
const planOverrideMetadata = StripeMetadataWithContentfulFactory({
webIconURL: 'https://plan.override/emailIcon',
});
const stripePlan = PlanFactory({
const stripePlan = StripePlanFactory({
metadata: planOverrideMetadata,
}) as Stripe.Plan;
stripePlan.product = ProductFactory({
stripePlan.product = StripeProductFactory({
metadata: validMetadata,
});
const { mappedPlans, nonMatchingPlans } =
@ -145,8 +147,8 @@ describe('StripeMapperService', () => {
productSet: 'set',
productOrder: 'order',
};
const stripePlan = PlanFactory() as Stripe.Plan;
stripePlan.product = ProductFactory({
const stripePlan = StripePlanFactory() as Stripe.Plan;
stripePlan.product = StripeProductFactory({
metadata: productMetadata,
});
const { mappedPlans, nonMatchingPlans } =
@ -183,16 +185,16 @@ describe('StripeMapperService', () => {
productSet: 'set',
productOrder: 'order',
};
const product = ProductFactory({
const product = StripeProductFactory({
metadata: productMetadata,
});
const stripePlan1 = PlanFactory({
const stripePlan1 = StripePlanFactory({
metadata: StripeMetadataWithContentfulFactory({
'product:details:1': 'Detail 1 in English',
}),
}) as Stripe.Plan;
stripePlan1.product = product;
const stripePlan2 = PlanFactory({
const stripePlan2 = StripePlanFactory({
metadata: StripeMetadataWithContentfulFactory({
'product:details:1': 'Detail 1 in French',
'product:details:2': 'Detail 2 in French',
@ -236,16 +238,16 @@ describe('StripeMapperService', () => {
productSet: 'set',
productOrder: 'order',
};
const product = ProductFactory({
const product = StripeProductFactory({
metadata: productMetadata,
});
const stripePlan1 = PlanFactory({
const stripePlan1 = StripePlanFactory({
metadata: StripeMetadataWithContentfulFactory({
'product:details:1': 'Detail 1 in German',
}),
}) as Stripe.Plan;
stripePlan1.product = product;
const stripePlan2 = PlanFactory({
const stripePlan2 = StripePlanFactory({
metadata: StripeMetadataWithContentfulFactory({
'product:details:1': 'Detail 1 in French',
'product:details:2': 'Detail 2 in French',

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

@ -5,12 +5,13 @@ import { faker } from '@faker-js/faker';
import { Kysely } from 'kysely';
import {
CustomerFactory,
InvoiceFactory,
StripeApiListFactory,
StripeClient,
StripeConfig,
StripeCustomerFactory,
StripeInvoiceFactory,
StripeManager,
SubscriptionFactory,
SubscriptionListFactory,
StripeSubscriptionFactory,
} from '@fxa/payments/stripe';
import { DB, testAccountDatabaseSetup } from '@fxa/shared/db/mysql/account';
@ -33,6 +34,7 @@ describe('PaypalManager', () => {
let paypalClient: PayPalClient;
let paypalManager: PayPalManager;
let stripeClient: StripeClient;
let stripeConfig: StripeConfig;
let stripeManager: StripeManager;
let paypalCustomerManager: PaypalCustomerManager;
@ -49,8 +51,13 @@ describe('PaypalManager', () => {
signature: faker.string.uuid(),
});
stripeConfig = {
apiKey: faker.string.uuid(),
taxIds: { EUR: 'EU1234' },
};
stripeClient = new StripeClient({} as any);
stripeManager = new StripeManager(stripeClient);
stripeManager = new StripeManager(stripeClient, stripeConfig);
paypalCustomerManager = new PaypalCustomerManager(kyselyDb);
paypalManager = new PayPalManager(
@ -155,7 +162,7 @@ describe('PaypalManager', () => {
describe('getCustomerBillingAgreementId', () => {
it("returns the customer's current PayPal billing agreement ID", async () => {
const mockPayPalCustomer = ResultPaypalCustomerFactory();
const mockStripeCustomer = CustomerFactory();
const mockStripeCustomer = StripeCustomerFactory();
paypalCustomerManager.fetchPaypalCustomersByUid = jest
.fn()
@ -168,7 +175,7 @@ describe('PaypalManager', () => {
});
it('throws PaypalCustomerNotFoundError if no PayPal customer record', async () => {
const mockStripeCustomer = CustomerFactory();
const mockStripeCustomer = StripeCustomerFactory();
paypalCustomerManager.fetchPaypalCustomersByUid = jest
.fn()
@ -182,7 +189,7 @@ describe('PaypalManager', () => {
it('throws PaypalCustomerMultipleRecordsError if more than one PayPal customer found', async () => {
const mockPayPalCustomer1 = ResultPaypalCustomerFactory();
const mockPayPalCustomer2 = ResultPaypalCustomerFactory();
const mockStripeCustomer = CustomerFactory();
const mockStripeCustomer = StripeCustomerFactory();
paypalCustomerManager.fetchPaypalCustomersByUid = jest
.fn()
@ -196,19 +203,16 @@ describe('PaypalManager', () => {
describe('getCustomerPayPalSubscriptions', () => {
it('return customer subscriptions where collection method is send_invoice', async () => {
const mockPayPalSubscription = SubscriptionFactory({
const mockPayPalSubscription = StripeSubscriptionFactory({
collection_method: 'send_invoice',
status: 'active',
});
const mockSubscriptionList = SubscriptionListFactory({
object: 'list',
url: '/v1/subscriptions',
has_more: false,
data: [mockPayPalSubscription],
});
const mockSubscriptionList = StripeApiListFactory([
mockPayPalSubscription,
]);
const mockCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
const expected = [mockPayPalSubscription];
@ -246,7 +250,7 @@ describe('PaypalManager', () => {
describe('processZeroInvoice', () => {
it('finalizes invoices with no amount set to zero', async () => {
const mockInvoice = InvoiceFactory();
const mockInvoice = StripeInvoiceFactory();
stripeManager.finalizeInvoiceWithoutAutoAdvance = jest
.fn()
@ -263,7 +267,7 @@ describe('PaypalManager', () => {
describe('processInvoice', () => {
it('calls processZeroInvoice when amount is less than minimum amount', async () => {
const mockInvoice = InvoiceFactory({
const mockInvoice = StripeInvoiceFactory({
amount_due: 0,
currency: 'usd',
});
@ -276,8 +280,8 @@ describe('PaypalManager', () => {
});
it('calls PayPalManager processNonZeroInvoice when amount is greater than minimum amount', async () => {
const mockCustomer = CustomerFactory();
const mockInvoice = InvoiceFactory({
const mockCustomer = StripeCustomerFactory();
const mockInvoice = StripeInvoiceFactory({
amount_due: 50,
currency: 'usd',
});

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

@ -1,17 +1,21 @@
export { CardFactory } from './lib/factories/card.factory';
export { CustomerFactory } from './lib/factories/customer.factory';
export { InvoiceLineItemFactory } from './lib/factories/invoice-line-item.factory';
export { InvoiceFactory } from './lib/factories/invoice.factory';
export { PriceFactory } from './lib/factories/price.factory';
export { PlanFactory } from './lib/factories/plan.factory';
export { ProductFactory } from './lib/factories/product.factory';
export {
SubscriptionFactory,
SubscriptionItemFactory,
SubscriptionListFactory,
StripeApiListFactory,
StripeResponseFactory,
} from './lib/factories/api-list.factory';
export { StripeCardFactory } from './lib/factories/card.factory';
export { StripeCustomerFactory } from './lib/factories/customer.factory';
export { StripeInvoiceLineItemFactory } from './lib/factories/invoice-line-item.factory';
export { StripeInvoiceFactory } from './lib/factories/invoice.factory';
export { StripePlanFactory } from './lib/factories/plan.factory';
export { StripePriceFactory } from './lib/factories/price.factory';
export { StripeProductFactory } from './lib/factories/product.factory';
export {
StripeSubscriptionFactory,
StripeSubscriptionItemFactory,
} from './lib/factories/subscription.factory';
export * from './lib/stripe.client';
export * from './lib/stripe.client.types';
export * from './lib/stripe.config';
export * from './lib/stripe.constants';
export * from './lib/stripe.error';
export * from './lib/stripe.manager';
export * from './lib/stripe.client.types';

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

@ -0,0 +1,26 @@
import { faker } from '@faker-js/faker';
import { StripeApiList, StripeResponse } from '../stripe.client.types';
export const StripeApiListFactory = <T extends Array<any>>(
data: T,
override?: Partial<StripeApiList<T>>
): StripeApiList<T[0]> => ({
object: 'list',
url: '/v1/subscriptions',
has_more: false,
data,
...override,
});
export const StripeResponseFactory = <T>(
data: T,
override?: Partial<StripeResponse<T>>
): StripeResponse<T> => ({
lastResponse: {
headers: {},
requestId: faker.string.uuid(),
statusCode: 200,
},
...data,
...override,
});

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

@ -5,7 +5,9 @@
import { faker } from '@faker-js/faker';
import { StripeCard } from '../stripe.client.types';
export const CardFactory = (override?: Partial<StripeCard>): StripeCard => ({
export const StripeCardFactory = (
override?: Partial<StripeCard>
): StripeCard => ({
id: 'card',
object: 'card',
address_city: faker.location.city(),

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

@ -5,7 +5,7 @@
import { faker } from '@faker-js/faker';
import { StripeCustomer } from '../stripe.client.types';
export const CustomerFactory = (
export const StripeCustomerFactory = (
override?: Partial<StripeCustomer>
): StripeCustomer => ({
id: `cus_${faker.string.alphanumeric({ length: 14 })}`,

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

@ -5,7 +5,7 @@
import { faker } from '@faker-js/faker';
import { StripeInvoiceLineItem } from '../stripe.client.types';
export const InvoiceLineItemFactory = (
export const StripeInvoiceLineItemFactory = (
override?: Partial<StripeInvoiceLineItem>
): StripeInvoiceLineItem => ({
id: faker.string.alphanumeric(10),

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

@ -3,10 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { faker } from '@faker-js/faker';
import { InvoiceLineItemFactory } from './invoice-line-item.factory';
import { StripeInvoiceLineItemFactory } from './invoice-line-item.factory';
import { StripeInvoice } from '../stripe.client.types';
export const InvoiceFactory = (
export const StripeInvoiceFactory = (
override?: Partial<StripeInvoice>
): StripeInvoice => ({
id: `in_${faker.string.alphanumeric({ length: 24 })}`,
@ -53,7 +53,7 @@ export const InvoiceFactory = (
latest_revision: null,
lines: {
object: 'list',
data: [InvoiceLineItemFactory()],
data: [StripeInvoiceLineItemFactory()],
has_more: false,
url: faker.internet.url(),
},

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

@ -5,7 +5,9 @@
import { faker } from '@faker-js/faker';
import { StripePlan } from '../stripe.client.types';
export const PlanFactory = (override?: Partial<StripePlan>): StripePlan => ({
export const StripePlanFactory = (
override?: Partial<StripePlan>
): StripePlan => ({
id: `plan_${faker.string.alphanumeric({ length: 24 })}`,
object: 'plan',
active: true,

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

@ -5,7 +5,9 @@
import { faker } from '@faker-js/faker';
import { StripePrice } from '../stripe.client.types';
export const PriceFactory = (override?: Partial<StripePrice>): StripePrice => ({
export const StripePriceFactory = (
override?: Partial<StripePrice>
): StripePrice => ({
id: `price_${faker.string.alphanumeric({ length: 24 })}`,
object: 'price',
active: true,

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

@ -5,7 +5,7 @@
import { faker } from '@faker-js/faker';
import { StripeProduct } from '../stripe.client.types';
export const ProductFactory = (
export const StripeProductFactory = (
override?: Partial<StripeProduct>
): StripeProduct => ({
id: `prod_${faker.string.alphanumeric({ length: 14 })}`,

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

@ -3,14 +3,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { faker } from '@faker-js/faker';
import { PriceFactory } from './price.factory';
import { StripePriceFactory } from './price.factory';
import {
StripeApiList,
StripeSubscription,
StripeSubscriptionItem,
} from '../stripe.client.types';
export const SubscriptionFactory = (
export const StripeSubscriptionFactory = (
override?: Partial<StripeSubscription>
): StripeSubscription => ({
id: `sub_${faker.string.alphanumeric({ length: 24 })}`,
@ -40,7 +39,7 @@ export const SubscriptionFactory = (
ended_at: null,
items: {
object: 'list',
data: [SubscriptionItemFactory()],
data: [StripeSubscriptionItemFactory()],
has_more: false,
url: `/v1/subscription_items?subscription=sub_${faker.string.alphanumeric({
length: 24,
@ -67,7 +66,7 @@ export const SubscriptionFactory = (
...override,
});
export const SubscriptionItemFactory = (
export const StripeSubscriptionItemFactory = (
override?: Partial<StripeSubscriptionItem>
): StripeSubscriptionItem => ({
id: `si_${faker.string.alphanumeric({ length: 14 })}`,
@ -98,19 +97,9 @@ export const SubscriptionItemFactory = (
trial_period_days: null,
usage_type: 'licensed',
},
price: PriceFactory(),
price: StripePriceFactory(),
quantity: 1,
subscription: `sub_${faker.string.alphanumeric({ length: 24 })}`,
tax_rates: [],
...override,
});
export const SubscriptionListFactory = (
override?: Partial<StripeApiList<StripeSubscription>>
): StripeApiList<StripeSubscription> => ({
object: 'list',
url: '/v1/subscriptions',
has_more: false,
data: [SubscriptionFactory()],
...override,
});

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

@ -3,45 +3,83 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { faker } from '@faker-js/faker';
import { CustomerFactory } from './factories/customer.factory';
import { InvoiceFactory } from './factories/invoice.factory';
import { SubscriptionListFactory } from './factories/subscription.factory';
import { Stripe } from 'stripe';
import {
StripeApiListFactory,
StripeResponseFactory,
} from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripeSubscriptionFactory } from './factories/subscription.factory';
import { StripeClient } from './stripe.client';
const mockJestFnGenerator = <T extends (...args: any[]) => any>() => {
return jest.fn<ReturnType<T>, Parameters<T>>();
};
const mockStripeCustomersRetrieve =
mockJestFnGenerator<typeof Stripe.prototype.customers.retrieve>();
const mockStripeCustomersUpdate =
mockJestFnGenerator<typeof Stripe.prototype.customers.update>();
const mockStripeFinalizeInvoice =
mockJestFnGenerator<typeof Stripe.prototype.invoices.finalizeInvoice>();
const mockStripeSubscriptionsList =
mockJestFnGenerator<typeof Stripe.prototype.subscriptions.list>();
jest.mock('stripe', () => ({
Stripe: function () {
return {
customers: {
retrieve: mockStripeCustomersRetrieve,
update: mockStripeCustomersUpdate,
},
invoices: {
finalizeInvoice: mockStripeFinalizeInvoice,
},
subscriptions: {
list: mockStripeSubscriptionsList,
},
};
},
}));
describe('StripeClient', () => {
let mockClient: StripeClient;
beforeEach(() => {
mockClient = new StripeClient({
apiKey: faker.string.uuid(),
taxIds: { EUR: 'EU1234' },
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(mockClient).toBeDefined();
});
describe('fetchCustomer', () => {
it('returns an existing customer from Stripe', async () => {
const mockCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
const mockResponse = StripeResponseFactory(mockCustomer);
mockClient.stripe.customers.retrieve = jest
.fn()
.mockResolvedValueOnce(mockCustomer);
mockStripeCustomersRetrieve.mockResolvedValueOnce(mockResponse);
const result = await mockClient.fetchCustomer(mockCustomer.id);
expect(result).toEqual(mockCustomer);
expect(result).toEqual(mockResponse);
});
});
describe('updateCustomer', () => {
it('updates an existing customer from Stripe', async () => {
const mockCustomer = CustomerFactory();
const mockUpdatedCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
const mockUpdatedCustomer = StripeCustomerFactory();
const mockResponse = StripeResponseFactory(mockUpdatedCustomer);
mockClient.stripe.customers.update = jest
.fn()
.mockResolvedValueOnce(mockUpdatedCustomer);
mockStripeCustomersUpdate.mockResolvedValueOnce(mockResponse);
const result = await mockClient.updateCustomer(mockCustomer.id, {
balance: mockUpdatedCustomer.balance,
@ -53,12 +91,12 @@ describe('StripeClient', () => {
describe('fetchSubscriptions', () => {
it('returns subscriptions from Stripe', async () => {
const mockCustomer = CustomerFactory();
const mockSubscriptionList = SubscriptionListFactory();
const mockCustomer = StripeCustomerFactory();
const mockSubscriptionList = StripeResponseFactory(
StripeApiListFactory([StripeSubscriptionFactory()])
);
mockClient.stripe.subscriptions.list = jest
.fn()
.mockResolvedValueOnce(mockSubscriptionList);
mockStripeSubscriptionsList.mockResolvedValue(mockSubscriptionList);
const result = await mockClient.fetchSubscriptions(mockCustomer.id);
expect(result).toEqual(mockSubscriptionList);
@ -67,23 +105,18 @@ describe('StripeClient', () => {
describe('finalizeInvoice', () => {
it('works successfully', async () => {
const mockInvoice = InvoiceFactory({
const mockInvoice = StripeInvoiceFactory({
auto_advance: false,
});
const mockResponse = StripeResponseFactory(mockInvoice);
mockClient.stripe.invoices.finalizeInvoice = jest
.fn()
.mockResolvedValueOnce({});
mockStripeFinalizeInvoice.mockResolvedValue(mockResponse);
const result = await mockClient.finalizeInvoice(mockInvoice.id, {
auto_advance: false,
});
expect(result).toEqual({});
expect(mockClient.stripe.invoices.finalizeInvoice).toBeCalledWith(
mockInvoice.id,
{ auto_advance: false }
);
expect(result).toEqual(mockResponse);
});
});
});

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

@ -5,13 +5,13 @@
import { Injectable } from '@nestjs/common';
import { Stripe } from 'stripe';
import { StripeClientConfig } from './stripe.client.config';
import {
StripeCustomer,
StripeDeletedCustomer,
StripeInvoice,
StripeSubscription,
} from './stripe.client.types';
import { StripeConfig } from './stripe.config';
/**
* A wrapper for Stripe that enforces that results have deterministic typings
@ -19,10 +19,9 @@ import {
*/
@Injectable()
export class StripeClient {
public readonly stripe: Stripe;
constructor(private stripeClientConfig: StripeClientConfig) {
this.stripe = new Stripe(this.stripeClientConfig.apiKey, {
private readonly stripe: Stripe;
constructor(private stripeConfig: StripeConfig) {
this.stripe = new Stripe(this.stripeConfig.apiKey, {
apiVersion: '2022-11-15',
maxNetworkRetries: 3,
});

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

@ -278,3 +278,5 @@ export type StripeCard = NegotiateExpanded<
>;
export type StripeApiList<T> = Stripe.ApiList<T>;
export type StripeApiListPromise<T> = Stripe.ApiListPromise<T>;
export type StripeResponse<T> = Stripe.Response<T>;

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

@ -2,9 +2,12 @@
* 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 { IsString } from 'class-validator';
import { IsObject, IsString } from 'class-validator';
export class StripeClientConfig {
export class StripeConfig {
@IsString()
public readonly apiKey!: string;
@IsObject()
public readonly taxIds!: { [key: string]: string };
}

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

@ -6,12 +6,9 @@ import { BaseError } from '@fxa/shared/error';
export class StripeError extends BaseError {
constructor(message: string, cause?: Error) {
super(
message,
{
cause,
},
);
super(message, {
cause,
});
}
}
@ -27,7 +24,7 @@ export class CustomerNotFoundError extends StripeError {
}
}
export class SubscriptionStripeError extends StripeError {
export class StripeNoMinimumChargeAmountAvailableError extends StripeError {
constructor() {
super('Currency does not have a minimum charge amount available.');
}

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

@ -3,26 +3,36 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Test, TestingModule } from '@nestjs/testing';
import { faker } from '@faker-js/faker';
import { CustomerFactory } from './factories/customer.factory';
import { InvoiceFactory } from './factories/invoice.factory';
import {
SubscriptionFactory,
SubscriptionListFactory,
} from './factories/subscription.factory';
import { StripeApiListFactory } from './factories/api-list.factory';
import { StripeCustomerFactory } from './factories/customer.factory';
import { StripeInvoiceFactory } from './factories/invoice.factory';
import { StripeSubscriptionFactory } from './factories/subscription.factory';
import { StripeClient } from './stripe.client';
import { StripeConfig } from './stripe.config';
import { StripeManager } from './stripe.manager';
describe('StripeManager', () => {
let manager: StripeManager;
let mockClient: StripeClient;
let mockConfig: StripeConfig;
beforeEach(async () => {
mockClient = new StripeClient({} as any);
mockClient = new StripeClient({
apiKey: faker.string.uuid(),
taxIds: { EUR: 'EU1234' },
});
mockConfig = {
apiKey: faker.string.uuid(),
taxIds: { EUR: 'EU1234' },
};
const module: TestingModule = await Test.createTestingModule({
providers: [
{ provide: StripeClient, useValue: mockClient },
{ provide: StripeConfig, useValue: mockConfig },
StripeManager,
],
}).compile();
@ -37,7 +47,7 @@ describe('StripeManager', () => {
describe('fetchActiveCustomer', () => {
it('returns an existing customer from Stripe', async () => {
const mockCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
mockClient.fetchCustomer = jest.fn().mockResolvedValueOnce(mockCustomer);
@ -48,7 +58,7 @@ describe('StripeManager', () => {
describe('finalizeInvoiceWithoutAutoAdvance', () => {
it('works successfully', async () => {
const mockInvoice = InvoiceFactory({
const mockInvoice = StripeInvoiceFactory({
auto_advance: false,
});
@ -76,17 +86,28 @@ describe('StripeManager', () => {
});
});
describe('getTaxIdForCurrency', () => {
it('returns the correct tax id for currency', async () => {
const mockCurrency = 'eur';
const result = manager.getTaxIdForCurrency(mockCurrency);
expect(result).toEqual('EU1234');
});
it('returns empty string when no tax id found', async () => {
const mockCurrency = faker.finance.currencyCode();
const result = manager.getTaxIdForCurrency(mockCurrency);
expect(result).toEqual(undefined);
});
});
describe('getSubscriptions', () => {
it('returns subscriptions', async () => {
const mockSubscription = SubscriptionFactory();
const mockSubscriptionList = SubscriptionListFactory({
object: 'list',
url: '/v1/subscriptions',
has_more: false,
data: [mockSubscription],
});
const mockSubscription = StripeSubscriptionFactory();
const mockSubscriptionList = StripeApiListFactory([mockSubscription]);
const mockCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
const expected = mockSubscriptionList;
@ -101,7 +122,7 @@ describe('StripeManager', () => {
describe('isCustomerStripeTaxEligible', () => {
it('should return true for a taxable customer', async () => {
const mockCustomer = CustomerFactory({
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'supported',
ip_address: null,
@ -114,7 +135,7 @@ describe('StripeManager', () => {
});
it('should return true for a customer in a not-collecting location', async () => {
const mockCustomer = CustomerFactory({
const mockCustomer = StripeCustomerFactory({
tax: {
automatic_tax: 'not_collecting',
ip_address: null,
@ -129,7 +150,7 @@ describe('StripeManager', () => {
describe('update customer tax ID', () => {
it('returns customer tax id if found', async () => {
const mockCustomer = CustomerFactory({
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: 'LeeroyJenkins' }],
default_payment_method: null,
@ -146,7 +167,7 @@ describe('StripeManager', () => {
});
it('returns undefined when customer tax id not found', async () => {
const mockCustomer = CustomerFactory();
const mockCustomer = StripeCustomerFactory();
mockClient.fetchCustomer = jest.fn().mockResolvedValueOnce(mockCustomer);
@ -156,8 +177,8 @@ describe('StripeManager', () => {
});
it('updates customer object with incoming tax id when match is not found', async () => {
const mockCustomer = CustomerFactory();
const mockUpdatedCustomer = CustomerFactory({
const mockCustomer = StripeCustomerFactory();
const mockUpdatedCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: 'EU1234' }],
default_payment_method: null,
@ -178,7 +199,7 @@ describe('StripeManager', () => {
});
it('does not update customer object when incoming tax id matches existing tax id', async () => {
const mockCustomer = CustomerFactory({
const mockCustomer = StripeCustomerFactory({
invoice_settings: {
custom_fields: [{ name: 'Tax ID', value: 'T43CAK315A713' }],
default_payment_method: null,

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

@ -5,20 +5,43 @@
import { Injectable } from '@nestjs/common';
import { StripeClient } from './stripe.client';
import { StripeCustomer } from './stripe.client.types';
import { StripeConfig } from './stripe.config';
import {
STRIPE_MINIMUM_CHARGE_AMOUNTS,
MOZILLA_TAX_ID,
STRIPE_MINIMUM_CHARGE_AMOUNTS,
} from './stripe.constants';
import {
CustomerDeletedError,
CustomerNotFoundError,
SubscriptionStripeError,
StripeNoMinimumChargeAmountAvailableError,
} from './stripe.error';
import { StripeCustomer } from './stripe.client.types';
@Injectable()
export class StripeManager {
constructor(private client: StripeClient) {}
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
@ -38,18 +61,6 @@ export class StripeManager {
});
}
/**
* 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 SubscriptionStripeError();
}
/**
* Retrieves subscriptions
*/