public async processInvoice()

in packages/fxa-auth-server/lib/payments/paypal/helper.ts [457:573]


  public async processInvoice(opts: {
    customer: Stripe.Customer;
    invoice: Stripe.Invoice;
    batchProcessing?: boolean;
    ipaddress?: string;
  }) {
    const { customer, invoice, batchProcessing = false, ipaddress } = opts;
    const agreementId = this.stripeHelper.getCustomerPaypalAgreement(customer);
    if (!agreementId) {
      throw error.internalValidationError('processInvoice', {
        message: 'Agreement ID not found.',
      });
    }
    if (!['draft', 'open'].includes(invoice.status ?? '')) {
      throw error.internalValidationError('processInvoice', {
        message: 'Invoice in invalid state.',
      });
    }

    const paymentAttempt = this.stripeHelper.getPaymentAttempts(invoice);

    // PayPal supports an idempotencyKey on transaction charges to avoid repeat
    // charges. This key is restricted to the invoice and payment
    // attempt in combination, so that retries can be made if
    // the prior attempt failed and a retry is desired.
    const idempotencyKey = this.generateIdempotencyKey(
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      invoice.id!,
      paymentAttempt
    );

    let countryCode: string | undefined =
      invoice.customer_shipping?.address?.country ?? undefined;

    if (!countryCode) {
      const validCountries = this.currencyHelper.currencyToCountryMap.get(
        invoice.currency.toUpperCase()
      );

      if (validCountries && validCountries.length > 0) {
        countryCode = validCountries[0];
      } else {
        this.log.error('processInvoice.countryCode', {
          message: 'No valid country code found for invoice',
          invoiceId: invoice.id,
          currency: invoice.currency,
        });
      }
    }

    const promises: Promise<any>[] = [
      this.chargeCustomer({
        amountInCents: invoice.amount_due,
        billingAgreementId: agreementId,
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        invoiceNumber: invoice.id!,
        currencyCode: invoice.currency,
        idempotencyKey,
        ...(countryCode && { countryCode }),
        ...(ipaddress && { ipaddress }),
        ...(invoice.tax && { taxAmountInCents: invoice.tax }),
      }),
    ];
    if (invoice.status === 'draft') {
      promises.push(this.stripeHelper.finalizeInvoice(invoice));
    }

    let transactionResponse;
    try {
      [transactionResponse] = (await Promise.all(promises)) as [
        ChargeResponse,
        any
      ];
    } catch (err) {
      if (PayPalClientError.hasPayPalNVPError(err) && !batchProcessing) {
        throwPaypalCodeError(err);
      }
      this.log.error('processInvoice', {
        err,
        nvpData: err.data,
        invoiceId: invoice.id,
      });

      throw err;
    }
    await this.stripeHelper.updatePaymentAttempts(invoice);

    switch (transactionResponse.paymentStatus) {
      case 'Completed':
      case 'Processed':
        return Promise.all([
          this.stripeHelper.updateInvoiceWithPaypalTransactionId(
            invoice,
            transactionResponse.transactionId
          ),
          this.stripeHelper.payInvoiceOutOfBand(invoice),
        ]);
      case 'Pending':
      case 'In-Progress':
        return;
      case 'Denied':
      case 'Failed':
      case 'Voided':
      case 'Expired':
        throw error.paymentFailed();
      default:
        // Unexpected response here, log details and throw validation error.
        this.log.error('processInvoice', {
          message: 'Unexpected PayPal transaction response.',
          transactionResponse,
        });
        throw error.internalValidationError('processInvoice', {
          message: 'Unexpected PayPal transaction response.',
          transactionResponse: transactionResponse.paymentStatus,
        });
    }
  }