feat(libs/payments): Implement customerChanged

This commit is contained in:
Lisa Chan 2024-09-25 18:18:37 -04:00
Родитель ae82b7f823
Коммит c20c754e4e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 9052E177BBC5E764
22 изменённых файлов: 239 добавлений и 216 удалений

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

@ -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,12 +164,6 @@ 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(),
@ -160,25 +176,37 @@ describe('CartService', () => {
}),
ip: faker.internet.ipv4(),
};
const taxAddress = TaxAddressFactory();
const mockPrice = StripePriceFactory();
const mockInvoicePreview = InvoicePreviewFactory();
const mockResolvedCurrency = faker.finance.currencyCode();
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();
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',
{
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',
{
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) {
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) {
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,