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,
});
}
}