Files
firefox-accounts-mirror/libs/payments/ui/src/lib/nestapp/nextjs-actions.service.ts
Davey Alvarez 8494d581d3 polish(payments-next): Adjust styling and server action for terms
Because:

* A few code style and css-style tweaks were needed as a follow to the terms page release

This commit:

* addresses some of the outcomes from style conversations

Closes #N/A
2025-12-05 09:33:13 -08:00

915 lines
29 KiB
TypeScript

/* 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 { Inject, Injectable, type LoggerService, Logger } from '@nestjs/common';
import { GoogleManager } from '@fxa/google';
import {
CartInvalidStateForActionError,
CartService,
SuccessCartDTO,
TaxChangeAllowedStatus,
TaxService,
} from '@fxa/payments/cart';
import { ContentServerManager } from '@fxa/payments/content-server';
import { CurrencyManager } from '@fxa/payments/currency';
import {
SubscriptionManagementService,
ChurnInterventionService,
} from '@fxa/payments/management';
import {
CheckoutTokenManager,
PaypalBillingAgreementManager,
} from '@fxa/payments/paypal';
import {
ProductConfigError,
ProductConfigurationManager,
} from '@fxa/shared/cms';
import { CartErrorReasonId, CartState } from '@fxa/shared/db/mysql/account';
import { SanitizeExceptions } from '@fxa/shared/error';
import { GeoDBManager } from '@fxa/shared/geodb';
import { AccountCustomerNotFoundError } from '@fxa/payments/stripe';
import { CheckoutCartWithPaypalActionArgs } from './validators/CheckoutCartWithPaypalActionArgs';
import { CheckoutCartWithStripeActionArgs } from './validators/CheckoutCartWithStripeActionArgs';
import { FetchCMSDataActionArgs } from './validators/FetchCMSDataActionArgs';
import { FinalizeCartWithErrorArgs } from './validators/FinalizeCartWithErrorArgs';
import { GetCartActionArgs } from './validators/GetCartActionArgs';
import { GetChurnInterventionDataActionArgs } from './validators/GetChurnInterventionDataActionArgs';
import { GetPayPalCheckoutTokenArgs } from './validators/GetPayPalCheckoutTokenArgs';
import { GetSubManPageContentActionArgs } from './validators/GetSubManPageContentActionArgs';
import { GetSubManPageContentActionResult } from './validators/GetSubManPageContentActionResult';
import { RestartCartActionArgs } from './validators/RestartCartActionArgs';
import { SetupCartActionArgs } from './validators/SetupCartActionArgs';
import { UpdateCartActionArgs } from './validators/UpdateCartActionArgs';
import { RecordEmitterEventArgs } from './validators/RecordEmitterEvent';
import { GetCouponArgs } from './validators/GetCouponArgs';
import { GetCouponResult } from './validators/GetCouponResult';
import { PaymentsEmitterService } from '@fxa/payments/events';
import { FinalizeProcessingCartActionArgs } from './validators/finalizeProcessingCartActionArgs';
import { RedeemChurnCouponActionArgs } from './validators/RedeemChurnCouponActionArgs';
import { SubmitNeedsInputActionArgs } from './validators/SubmitNeedsInputActionArgs';
import { GetNeedsInputActionArgs } from './validators/GetNeedsInputActionArgs';
import { ValidatePostalCodeActionArgs } from './validators/ValidatePostalCodeActionArgs';
import { DetermineCurrencyActionArgs } from './validators/DetermineCurrencyActionArgs';
import { DetermineStaySubscribedEligibilityActionArgs } from './validators/DetermineStaySubscribedEligibilityActionArgs';
import { NextIOValidator } from './NextIOValidator';
import type {
CommonMetrics,
PaymentProvidersType,
} from '@fxa/payments/metrics';
import { GetCartActionResult } from './validators/GetCartActionResult';
import { GetChurnInterventionDataActionResult } from './validators/GetChurnInterventionDataActionResult';
import { GetSuccessCartActionResult } from './validators/GetSuccessCartActionResult';
import {
CouponErrorCannotRedeem,
PromotionCodeSanitizedError,
TaxAddress,
type SubplatInterval,
} from '@fxa/payments/customer';
import { EligibilityService, LocationStatus } from '@fxa/payments/eligibility';
import { WithTypeCachableAsyncLocalStorage } from '@fxa/shared/db/type-cacheable';
import { GetPayPalCheckoutTokenResult } from './validators/GetPayPalCheckoutTokenResult';
import { FetchCMSDataActionResult } from './validators/FetchCMSDataActionResult';
import { getNeedsInputActionResult } from './validators/GetNeedsInputActionResult';
import { GetMetricsFlowActionResult } from './validators/GetMetricsFlowActionResult';
import { ValidatePostalCodeActionResult } from './validators/ValidatePostalCodeActionResult';
import { DetermineCurrencyActionResult } from './validators/DetermineCurrencyActionResult';
import { SetupCartActionResult } from './validators/SetupCartActionResult';
import { RestartCartActionResult } from './validators/RestartCartActionResult';
import { GetCancelFlowContentActionResult } from './validators/GetCancelFlowContentActionResult';
import { GetStaySubscribedFlowContentActionResult } from './validators/GetStaySubscribedFlowContentActionResult';
import { GetSubscriptionPageContentActionArgs } from './validators/GetSubscriptionPageContentActionArgs';
import { GetTaxAddressArgs } from './validators/GetTaxAddressArgs';
import { GetTaxAddressResult } from './validators/GetTaxAddressResult';
import {
CartVersionMismatchError,
InvalidPromoCodeCartError,
} from 'libs/payments/cart/src/lib/cart.error';
import { UpdateCartActionResult } from './validators/UpdateCartActionResult';
import { ValidateLocationActionResult } from './validators/ValidateLocationActionResult';
import { ValidateLocationActionArgs } from './validators/ValidateLocationActionArgs';
import { UpdateTaxAddressActionArgs } from './validators/UpdateTaxAddressActionArgs';
import { UpdateTaxAddressActionResult } from './validators/UpdateTaxAddressActionResult';
import {
CaptureTimingWithStatsD,
StatsDService,
type StatsD,
} from '@fxa/shared/metrics/statsd';
import { GetCartStateActionArgs } from './validators/GetCartStateActionArgs';
import { GetCartStateActionResult } from './validators/GetCartStateActionResult';
import type { SubscriptionAttributionParams } from '@fxa/payments/cart';
import { ServerLogActionArgs } from './validators/ServerLogActionArgs';
import { ProfileClient } from '@fxa/profile/client';
import { GetUserinfoResult } from './validators/GetUserinfoResult';
import { GetUserinfoArgs } from './validators/GetUserinfoArgs';
import { GetStripePaymentManagementDetailsArgs } from './validators/GetStripeClientSessionActionArgs';
import { GetStripePaymentManagementDetailsResult } from './validators/GetStripePaymentManagementDetailsResult';
import { UpdateStripePaymentDetailsArgs } from './validators/UpdateStripePaymentDetailsActionArgs';
import { UpdateStripePaymentDetailsResult } from './validators/UpdateStripePaymentDetailsActionResult';
import { SetDefaultStripePaymentDetailsActionArgs } from './validators/SetDefaultStripePaymentDetailsActionArgs';
import { CancelSubscriptionAtPeriodEndActionArgs } from './validators/CancelSubscriptionAtPeriodEndActionArgs';
import { CancelSubscriptionAtPeriodEndActionResult } from './validators/CancelSubscriptionAtPeriodEndActionResult';
import { RedeemChurnCouponActionResult } from './validators/RedeemChurnCouponActionResult';
import { ResubscribeSubscriptionActionArgs } from './validators/ResubscribeSubscriptionActionArgs';
import { ResubscribeSubscriptionActionResult } from './validators/ResubscribeSubscriptionActionResult';
import { GetPaypalBillingAgreementActiveIdArgs } from './validators/GetPaypalBillingAgreementActiveIdArgs';
import { GetPaypalBillingAgreementActiveIdResult } from './validators/GetPaypalBillingAgreementActiveIdResult';
import { CreatePaypalBillingAgreementIdArgs } from './validators/CreatePaypalBillingAgreementIdArgs';
import { DetermineCurrencyForCustomerActionArgs } from './validators/DetermineCurrencyForCustomerActionArgs';
import { DetermineCurrencyForCustomerActionResult } from './validators/DetermineCurrencyForCustomerActionResult';
import { DetermineStaySubscribedEligibilityActionResult } from './validators/DetermineStaySubscribedEligibilityActionResult';
import { NimbusManager } from '@fxa/payments/experiments';
import { GetExperimentsActionArgs } from './validators/GetExperimentsActionArgs';
import { GetExperimentsActionResult } from './validators/GetExperimentsActionResult';
import { GetCMSChurnInterventionActionResult } from './validators/GetCMSChurnInterventionActionResult';
import { GetCMSChurnInterventionActionArgs } from './validators/GetCMSChurnInterventionActionArgs';
/**
* ANY AND ALL methods exposed via this service should be considered publicly accessible and callable with any arguments.
* ALL server actions must use this service as a proxy to other NestJS services.
*/
@Injectable()
export class NextJSActionsService {
constructor(
private cartService: CartService,
private taxService: TaxService,
private checkoutTokenManager: CheckoutTokenManager,
private churnInterventionService: ChurnInterventionService,
private contentServerManager: ContentServerManager,
private emitterService: PaymentsEmitterService,
private googleManager: GoogleManager,
private geodbManager: GeoDBManager,
private currencyManager: CurrencyManager,
private eligibilityService: EligibilityService,
private productConfigurationManager: ProductConfigurationManager,
private profileClient: ProfileClient,
private subscriptionManagementService: SubscriptionManagementService,
private paypalBillingAgreementManager: PaypalBillingAgreementManager,
private nimbusManager: NimbusManager,
@Inject(StatsDService) public statsd: StatsD,
@Inject(Logger) private log: LoggerService
) {}
@SanitizeExceptions()
@NextIOValidator(
CancelSubscriptionAtPeriodEndActionArgs,
CancelSubscriptionAtPeriodEndActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async cancelSubscriptionAtPeriodEnd(args: {
uid: string;
subscriptionId: string;
}) {
try {
await this.subscriptionManagementService.cancelSubscriptionAtPeriodEnd(
args.uid,
args.subscriptionId
);
return {
ok: true,
};
} catch (error) {
return {
ok: false,
};
}
}
@SanitizeExceptions()
@NextIOValidator(GetCartStateActionArgs, GetCartStateActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getCartState(args: { cartId: string }) {
const cart = await this.cartService.getCartState(args.cartId);
return cart;
}
@SanitizeExceptions()
@NextIOValidator(GetCartActionArgs, GetCartActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getCart(args: { cartId: string }) {
return this.cartService.getCart(args.cartId);
}
@SanitizeExceptions()
@NextIOValidator(GetExperimentsActionArgs, GetExperimentsActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getExperiments(args: {
language: string;
ip: string;
experimentationPreview: boolean;
fxaUid?: string;
experimentationId?: string;
}) {
const nimbusUserId = this.nimbusManager.generateNimbusId(
args.fxaUid,
args.experimentationId
);
const location = this.geodbManager.getTaxAddress(args.ip);
const experiments = await this.nimbusManager.fetchExperiments({
nimbusUserId,
language: args.language,
region: location?.countryCode,
preview: args.experimentationPreview,
});
if (experiments) {
return {
experiments,
};
} else {
return undefined;
}
}
@SanitizeExceptions({
allowlist: [CartInvalidStateForActionError],
})
@NextIOValidator(GetCartActionArgs, GetSuccessCartActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getSuccessCart(args: { cartId: string }): Promise<SuccessCartDTO> {
const cart = await this.cartService.getCart(args.cartId);
if (cart.state !== CartState.SUCCESS) {
throw new Error('Cart is not in success state');
}
return cart;
}
@SanitizeExceptions()
@NextIOValidator(
GetChurnInterventionDataActionArgs,
GetChurnInterventionDataActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getChurnInterventionEntryData(args: {
customerId: string;
churnInterventionId: string;
}) {
const data = await this.churnInterventionService.getChurnInterventionForCustomerId(
args.customerId,
args.churnInterventionId
);
return data;
}
@SanitizeExceptions()
@NextIOValidator(
DetermineStaySubscribedEligibilityActionArgs,
DetermineStaySubscribedEligibilityActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async determineStaySubscribedEligibility(args: {
uid: string;
subscriptionId: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
return await this.churnInterventionService.determineStaySubscribedEligibility(
args.uid,
args.subscriptionId,
args.acceptLanguage,
args.selectedLanguage
);
}
@SanitizeExceptions()
@NextIOValidator(RedeemChurnCouponActionArgs, RedeemChurnCouponActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async redeemChurnCoupon(args: {
uid: string;
subscriptionId: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
return await this.churnInterventionService.redeemChurnCoupon(
args.uid,
args.subscriptionId,
args.acceptLanguage,
args.selectedLanguage
);
}
@SanitizeExceptions({
allowlist: [PromotionCodeSanitizedError, CartVersionMismatchError],
})
@NextIOValidator(UpdateCartActionArgs, UpdateCartActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async updateCart(args: {
cartId: string;
version: number;
cartDetails: {
uid?: string;
taxAddress?: {
countryCode: string;
postalCode: string;
};
couponCode?: string;
};
}) {
return this.cartService.updateCart(
args.cartId,
args.version,
args.cartDetails
);
}
@SanitizeExceptions()
@NextIOValidator(GetCouponArgs, GetCouponResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getCoupon(args: { cartId: string; version: number }) {
const couponCode = await this.cartService.getCoupon({
cartId: args.cartId,
version: args.version,
});
return couponCode;
}
@SanitizeExceptions()
@NextIOValidator(RestartCartActionArgs, RestartCartActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async restartCart(args: { cartId: string }) {
const cart = await this.cartService.restartCart(args.cartId);
return cart;
}
@SanitizeExceptions({
allowlist: [
CouponErrorCannotRedeem,
InvalidPromoCodeCartError,
ProductConfigError,
],
})
@NextIOValidator(SetupCartActionArgs, SetupCartActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async setupCart(args: {
interval: SubplatInterval;
offeringConfigId: string;
experiment?: string;
promoCode?: string;
uid?: string;
taxAddress: TaxAddress;
}) {
const cart = await this.cartService.setupCart({
...args,
});
return cart;
}
@SanitizeExceptions()
@NextIOValidator(FinalizeCartWithErrorArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async finalizeCartWithError(args: {
cartId: string;
errorReasonId: CartErrorReasonId;
}) {
await this.cartService.finalizeCartWithError(
args.cartId,
args.errorReasonId
);
}
@SanitizeExceptions()
@NextIOValidator(FinalizeProcessingCartActionArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async finalizeProcessingCart(args: { cartId: string }) {
await this.cartService.finalizeProcessingCart(args.cartId);
}
@SanitizeExceptions()
@NextIOValidator(GetPayPalCheckoutTokenArgs, GetPayPalCheckoutTokenResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getPayPalCheckoutToken(args: { currencyCode: string }) {
const token = await this.checkoutTokenManager.get(args.currencyCode);
return {
token,
};
}
@SanitizeExceptions()
@NextIOValidator(
GetPaypalBillingAgreementActiveIdArgs,
GetPaypalBillingAgreementActiveIdResult
)
@CaptureTimingWithStatsD()
async getPaypalBillingAgreementActiveId(args: { uid: string }) {
const paypalBillingAgreementId =
await this.paypalBillingAgreementManager.retrieveActiveId(args.uid);
return {
paypalBillingAgreementId,
};
}
@SanitizeExceptions()
@NextIOValidator(CreatePaypalBillingAgreementIdArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async createPayPalBillingAgreementId(args: { uid: string; token: string }) {
await this.subscriptionManagementService.createPaypalBillingAgreementId(
args.uid,
args.token
);
}
@SanitizeExceptions()
@NextIOValidator(GetTaxAddressArgs, GetTaxAddressResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getTaxAddress(args: { ipAddress: string; uid?: string }) {
const result = await this.taxService.getTaxAddress(
args.ipAddress,
args.uid
);
return {
result,
};
}
@SanitizeExceptions()
@NextIOValidator(CheckoutCartWithPaypalActionArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async checkoutCartWithPaypal(args: {
cartId: string;
version: number;
customerData: { locale: string; displayName: string };
attribution: SubscriptionAttributionParams;
sessionUid?: string;
token?: string;
}) {
await this.cartService.checkoutCartWithPaypal(
args.cartId,
args.version,
args.customerData,
args.attribution,
args.sessionUid,
args.token
);
}
@SanitizeExceptions()
@NextIOValidator(CheckoutCartWithStripeActionArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async checkoutCartWithStripe(args: {
cartId: string;
version: number;
confirmationTokenId: string;
customerData: { locale: string; displayName: string };
attribution: SubscriptionAttributionParams;
sessionUid?: string;
}) {
await this.cartService.checkoutCartWithStripe(
args.cartId,
args.version,
args.confirmationTokenId,
args.customerData,
args.attribution,
args.sessionUid
);
}
@SanitizeExceptions({ allowlist: [ProductConfigError] })
@NextIOValidator(FetchCMSDataActionArgs, FetchCMSDataActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async fetchCMSData(args: {
offeringId: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
const offering = await this.productConfigurationManager.fetchCMSData(
args.offeringId,
args.acceptLanguage || undefined,
args.selectedLanguage
);
return offering;
}
@SanitizeExceptions()
@NextIOValidator(
GetSubManPageContentActionArgs,
GetSubManPageContentActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getSubManPageContent(args: {
uid: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
const result = await this.subscriptionManagementService.getPageContent(
args.uid,
args.acceptLanguage || undefined,
args.selectedLanguage
);
return result;
}
@SanitizeExceptions()
@NextIOValidator(RecordEmitterEventArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async recordEmitterEvent(args: {
eventName: string;
requestArgs: CommonMetrics;
paymentProvider: PaymentProvidersType | undefined;
}) {
const { eventName, requestArgs, paymentProvider } = args;
switch (eventName) {
case 'checkoutView':
case 'checkoutEngage': {
this.emitterService.getEmitter().emit(eventName, {
...requestArgs,
});
break;
}
case 'checkoutSubmit':
case 'checkoutSuccess':
case 'checkoutFail': {
this.emitterService.getEmitter().emit(eventName, {
...requestArgs,
paymentProvider: paymentProvider,
});
break;
}
default: {
throw new Error(`Event ${args.eventName} not supported`);
}
}
}
@SanitizeExceptions()
@NextIOValidator(GetNeedsInputActionArgs, getNeedsInputActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getNeedsInput(args: { cartId: string }) {
return await this.cartService.getNeedsInput(args.cartId);
}
@SanitizeExceptions()
@NextIOValidator(SubmitNeedsInputActionArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async submitNeedsInput(args: { cartId: string }) {
await this.cartService.submitNeedsInput(args.cartId);
}
@SanitizeExceptions()
@NextIOValidator(undefined, GetMetricsFlowActionResult)
@CaptureTimingWithStatsD()
async getMetricsFlow() {
return this.contentServerManager.getMetricsFlow();
}
@SanitizeExceptions()
@NextIOValidator(ValidatePostalCodeActionArgs, ValidatePostalCodeActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async validateAndFormatPostalCode(args: {
postalCode: string;
countryCode: string;
}) {
return await this.googleManager.validateAndFormatPostalCode(
args.postalCode,
args.countryCode
);
}
@SanitizeExceptions()
@NextIOValidator(
GetSubscriptionPageContentActionArgs,
GetCancelFlowContentActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getCancelFlowContent(args: {
uid: string;
subscriptionId: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
const result =
await this.subscriptionManagementService.getCancelFlowContent(
args.uid,
args.subscriptionId,
args.acceptLanguage || undefined,
args.selectedLanguage
);
return result;
}
@SanitizeExceptions()
@NextIOValidator(
GetSubscriptionPageContentActionArgs,
GetStaySubscribedFlowContentActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getStaySubscribedFlowContent(args: {
uid: string;
subscriptionId: string;
acceptLanguage?: string | null;
selectedLanguage?: string;
}) {
const result =
await this.subscriptionManagementService.getStaySubscribedFlowContent(
args.uid,
args.subscriptionId,
args.acceptLanguage || undefined,
args.selectedLanguage
);
return result;
}
@SanitizeExceptions()
@NextIOValidator(DetermineCurrencyActionArgs, DetermineCurrencyActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async determineCurrency(args: { ip: string }) {
const location = this.geodbManager.getTaxAddress(args.ip);
if (!location?.countryCode) {
return {};
}
const currency = this.currencyManager.getCurrencyForCountry(
location.countryCode
);
return {
currency,
};
}
@SanitizeExceptions()
@NextIOValidator(
DetermineCurrencyForCustomerActionArgs,
DetermineCurrencyForCustomerActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async determineCurrencyForCustomer(args: { uid: string }) {
const currency =
await this.subscriptionManagementService.getCurrencyForCustomer(args.uid);
return {
currency,
};
}
@SanitizeExceptions()
@NextIOValidator(
ResubscribeSubscriptionActionArgs,
ResubscribeSubscriptionActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async resubscribeSubscription(args: {
uid: string;
subscriptionId: string;
}) {
try {
await this.subscriptionManagementService.resubscribeSubscription(
args.uid,
args.subscriptionId
);
return {
ok: true,
};
} catch (error) {
return {
ok: false,
};
}
}
@SanitizeExceptions()
@NextIOValidator(UpdateTaxAddressActionArgs, UpdateTaxAddressActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async updateTaxAddress(args: {
cartId: string;
version: number;
offeringId: string;
taxAddress: TaxAddress;
uid?: string;
}): Promise<
| {
ok: true;
taxAddress: TaxAddress;
}
| {
ok: false;
error: string;
}
> {
const { cartId, version, offeringId, taxAddress, uid } = args;
// Validate Tax Address before updating
const { isValid, status } = await this.validateLocation({
offeringId,
taxAddress,
uid,
});
if (!isValid) {
return {
ok: false,
error: status,
};
}
try {
const { taxAddress: cartTaxAddress } = await this.cartService.updateCart(
cartId,
version,
{
taxAddress,
}
);
if (!cartTaxAddress) {
return {
ok: false,
error: 'cart_tax_address_not_updated',
};
}
} catch (error) {
if (
error instanceof CartVersionMismatchError ||
error instanceof CartInvalidStateForActionError
) {
return {
ok: false,
error: 'cart_tax_address_not_updated',
};
} else {
throw error;
}
}
return {
ok: true,
taxAddress,
};
}
@SanitizeExceptions()
@NextIOValidator(ValidateLocationActionArgs, ValidateLocationActionResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async validateLocation(args: {
offeringId: string;
taxAddress?: TaxAddress;
uid?: string;
}) {
const { status: locationStatus } =
await this.eligibilityService.getProductAvailabilityForLocation(
args.offeringId,
args.taxAddress?.countryCode
);
if (locationStatus === LocationStatus.Valid) {
if (args.uid && args.taxAddress) {
const { status: taxChangeStatus, currentCurrency } =
await this.taxService.getTaxChangeStatus(args.uid, args.taxAddress);
return {
isValid: taxChangeStatus === TaxChangeAllowedStatus.Allowed,
status: taxChangeStatus,
currentCurrency,
};
} else {
return {
isValid: true,
status: locationStatus,
};
}
} else {
return {
isValid: false,
status: locationStatus,
};
}
}
@SanitizeExceptions()
@NextIOValidator(ServerLogActionArgs, undefined)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async serverLog(args: { message: string; data?: any }) {
this.log.log(args.message, args.data);
}
@SanitizeExceptions()
@NextIOValidator(GetUserinfoArgs, GetUserinfoResult)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getUserinfo(args: { accessToken: string; userinfoUrl: string }) {
return await this.profileClient.getUserinfo(
args.userinfoUrl,
args.accessToken
);
}
@SanitizeExceptions({ allowlist: [AccountCustomerNotFoundError] })
@NextIOValidator(
GetStripePaymentManagementDetailsArgs,
GetStripePaymentManagementDetailsResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getStripePaymentManagementDetails(args: { uid: string }) {
return await this.subscriptionManagementService.getStripePaymentManagementDetails(
args.uid
);
}
@SanitizeExceptions()
@NextIOValidator(
UpdateStripePaymentDetailsArgs,
UpdateStripePaymentDetailsResult
)
@CaptureTimingWithStatsD()
async updateStripePaymentDetails(args: {
uid: string;
confirmationTokenId: string;
}) {
return await this.subscriptionManagementService.updateStripePaymentDetails(
args.uid,
args.confirmationTokenId
);
}
@SanitizeExceptions()
@NextIOValidator(SetDefaultStripePaymentDetailsActionArgs, undefined)
@CaptureTimingWithStatsD()
async setDefaultStripePaymentDetails(args: {
uid: string;
paymentMethodId: string;
fullName: string;
}) {
return await this.subscriptionManagementService.setDefaultStripePaymentDetails(
args.uid,
args.paymentMethodId,
args.fullName
);
}
@SanitizeExceptions()
@NextIOValidator(
GetCMSChurnInterventionActionArgs,
GetCMSChurnInterventionActionResult
)
@WithTypeCachableAsyncLocalStorage()
@CaptureTimingWithStatsD()
async getCMSChurnIntervention(args: {
interval: SubplatInterval;
churnType: 'cancel' | 'stay_subscribed';
stripeProductId?: string;
offeringApiIdentifier?: string;
acceptLanguage?: string;
selectedLanguage?: string;
}) {
return await this.churnInterventionService.getChurnInterventionForProduct(
args.interval,
args.churnType,
args.stripeProductId,
args.offeringApiIdentifier,
args.acceptLanguage,
args.selectedLanguage
);
}
}