async extractInvoiceDetailsForEmail()

in packages/fxa-auth-server/lib/payments/stripe.ts [2603:2790]


  async extractInvoiceDetailsForEmail(invoice: Stripe.Invoice) {
    const customer = await this.expandResource(
      invoice.customer,
      CUSTOMER_RESOURCE
    );
    if (!customer || customer.deleted) {
      throw error.unknownCustomer(invoice.customer);
    }

    // Get the new subscription, ignoring any invoiceitem line items
    // that could contain prorations for old subscriptions
    const subscriptionLineItem = invoice.lines.data.find(
      (line) => line.type === 'subscription'
    );

    // In certain instances the invoice won't have a 'subscription' line item.
    // In those cases, select the 'invoiceitem' without proration_details.credited_items
    const invoiceitemLineItem = !subscriptionLineItem
      ? invoice.lines.data.find(
          (line) =>
            line.type === 'invoiceitem' &&
            !line.proration_details?.credited_items
        )
      : undefined;

    const lineItem = subscriptionLineItem || invoiceitemLineItem;

    if (!lineItem) {
      // No subscription or invoiceitem is present for the invoice. This should never happen
      // since all invoices have a related incoming subscription as one of the line items.
      throw error.internalValidationError(
        'extractInvoiceDetailsForEmail',
        invoice,
        new Error(
          `No subscription or invoiceitem line items found for invoice: ${invoice.id}`
        )
      );
    }

    // Dig up & expand objects in the invoice that usually come as just IDs
    const { plan } = lineItem;
    if (!plan) {
      // No plan is present if this is not a subscription or proration, which
      // should never happen as we only have subscriptions.
      throw error.internalValidationError(
        'extractInvoiceDetailsForEmail',
        invoice.lines.data[0],
        new Error(`Unexpected line item: ${invoice.lines.data[0].id}`)
      );
    }
    const [abbrevProduct, charge] = await Promise.all([
      this.expandAbbrevProductForPlan(plan),
      this.expandResource(invoice.charge, CHARGES_RESOURCE),
    ]);

    // if the invoice does not have the deprecated discount property but has a discount ID in discounts
    // expand the discount
    let discountType: Stripe.Coupon.Duration | null = null;
    let discountDuration: number | null = null;

    if (invoice.discount) {
      discountType = invoice.discount.coupon.duration;
      discountDuration = invoice.discount.coupon.duration_in_months;
    }

    if (
      invoice.id &&
      !invoice.discount &&
      !!invoice.discounts?.length &&
      invoice.discounts.length === 1
    ) {
      const invoiceWithDiscount = await this.getInvoiceWithDiscount(invoice.id);
      const discount = invoiceWithDiscount.discounts?.pop() as Stripe.Discount;
      discountType = discount.coupon.duration;
      discountDuration = discount.coupon.duration_in_months;
    }

    if (!!invoice.discounts?.length && invoice.discounts.length > 1) {
      throw error.internalValidationError(
        'extractInvoiceDetailsForEmail',
        invoice,
        new Error(`Invoice has multiple discounts.`)
      );
    }

    if (!abbrevProduct) {
      throw error.internalValidationError(
        'extractInvoiceDetailsForEmail',
        invoice,
        new Error(`No product attached to plan ${plan.id}`)
      );
    }

    if (!customer.email) {
      throw error.internalValidationError(
        'extractInvoiceDetailsForEmail',
        { customerId: customer.id },
        'Customer missing email.'
      );
    }

    const {
      email,
      metadata: { userid: uid },
    } = customer;
    const { product_id: productId, product_name: productName } = abbrevProduct;
    const {
      number: invoiceNumber,
      created: invoiceDate,
      currency: invoiceTotalCurrency,
      total: invoiceTotalInCents,
      subtotal: invoiceSubtotalInCents,
      hosted_invoice_url: invoiceLink,
      tax: invoiceTaxAmountInCents,
      status: invoiceStatus,
    } = invoice;

    const nextInvoiceDate = lineItem.period.end;

    const invoiceDiscountAmountInCents =
      (invoice.total_discount_amounts &&
        invoice.total_discount_amounts.length &&
        invoice.total_discount_amounts[0].amount) ||
      null;

    // Only show the Subtotal when there is a Discount
    const showSubtotal =
      invoiceDiscountAmountInCents || discountType || discountDuration;

    const { id: planId, nickname: planName } = plan;
    const abbrevPlan = await this.findAbbrevPlanById(planId);
    const productMetadata = this.mergeMetadata(
      {
        ...plan,
        metadata: abbrevPlan.plan_metadata,
      },
      abbrevProduct
    );

    // Use Firestore product configs if that exist
    const planConfig: Partial<PlanConfig> =
      await this.maybeGetPlanConfig(planId);

    const { emailIconURL: planEmailIconURL = '', successActionButtonURL } = {
      emailIconURL: planConfig.urls?.emailIcon || productMetadata.emailIconURL,
      successActionButtonURL:
        planConfig.urls?.successActionButton ||
        productMetadata.successActionButtonURL,
    };

    const planSuccessActionButtonURL = successActionButtonURL || '';

    const { lastFour, cardType } = this.extractCardDetails({
      charge,
    });

    const payment_provider = this.getPaymentProvider(customer);

    return {
      uid,
      email,
      cardType,
      lastFour,
      payment_provider,
      invoiceLink,
      invoiceNumber,
      invoiceStatus,
      invoiceTotalInCents,
      invoiceTotalCurrency,
      invoiceSubtotalInCents: showSubtotal ? invoiceSubtotalInCents : null,
      invoiceDiscountAmountInCents,
      invoiceTaxAmountInCents,
      invoiceDate: new Date(invoiceDate * 1000),
      nextInvoiceDate: new Date(nextInvoiceDate * 1000),
      productId,
      productName,
      planId,
      planName,
      planEmailIconURL,
      planSuccessActionButtonURL,
      planConfig,
      productMetadata,
      showPaymentMethod: !!invoiceTotalInCents,
      showTaxAmount: false, // Currently we do not want to show tax amounts in emails
      discountType,
      discountDuration,
    };
  }