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