in packages/fxa-auth-server/lib/routes/subscriptions/stripe.ts [651:806]
async createSubscriptionWithPMI(request: AuthRequest): Promise<{
sourceCountry: string | null;
subscription: DeepPartial<Stripe.Subscription>;
}> {
this.log.begin('subscriptions.createSubscriptionWithPMI', request);
const { uid, email, account } = await handleAuth(
this.db,
request.auth,
true
);
await this.customs.check(request, email, 'createSubscriptionWithPMI');
const taxAddress = buildTaxAddress(
this.log,
request.app.clientAddress,
request.app.geo.location
);
const countryCode = taxAddress?.countryCode;
if (countryCode && this.unsupportedLocations.includes(countryCode)) {
throw error.unsupportedLocation(countryCode);
}
try {
const customer = await this.stripeHelper.fetchCustomer(uid, ['tax']);
if (!customer) {
throw error.unknownCustomer(uid);
}
const {
priceId,
paymentMethodId,
promotionCode: promotionCodeFromRequest,
metricsContext,
} = request.payload as Record<string, string>;
// Make sure to clean up any subscriptions that may be hanging with no payment
const existingSubscription =
this.stripeHelper.findCustomerSubscriptionByPlanId(customer, priceId);
if (existingSubscription?.status === 'incomplete') {
await this.stripeHelper.cancelSubscription(existingSubscription.id);
}
// Validate that the user doesn't have conflicting subscriptions, for instance via IAP
const { subscriptionEligibilityResult } =
await this.capabilityService.getPlanEligibility(
customer.metadata.userid,
priceId
);
if (
subscriptionEligibilityResult !== SubscriptionEligibilityResult.CREATE
) {
throw error.userAlreadySubscribedToProduct();
}
const promotionCode: Stripe.PromotionCode | undefined =
await this.extractPromotionCode(promotionCodeFromRequest, priceId);
let paymentMethod: Stripe.PaymentMethod | undefined;
const planCurrency = (await this.stripeHelper.findAbbrevPlanById(priceId))
.currency;
const automaticTax =
this.stripeHelper.isCustomerTaxableWithSubscriptionCurrency(
customer,
planCurrency
);
// Skip the payment source check if there's no payment method id.
if (paymentMethodId) {
paymentMethod =
await this.stripeHelper.getPaymentMethod(paymentMethodId);
const paymentMethodCountry = paymentMethod.card?.country;
if (
!this.stripeHelper.currencyHelper.isCurrencyCompatibleWithCountry(
planCurrency,
paymentMethodCountry
)
) {
throw error.currencyCountryMismatch(
planCurrency,
paymentMethodCountry
);
}
if (!this.stripeHelper.customerTaxId(customer)) {
await this.stripeHelper.addTaxIdToCustomer(customer, planCurrency);
}
}
const subscription: any =
await this.stripeHelper.createSubscriptionWithPMI({
customerId: customer.id,
priceId,
paymentMethodId,
promotionCode: promotionCode,
automaticTax,
});
if (!automaticTax) {
this.log.warn(
'subscriptions.createSubscriptionWithPMI.automatic_tax_failed',
{
uid,
automatic_tax: customer.tax?.automatic_tax,
}
);
}
const sourceCountry =
this.stripeHelper.extractSourceCountryFromSubscription(subscription);
await this.customerChanged(request, uid, email);
this.log.info('subscriptions.createSubscriptionWithPMI.success', {
uid,
subscriptionId: subscription.id,
});
await sendFinishSetupEmailForStubAccount({
uid,
account,
subscription,
stripeHelper: this.stripeHelper,
mailer: this.mailer,
subscriptionAccountReminders: this.subscriptionAccountReminders,
metricsContext,
});
return {
sourceCountry,
subscription: filterSubscription(subscription),
};
} catch (err) {
try {
if (account.verifierSetAt <= 0) {
await deleteAccountIfUnverified(
this.db,
this.stripeHelper,
this.log,
request,
email
);
}
} catch (deleteAccountError) {
if (
deleteAccountError.errno !== error.ERRNO.ACCOUNT_EXISTS &&
deleteAccountError.errno !==
error.ERRNO.VERIFIED_SECONDARY_EMAIL_EXISTS
) {
throw deleteAccountError;
}
}
throw err;
}
}