зеркало из https://github.com/mozilla/fxa.git
feat(libs/payments): Implement customerChanged
This commit is contained in:
Родитель
ae82b7f823
Коммит
c20c754e4e
|
@ -409,7 +409,6 @@ commands:
|
|||
paths:
|
||||
- artifacts/blob-report
|
||||
|
||||
|
||||
rename-reports-chromium:
|
||||
steps:
|
||||
- run:
|
||||
|
@ -624,7 +623,7 @@ jobs:
|
|||
|
||||
build:
|
||||
executor: default-executor
|
||||
resource_class: large
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- git-checkout
|
||||
- restore-workspace
|
||||
|
@ -908,7 +907,7 @@ jobs:
|
|||
|
||||
build-and-deploy-storybooks:
|
||||
executor: default-executor
|
||||
resource_class: large
|
||||
resource_class: xlarge
|
||||
steps:
|
||||
- git-checkout
|
||||
- restore-workspace
|
||||
|
|
|
@ -93,6 +93,15 @@ CSP__PAYPAL_API='https://www.sandbox.paypal.com'
|
|||
SENTRY__SERVER_NAME=fxa-payments-next-server
|
||||
SENTRY__AUTH_TOKEN=
|
||||
|
||||
# NotifierSns Config
|
||||
NOTIFIER_SNS_CONFIG__SNS_TOPIC_ARN=arn:aws:sns:us-west-2:123456789012:MyTopic
|
||||
NOTIFIER_SNS_CONFIG__SNS_TOPIC_ENDPOINT=http://localhost:4566
|
||||
|
||||
# ProfileClient Config
|
||||
PROFILE_CLIENT_CONFIG__URL=http://localhost:1111
|
||||
PROFILE_CLIENT_CONFIG__SECRET_BEARER_TOKEN='YOU MUST CHANGE ME'
|
||||
PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
|
||||
|
||||
# Other
|
||||
CONTENT_SERVER_URL=http://localhost:3030
|
||||
SUPPORT_URL=https://support.mozilla.org
|
||||
|
|
|
@ -89,6 +89,15 @@ CSP__PAYPAL_API='https://www.paypal.com'
|
|||
SENTRY__SERVER_NAME=fxa-payments-next-server
|
||||
SENTRY__AUTH_TOKEN=
|
||||
|
||||
# NotifierSns Config
|
||||
NOTIFIER_SNS_CONFIG__SNS_TOPIC_ARN=arn:aws:sns:us-west-2:123456789012:MyTopic
|
||||
NOTIFIER_SNS_CONFIG__SNS_TOPIC_ENDPOINT=http://localhost:4566
|
||||
|
||||
# ProfileClient Config
|
||||
PROFILE_CLIENT_CONFIG__URL=https://profile.accounts.firefox.com
|
||||
PROFILE_CLIENT_CONFIG__SECRET_BEARER_TOKEN='YOU MUST CHANGE ME'
|
||||
PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
|
||||
|
||||
# Other
|
||||
CONTENT_SERVER_URL=https://accounts.firefox.com
|
||||
SUPPORT_URL=https://support.mozilla.org
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import {
|
||||
|
@ -39,6 +38,10 @@ import {
|
|||
MockStripeConfigProvider,
|
||||
AccountCustomerManager,
|
||||
} from '@fxa/payments/stripe';
|
||||
import {
|
||||
MockProfileClientConfigProvider,
|
||||
ProfileClient,
|
||||
} from '@fxa/profile/client';
|
||||
import {
|
||||
MockStrapiClientConfigProvider,
|
||||
ProductConfigurationManager,
|
||||
|
@ -57,8 +60,14 @@ import {
|
|||
GeoDBManagerConfig,
|
||||
MockGeoDBNestFactory,
|
||||
} from '@fxa/shared/geodb';
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import {
|
||||
MockNotifierSnsConfigProvider,
|
||||
NotifierService,
|
||||
NotifierSnsProvider,
|
||||
} from '@fxa/shared/notifier';
|
||||
import {
|
||||
CheckoutCustomerDataFactory,
|
||||
FinishErrorCartFactory,
|
||||
|
@ -89,6 +98,11 @@ describe('CartService', () => {
|
|||
let invoiceManager: InvoiceManager;
|
||||
let productConfigurationManager: ProductConfigurationManager;
|
||||
|
||||
const mockLogger = {
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
|
@ -97,7 +111,6 @@ describe('CartService', () => {
|
|||
CartManager,
|
||||
CartService,
|
||||
CheckoutService,
|
||||
ConfigService,
|
||||
CustomerManager,
|
||||
EligibilityManager,
|
||||
EligibilityService,
|
||||
|
@ -107,10 +120,14 @@ describe('CartService', () => {
|
|||
MockAccountDatabaseNestFactory,
|
||||
MockFirestoreProvider,
|
||||
MockGeoDBNestFactory,
|
||||
MockNotifierSnsConfigProvider,
|
||||
MockPaypalClientConfigProvider,
|
||||
MockProfileClientConfigProvider,
|
||||
MockStatsDProvider,
|
||||
MockStrapiClientConfigProvider,
|
||||
MockStripeConfigProvider,
|
||||
NotifierService,
|
||||
NotifierSnsProvider,
|
||||
PaymentMethodManager,
|
||||
PaypalBillingAgreementManager,
|
||||
PayPalClient,
|
||||
|
@ -118,12 +135,17 @@ describe('CartService', () => {
|
|||
PriceManager,
|
||||
ProductConfigurationManager,
|
||||
ProductManager,
|
||||
ProfileClient,
|
||||
PromotionCodeManager,
|
||||
StrapiClient,
|
||||
StripeClient,
|
||||
SubscriptionManager,
|
||||
CurrencyManager,
|
||||
MockCurrencyConfigProvider,
|
||||
{
|
||||
provide: LOGGER_PROVIDER,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
@ -142,43 +164,49 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
describe('setupCart', () => {
|
||||
it('calls createCart with expected parameters', async () => {
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockAccountCustomer = ResultAccountCustomerFactory({
|
||||
stripeCustomerId: mockCustomer.id,
|
||||
});
|
||||
const mockResultCart = ResultCartFactory();
|
||||
const args = {
|
||||
interval: SubplatInterval.Monthly,
|
||||
offeringConfigId: faker.string.uuid(),
|
||||
experiment: faker.string.uuid(),
|
||||
promoCode: faker.word.noun(),
|
||||
uid: faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
ip: faker.internet.ipv4(),
|
||||
};
|
||||
const taxAddress = TaxAddressFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory();
|
||||
const mockResolvedCurrency = faker.finance.currencyCode();
|
||||
const args = {
|
||||
interval: SubplatInterval.Monthly,
|
||||
offeringConfigId: faker.string.uuid(),
|
||||
experiment: faker.string.uuid(),
|
||||
promoCode: faker.word.noun(),
|
||||
uid: faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
ip: faker.internet.ipv4(),
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(eligibilityService, 'checkEligibility')
|
||||
.mockResolvedValue(EligibilityStatus.CREATE);
|
||||
jest.spyOn(geodbManager, 'getTaxAddress').mockReturnValue(taxAddress);
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockAccountCustomer = ResultAccountCustomerFactory({
|
||||
stripeCustomerId: mockCustomer.id,
|
||||
});
|
||||
const mockInvoicePreview = InvoicePreviewFactory();
|
||||
const mockResultCart = ResultCartFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
const taxAddress = TaxAddressFactory();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(geodbManager, 'getTaxAddress').mockReturnValue(taxAddress);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest
|
||||
.spyOn(invoiceManager, 'preview')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
jest
|
||||
.spyOn(eligibilityService, 'checkEligibility')
|
||||
.mockResolvedValue(EligibilityStatus.CREATE);
|
||||
});
|
||||
|
||||
it('calls createCart with expected parameters', async () => {
|
||||
const mockResultCart = ResultCartFactory();
|
||||
const mockResolvedCurrency = faker.finance.currencyCode();
|
||||
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockResolvedValue(undefined);
|
||||
|
@ -206,44 +234,10 @@ describe('CartService', () => {
|
|||
|
||||
it('throws an error when couponCode is invalid', async () => {
|
||||
const mockAccount = AccountFactory();
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockAccountCustomer = ResultAccountCustomerFactory({
|
||||
stripeCustomerId: mockCustomer.id,
|
||||
});
|
||||
const mockResultCart = ResultCartFactory();
|
||||
const args = {
|
||||
interval: SubplatInterval.Monthly,
|
||||
offeringConfigId: faker.string.uuid(),
|
||||
experiment: faker.string.uuid(),
|
||||
promoCode: faker.word.noun(),
|
||||
uid: faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
ip: faker.internet.ipv4(),
|
||||
};
|
||||
const taxAddress = TaxAddressFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory();
|
||||
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockRejectedValue(undefined);
|
||||
jest
|
||||
.spyOn(eligibilityService, 'checkEligibility')
|
||||
.mockResolvedValue(EligibilityStatus.CREATE);
|
||||
jest.spyOn(geodbManager, 'getTaxAddress').mockReturnValue(taxAddress);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest
|
||||
.spyOn(invoiceManager, 'preview')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockResultCart);
|
||||
jest
|
||||
.spyOn(accountManager, 'getAccounts')
|
||||
|
@ -258,44 +252,7 @@ describe('CartService', () => {
|
|||
|
||||
it('throws an error when country to currency result is invalid', async () => {
|
||||
const mockAccount = AccountFactory();
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockAccountCustomer = ResultAccountCustomerFactory({
|
||||
stripeCustomerId: mockCustomer.id,
|
||||
});
|
||||
const mockResultCart = ResultCartFactory();
|
||||
const args = {
|
||||
interval: SubplatInterval.Monthly,
|
||||
offeringConfigId: faker.string.uuid(),
|
||||
experiment: faker.string.uuid(),
|
||||
promoCode: faker.word.noun(),
|
||||
uid: faker.string.hexadecimal({
|
||||
length: 32,
|
||||
prefix: '',
|
||||
casing: 'lower',
|
||||
}),
|
||||
ip: faker.internet.ipv4(),
|
||||
};
|
||||
const taxAddress = TaxAddressFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory();
|
||||
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockRejectedValue(undefined);
|
||||
jest
|
||||
.spyOn(eligibilityService, 'checkEligibility')
|
||||
.mockResolvedValue(EligibilityStatus.CREATE);
|
||||
jest.spyOn(geodbManager, 'getTaxAddress').mockReturnValue(taxAddress);
|
||||
jest
|
||||
.spyOn(accountCustomerManager, 'getAccountCustomerByUid')
|
||||
.mockResolvedValue(mockAccountCustomer);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest
|
||||
.spyOn(invoiceManager, 'preview')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockResolvedValue(undefined);
|
||||
|
@ -316,21 +273,24 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
describe('restartCart', () => {
|
||||
it('fetches old cart and creates new cart with same details', async () => {
|
||||
const mockOldCart = ResultCartFactory({
|
||||
couponCode: faker.word.noun(),
|
||||
});
|
||||
const mockNewCart = ResultCartFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockOldCart = ResultCartFactory({
|
||||
couponCode: faker.word.noun(),
|
||||
});
|
||||
const mockNewCart = ResultCartFactory();
|
||||
const mockPrice = StripePriceFactory();
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockOldCart);
|
||||
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockNewCart);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
});
|
||||
|
||||
it('fetches old cart and creates new cart with same details', async () => {
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockResolvedValue(undefined);
|
||||
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockNewCart);
|
||||
|
||||
const result = await cartService.restartCart(mockOldCart.id);
|
||||
|
||||
|
@ -351,16 +311,10 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
it('throws an error when couponCode is invalid', async () => {
|
||||
const mockOldCart = ResultCartFactory({
|
||||
couponCode: faker.word.noun(),
|
||||
});
|
||||
const mockNewCart = ResultCartFactory();
|
||||
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockOldCart);
|
||||
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockNewCart);
|
||||
jest
|
||||
.spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice')
|
||||
.mockRejectedValue(undefined);
|
||||
jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockNewCart);
|
||||
|
||||
await expect(() =>
|
||||
cartService.restartCart(mockOldCart.id)
|
||||
|
|
|
@ -161,7 +161,7 @@ export class CartService {
|
|||
oldCart.interval as SubplatInterval
|
||||
);
|
||||
|
||||
this.promotionCodeManager.assertValidPromotionCodeNameForPrice(
|
||||
await this.promotionCodeManager.assertValidPromotionCodeNameForPrice(
|
||||
oldCart.couponCode,
|
||||
price
|
||||
);
|
||||
|
|
|
@ -51,6 +51,10 @@ import {
|
|||
MockStripeConfigProvider,
|
||||
AccountCustomerManager,
|
||||
} from '@fxa/payments/stripe';
|
||||
import {
|
||||
MockProfileClientConfigProvider,
|
||||
ProfileClient,
|
||||
} from '@fxa/profile/client';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import {
|
||||
MockStrapiClientConfigProvider,
|
||||
|
@ -62,7 +66,13 @@ import {
|
|||
CartEligibilityStatus,
|
||||
MockAccountDatabaseNestFactory,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { MockStatsDProvider, StatsDService } from '@fxa/shared/metrics/statsd';
|
||||
import {
|
||||
MockNotifierSnsConfigProvider,
|
||||
NotifierService,
|
||||
NotifierSnsProvider,
|
||||
} from '@fxa/shared/notifier';
|
||||
import {
|
||||
CartEligibilityMismatchError,
|
||||
CartTotalMismatchError,
|
||||
|
@ -80,14 +90,21 @@ describe('CheckoutService', () => {
|
|||
let customerManager: CustomerManager;
|
||||
let eligibilityService: EligibilityService;
|
||||
let invoiceManager: InvoiceManager;
|
||||
let mockStatsd: StatsD;
|
||||
let paymentMethodManager: PaymentMethodManager;
|
||||
let paypalBillingAgreementManager: PaypalBillingAgreementManager;
|
||||
let paypalCustomerManager: PaypalCustomerManager;
|
||||
let privateMethod: any;
|
||||
let productConfigurationManager: ProductConfigurationManager;
|
||||
let profileClient: ProfileClient;
|
||||
let promotionCodeManager: PromotionCodeManager;
|
||||
let statsd: StatsD;
|
||||
let subscriptionManager: SubscriptionManager;
|
||||
|
||||
const mockLogger = {
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
|
@ -101,9 +118,13 @@ describe('CheckoutService', () => {
|
|||
InvoiceManager,
|
||||
MockAccountDatabaseNestFactory,
|
||||
MockFirestoreProvider,
|
||||
MockNotifierSnsConfigProvider,
|
||||
MockProfileClientConfigProvider,
|
||||
MockStatsDProvider,
|
||||
MockStrapiClientConfigProvider,
|
||||
MockStripeConfigProvider,
|
||||
NotifierService,
|
||||
NotifierSnsProvider,
|
||||
PaymentMethodManager,
|
||||
PaypalBillingAgreementManager,
|
||||
PayPalClient,
|
||||
|
@ -112,11 +133,16 @@ describe('CheckoutService', () => {
|
|||
PriceManager,
|
||||
ProductConfigurationManager,
|
||||
ProductManager,
|
||||
ProfileClient,
|
||||
PromotionCodeManager,
|
||||
StrapiClient,
|
||||
StripeClient,
|
||||
StripeConfig,
|
||||
SubscriptionManager,
|
||||
{
|
||||
provide: LOGGER_PROVIDER,
|
||||
useValue: mockLogger,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
@ -127,14 +153,18 @@ describe('CheckoutService', () => {
|
|||
customerManager = moduleRef.get(CustomerManager);
|
||||
eligibilityService = moduleRef.get(EligibilityService);
|
||||
invoiceManager = moduleRef.get(InvoiceManager);
|
||||
mockStatsd = moduleRef.get(StatsDService);
|
||||
paymentMethodManager = moduleRef.get(PaymentMethodManager);
|
||||
paypalBillingAgreementManager = moduleRef.get(
|
||||
PaypalBillingAgreementManager
|
||||
);
|
||||
paypalCustomerManager = moduleRef.get(PaypalCustomerManager);
|
||||
privateMethod = jest
|
||||
.spyOn(checkoutService as any, 'customerChanged')
|
||||
.mockResolvedValue({});
|
||||
profileClient = moduleRef.get(ProfileClient);
|
||||
productConfigurationManager = moduleRef.get(ProductConfigurationManager);
|
||||
promotionCodeManager = moduleRef.get(PromotionCodeManager);
|
||||
statsd = moduleRef.get(StatsDService);
|
||||
subscriptionManager = moduleRef.get(SubscriptionManager);
|
||||
});
|
||||
|
||||
|
@ -415,29 +445,31 @@ describe('CheckoutService', () => {
|
|||
});
|
||||
|
||||
describe('postPaySteps', () => {
|
||||
const mockUid = faker.string.uuid();
|
||||
const mockSubscription = StripeResponseFactory(StripeSubscriptionFactory());
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(customerManager, 'setTaxId').mockResolvedValue();
|
||||
jest.spyOn(profileClient, 'deleteCache').mockResolvedValue('test');
|
||||
});
|
||||
|
||||
it('success', async () => {
|
||||
const mockCart = ResultCartFactory();
|
||||
const mockSubscription = StripeResponseFactory(
|
||||
StripeSubscriptionFactory()
|
||||
);
|
||||
|
||||
jest.spyOn(customerManager, 'setTaxId').mockResolvedValue();
|
||||
|
||||
await checkoutService.postPaySteps(mockCart, mockSubscription);
|
||||
await checkoutService.postPaySteps(mockCart, mockSubscription, mockUid);
|
||||
|
||||
expect(customerManager.setTaxId).toHaveBeenCalledWith(
|
||||
mockSubscription.customer,
|
||||
mockSubscription.currency
|
||||
);
|
||||
|
||||
expect(privateMethod).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('success - adds coupon code to subscription metadata if it exists', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
couponCode: faker.string.uuid(),
|
||||
});
|
||||
const mockSubscription = StripeResponseFactory(
|
||||
StripeSubscriptionFactory()
|
||||
);
|
||||
const mockUpdatedSubscription = StripeResponseFactory(
|
||||
StripeSubscriptionFactory({
|
||||
metadata: {
|
||||
|
@ -447,17 +479,17 @@ describe('CheckoutService', () => {
|
|||
})
|
||||
);
|
||||
|
||||
jest.spyOn(customerManager, 'setTaxId').mockResolvedValue();
|
||||
jest
|
||||
.spyOn(subscriptionManager, 'update')
|
||||
.mockResolvedValue(mockUpdatedSubscription);
|
||||
|
||||
await checkoutService.postPaySteps(mockCart, mockSubscription);
|
||||
await checkoutService.postPaySteps(mockCart, mockSubscription, mockUid);
|
||||
|
||||
expect(customerManager.setTaxId).toHaveBeenCalledWith(
|
||||
mockSubscription.customer,
|
||||
mockSubscription.currency
|
||||
);
|
||||
expect(privateMethod).toHaveBeenCalled();
|
||||
expect(subscriptionManager.update).toHaveBeenCalledWith(
|
||||
mockSubscription.id,
|
||||
{
|
||||
|
@ -497,6 +529,7 @@ describe('CheckoutService', () => {
|
|||
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue({
|
||||
uid: mockCart.uid as string,
|
||||
customer: mockCustomer,
|
||||
email: faker.internet.email(),
|
||||
enableAutomaticTax: true,
|
||||
promotionCode: mockPromotionCode,
|
||||
price: mockPrice,
|
||||
|
@ -505,7 +538,7 @@ describe('CheckoutService', () => {
|
|||
.spyOn(paymentMethodManager, 'attach')
|
||||
.mockResolvedValue(mockPaymentMethod);
|
||||
jest.spyOn(customerManager, 'update').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(mockStatsd, 'increment');
|
||||
jest.spyOn(statsd, 'increment');
|
||||
jest
|
||||
.spyOn(subscriptionManager, 'create')
|
||||
.mockResolvedValue(mockSubscription);
|
||||
|
@ -552,12 +585,9 @@ describe('CheckoutService', () => {
|
|||
});
|
||||
|
||||
it('increments the statsd counter', async () => {
|
||||
expect(mockStatsd.increment).toHaveBeenCalledWith(
|
||||
'stripe_subscription',
|
||||
{
|
||||
payment_provider: 'stripe',
|
||||
}
|
||||
);
|
||||
expect(statsd.increment).toHaveBeenCalledWith('stripe_subscription', {
|
||||
payment_provider: 'stripe',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the subscription', async () => {
|
||||
|
@ -618,6 +648,7 @@ describe('CheckoutService', () => {
|
|||
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue({
|
||||
uid: mockCart.uid as string,
|
||||
customer: mockCustomer,
|
||||
email: faker.internet.email(),
|
||||
enableAutomaticTax: true,
|
||||
promotionCode: mockPromotionCode,
|
||||
price: mockPrice,
|
||||
|
@ -628,7 +659,7 @@ describe('CheckoutService', () => {
|
|||
jest
|
||||
.spyOn(paypalBillingAgreementManager, 'retrieveOrCreateId')
|
||||
.mockResolvedValue(mockBillingAgreementId);
|
||||
jest.spyOn(mockStatsd, 'increment');
|
||||
jest.spyOn(statsd, 'increment');
|
||||
jest
|
||||
.spyOn(subscriptionManager, 'create')
|
||||
.mockResolvedValue(mockSubscription);
|
||||
|
@ -668,12 +699,9 @@ describe('CheckoutService', () => {
|
|||
});
|
||||
|
||||
it('increments the statsd counter', async () => {
|
||||
expect(mockStatsd.increment).toHaveBeenCalledWith(
|
||||
'stripe_subscription',
|
||||
{
|
||||
payment_provider: 'paypal',
|
||||
}
|
||||
);
|
||||
expect(statsd.increment).toHaveBeenCalledWith('stripe_subscription', {
|
||||
payment_provider: 'paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the subscription', async () => {
|
||||
|
|
|
@ -27,9 +27,11 @@ import {
|
|||
StripeCustomer,
|
||||
StripePromotionCode,
|
||||
} from '@fxa/payments/stripe';
|
||||
import { ProfileClient } from '@fxa/profile/client';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import { ProductConfigurationManager } from '@fxa/shared/cms';
|
||||
import { StatsDService } from '@fxa/shared/metrics/statsd';
|
||||
import { NotifierService } from '@fxa/shared/notifier';
|
||||
import {
|
||||
CartTotalMismatchError,
|
||||
CartEligibilityMismatchError,
|
||||
|
@ -51,15 +53,32 @@ export class CheckoutService {
|
|||
private customerManager: CustomerManager,
|
||||
private eligibilityService: EligibilityService,
|
||||
private invoiceManager: InvoiceManager,
|
||||
private notifierService: NotifierService,
|
||||
private paymentMethodManager: PaymentMethodManager,
|
||||
private paypalBillingAgreementManager: PaypalBillingAgreementManager,
|
||||
private paypalCustomerManager: PaypalCustomerManager,
|
||||
private productConfigurationManager: ProductConfigurationManager,
|
||||
private profileClient: ProfileClient,
|
||||
private promotionCodeManager: PromotionCodeManager,
|
||||
private subscriptionManager: SubscriptionManager,
|
||||
@Inject(StatsDService) private statsd: StatsD
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Reload the customer data to reflect a change.
|
||||
*/
|
||||
private async customerChanged(uid: string) {
|
||||
await this.profileClient.deleteCache(uid);
|
||||
|
||||
this.notifierService.send({
|
||||
event: 'profileDataChange',
|
||||
data: {
|
||||
ts: Date.now() / 1000,
|
||||
uid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async prePaySteps(cart: ResultCart, customerData: CheckoutCustomerData) {
|
||||
const taxAddress = cart.taxAddress as any as TaxAddress;
|
||||
|
||||
|
@ -182,18 +201,23 @@ export class CheckoutService {
|
|||
return {
|
||||
uid: uid,
|
||||
customer,
|
||||
email: cart.email,
|
||||
enableAutomaticTax,
|
||||
promotionCode,
|
||||
price,
|
||||
};
|
||||
}
|
||||
|
||||
async postPaySteps(cart: ResultCart, subscription: StripeSubscription) {
|
||||
async postPaySteps(
|
||||
cart: ResultCart,
|
||||
subscription: StripeSubscription,
|
||||
uid: string
|
||||
) {
|
||||
const { customer: customerId, currency } = subscription;
|
||||
|
||||
await this.customerManager.setTaxId(customerId, currency);
|
||||
|
||||
// TODO: call customerChanged
|
||||
await this.customerChanged(uid);
|
||||
|
||||
if (cart.couponCode) {
|
||||
const subscriptionMetadata = {
|
||||
|
@ -214,7 +238,7 @@ export class CheckoutService {
|
|||
paymentMethodId: string,
|
||||
customerData: CheckoutCustomerData
|
||||
) {
|
||||
const { customer, enableAutomaticTax, promotionCode, price } =
|
||||
const { uid, customer, enableAutomaticTax, promotionCode, price } =
|
||||
await this.prePaySteps(cart, customerData);
|
||||
|
||||
await this.paymentMethodManager.attach(paymentMethodId, {
|
||||
|
@ -281,7 +305,7 @@ export class CheckoutService {
|
|||
);
|
||||
}
|
||||
|
||||
await this.postPaySteps(cart, subscription);
|
||||
await this.postPaySteps(cart, subscription, uid);
|
||||
}
|
||||
|
||||
async payWithPaypal(
|
||||
|
@ -360,6 +384,6 @@ export class CheckoutService {
|
|||
await this.paypalBillingAgreementManager.cancel(billingAgreementId);
|
||||
}
|
||||
|
||||
await this.postPaySteps(cart, subscription);
|
||||
await this.postPaySteps(cart, subscription, uid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +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';
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
constructor() {}
|
||||
|
||||
// TODO: this method should be moved down to the manager layer
|
||||
async customerChanged(uid: string, email: string) {
|
||||
// @todo - Unblocked by FXA-9274
|
||||
//const devices = await this.db.devices(uid);
|
||||
// @todo - Unblocked by FXA-9275
|
||||
//await this.profile.deleteCache(uid);
|
||||
// @todo - Unblocked by FXA-9276
|
||||
//await this.push.notifyProfileUpdated(uid, devices);
|
||||
// @todo - Unblocked by FXA-9277
|
||||
//this.log.notifyAttachedServices('profileDataChange', {} as any, {
|
||||
// uid,
|
||||
// email,
|
||||
//});
|
||||
}
|
||||
}
|
|
@ -25,22 +25,25 @@ import {
|
|||
PromotionCodeManager,
|
||||
SubscriptionManager,
|
||||
} from '@fxa/payments/customer';
|
||||
import { PaymentsGleanManager } from '@fxa/payments/metrics';
|
||||
import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider';
|
||||
import { AccountCustomerManager, StripeClient } from '@fxa/payments/stripe';
|
||||
import { ProfileClient } from '@fxa/profile/client';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import { ProductConfigurationManager, StrapiClient } from '@fxa/shared/cms';
|
||||
import { FirestoreProvider } from '@fxa/shared/db/firestore';
|
||||
import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
|
||||
import { GeoDBManager, GeoDBNestFactory } from '@fxa/shared/geodb';
|
||||
import { LocalizerRscFactoryProvider } from '@fxa/shared/l10n/server';
|
||||
import { logger, LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { StatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { PaymentsGleanManager } from '@fxa/payments/metrics';
|
||||
import { NotifierService, NotifierSnsProvider } from '@fxa/shared/notifier';
|
||||
|
||||
import { RootConfig } from './config';
|
||||
import { NextJSActionsService } from './nextjs-actions.service';
|
||||
import { validate } from '../config.utils';
|
||||
import { CurrencyManager } from '@fxa/payments/currency';
|
||||
import { PaymentsEmitterService } from '../emitter/emitter.service';
|
||||
import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -80,6 +83,8 @@ import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider';
|
|||
InvoiceManager,
|
||||
LocalizerRscFactoryProvider,
|
||||
NextJSActionsService,
|
||||
NotifierService,
|
||||
NotifierSnsProvider,
|
||||
PaymentMethodManager,
|
||||
PaypalBillingAgreementManager,
|
||||
PayPalClient,
|
||||
|
@ -87,6 +92,7 @@ import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider';
|
|||
PriceManager,
|
||||
ProductConfigurationManager,
|
||||
ProductManager,
|
||||
ProfileClient,
|
||||
PromotionCodeManager,
|
||||
StatsDProvider,
|
||||
StrapiClient,
|
||||
|
@ -95,6 +101,7 @@ import { PaymentsGleanFactory } from '@fxa/payments/metrics/provider';
|
|||
PaymentsGleanFactory,
|
||||
PaymentsGleanManager,
|
||||
PaymentsEmitterService,
|
||||
{ provide: LOGGER_PROVIDER, useValue: logger },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -14,6 +14,8 @@ import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.conf
|
|||
import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config';
|
||||
import { PaymentsGleanConfig } from '@fxa/payments/metrics';
|
||||
import { CurrencyConfig } from 'libs/payments/currency/src/lib/currency.config';
|
||||
import { ProfileClientConfig } from '@fxa/profile/client';
|
||||
import { NotifierSnsConfig } from '@fxa/shared/notifier';
|
||||
|
||||
export class RootConfig {
|
||||
@Type(() => MySQLConfig)
|
||||
|
@ -64,4 +66,14 @@ export class RootConfig {
|
|||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly gleanConfig!: Partial<PaymentsGleanConfig>;
|
||||
|
||||
@Type(() => ProfileClientConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly profileClientConfig!: Partial<ProfileClientConfig>;
|
||||
|
||||
@Type(() => NotifierSnsConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly notifierSnsConfig!: Partial<NotifierSnsConfig>;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* 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 { Test } from '@nestjs/testing';
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { ProfileClient } from './profile.client';
|
||||
import { MockProfileClientConfigProvider } from './profile.config';
|
||||
|
@ -47,11 +47,9 @@ describe('ProfileClient', () => {
|
|||
jest
|
||||
.spyOn(profileClient as any, 'makeRequest')
|
||||
.mockRejectedValue(new Error('fail'));
|
||||
try {
|
||||
await profileClient.deleteCache('test');
|
||||
} catch (e) {
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
await expect(profileClient.deleteCache('test')).rejects.toThrow();
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -65,11 +63,12 @@ describe('ProfileClient', () => {
|
|||
jest
|
||||
.spyOn(profileClient as any, 'makeRequest')
|
||||
.mockRejectedValue(new Error('fail'));
|
||||
try {
|
||||
await profileClient.updateDisplayName('test', 'test');
|
||||
} catch (e) {
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
await expect(
|
||||
profileClient.updateDisplayName('test', 'test')
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { Inject, Injectable, LoggerService } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { LoggerService } from '@nestjs/common';
|
||||
import Agent from 'agentkeepalive';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { ProfileClientConfig } from './profile.config';
|
||||
|
|
|
@ -7,7 +7,7 @@ import { faker } from '@faker-js/faker';
|
|||
import { Provider } from '@nestjs/common';
|
||||
|
||||
export class ProfileClientConfig {
|
||||
@IsUrl()
|
||||
@IsUrl({ require_tld: false })
|
||||
public readonly url!: string;
|
||||
|
||||
@IsString()
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { StatsD } from 'hot-shots';
|
||||
|
||||
import { MockStatsDProvider, StatsDService } from '@fxa/shared/metrics/statsd';
|
||||
import { NotifierService } from './notifier.service';
|
||||
import {
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
import { LOGGER_PROVIDER } from '@fxa/shared/log';
|
||||
import { StatsDService } from '@fxa/shared/metrics/statsd';
|
||||
import { Inject, Injectable, LoggerService } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { LoggerService } from '@nestjs/common';
|
||||
import { AWSError, SNS } from 'aws-sdk';
|
||||
import { StatsD } from 'hot-shots';
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ export class NotifierSnsConfig {
|
|||
@IsString()
|
||||
public readonly snsTopicArn!: string;
|
||||
|
||||
@IsUrl()
|
||||
@IsUrl({ require_tld: false })
|
||||
public readonly snsTopicEndpoint!: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SNS } from 'aws-sdk';
|
||||
import { MockNotifierSnsConfig } from './notifier.sns.config';
|
||||
import {
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierSnsFactory,
|
||||
NotifierSnsService,
|
||||
} from './notifier.sns.provider';
|
||||
|
||||
|
@ -22,14 +23,10 @@ jest.mock('aws-sdk', () => {
|
|||
describe('NotifierSnsFactory', () => {
|
||||
let sns: SNS;
|
||||
|
||||
const mockConfig = {
|
||||
snsTopicArn: 'arn:aws:sns:us-east-1:100010001000:fxa-account-change-dev',
|
||||
snsTopicEndpoint: 'http://localhost:4100/',
|
||||
};
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockImplementation((key: string) => {
|
||||
if (key === 'notifier.sns') {
|
||||
return mockConfig;
|
||||
return MockNotifierSnsConfig;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
@ -39,11 +36,8 @@ describe('NotifierSnsFactory', () => {
|
|||
jest.clearAllMocks();
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NotifierSnsFactory,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
LegacyNotifierSnsFactory,
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
@ -53,8 +47,8 @@ describe('NotifierSnsFactory', () => {
|
|||
it('should provide statsd', async () => {
|
||||
expect(sns).toBeDefined();
|
||||
expect(mockSNS).toBeCalledWith({
|
||||
endpoint: mockConfig.snsTopicEndpoint,
|
||||
region: 'us-east-1',
|
||||
endpoint: MockNotifierSnsConfig.snsTopicEndpoint,
|
||||
region: 'us-west-2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,19 @@ export function setupSns(config: NotifierSnsConfig) {
|
|||
* Factory for providing access to SNS
|
||||
*/
|
||||
export const NotifierSnsService = Symbol('NOTIFIER_SNS');
|
||||
export const NotifierSnsFactory: Provider<SNS> = {
|
||||
export const NotifierSnsProvider: Provider<SNS> = {
|
||||
provide: NotifierSnsService,
|
||||
useFactory: (config: NotifierSnsConfig) => {
|
||||
if (config == null) {
|
||||
throw new Error('Could not locate notifier.sns config');
|
||||
}
|
||||
const sns = setupSns(config);
|
||||
return sns;
|
||||
},
|
||||
inject: [NotifierSnsConfig],
|
||||
};
|
||||
|
||||
export const LegacyNotifierSnsFactory: Provider<SNS> = {
|
||||
provide: NotifierSnsService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const config = configService.get<NotifierSnsConfig>('notifier.sns');
|
||||
|
|
|
@ -17,7 +17,7 @@ import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
|||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import {
|
||||
LegacyNotifierServiceProvider,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierSnsFactory,
|
||||
} from '@fxa/shared/notifier';
|
||||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
@ -89,8 +89,8 @@ const version = getVersionInfo(__dirname);
|
|||
provide: LOGGER_PROVIDER,
|
||||
useClass: MozLoggerService,
|
||||
},
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierServiceProvider,
|
||||
LegacyNotifierSnsFactory,
|
||||
LegacyStatsDProvider,
|
||||
],
|
||||
})
|
||||
|
|
|
@ -7,7 +7,7 @@ import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
|||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import {
|
||||
LegacyNotifierServiceProvider,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierSnsFactory,
|
||||
} from '@fxa/shared/notifier';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BackendModule } from '../backend/backend.module';
|
||||
|
@ -31,8 +31,8 @@ import { RelyingPartyResolver } from './relying-party/relying-party.resolver';
|
|||
AccountResolver,
|
||||
EmailBounceResolver,
|
||||
LegacyStatsDProvider,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierServiceProvider,
|
||||
LegacyNotifierSnsFactory,
|
||||
{
|
||||
provide: LOGGER_PROVIDER,
|
||||
useClass: MozLoggerService,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
|||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import {
|
||||
LegacyNotifierServiceProvider,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierSnsFactory,
|
||||
} from '@fxa/shared/notifier';
|
||||
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
|
||||
|
||||
|
@ -56,8 +56,8 @@ const version = getVersionInfo(__dirname);
|
|||
providers: [
|
||||
LegacyStatsDProvider,
|
||||
MozLoggerService,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierServiceProvider,
|
||||
LegacyNotifierSnsFactory,
|
||||
ComplexityPlugin,
|
||||
{
|
||||
provide: LOGGER_PROVIDER,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { LegacyStatsDProvider } from '@fxa/shared/metrics/statsd';
|
|||
import { MozLoggerService } from '@fxa/shared/mozlog';
|
||||
import {
|
||||
LegacyNotifierServiceProvider,
|
||||
NotifierSnsFactory,
|
||||
LegacyNotifierSnsFactory,
|
||||
} from '@fxa/shared/notifier';
|
||||
import {
|
||||
HttpException,
|
||||
|
@ -59,11 +59,11 @@ export const GraphQLConfigFactory = async (
|
|||
AccountResolver,
|
||||
ClientInfoResolver,
|
||||
CustomsService,
|
||||
LegacyNotifierServiceProvider,
|
||||
LegacyNotifierSnsFactory,
|
||||
LegacyStatsDProvider,
|
||||
LegalResolver,
|
||||
LegacyNotifierServiceProvider,
|
||||
MozLoggerService,
|
||||
NotifierSnsFactory,
|
||||
SentryPlugin,
|
||||
SessionResolver,
|
||||
SubscriptionResolver,
|
||||
|
|
Загрузка…
Ссылка в новой задаче