зеркало из https://github.com/mozilla/fxa.git
Merge pull request #16625 from mozilla/FXA-8946
feat(payments-stripe): Create StripeManager getTaxIdForCurrency
This commit is contained in:
Коммит
3354f25b44
|
@ -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
|
||||
*/
|
||||
|
|
Загрузка…
Ссылка в новой задаче