Skip to content

Commit 1463f62

Browse files
Merge pull request #18313 from mozilla/FXA-10948
feat(paypal): Send country code with paypal transactions
2 parents 188431b + a3e6192 commit 1463f62

16 files changed

Lines changed: 116 additions & 6 deletions

File tree

libs/payments/cart/src/lib/checkout.service.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,9 @@ describe('CheckoutService', () => {
688688
);
689689
const mockPaypalCustomer = ResultPaypalCustomerFactory();
690690
const mockInvoice = StripeResponseFactory(
691-
StripeInvoiceFactory({ status: 'paid' })
691+
StripeInvoiceFactory({
692+
status: 'paid',
693+
})
692694
);
693695
const mockPrice = StripePriceFactory();
694696
const mockPrePayStepsResult = PrePayStepsResultFactory({
@@ -852,7 +854,9 @@ describe('CheckoutService', () => {
852854
);
853855
const mockPaypalCustomer = ResultPaypalCustomerFactory();
854856
const mockInvoice = StripeResponseFactory(
855-
StripeInvoiceFactory({ status: 'uncollectible' })
857+
StripeInvoiceFactory({
858+
status: 'uncollectible',
859+
})
856860
);
857861
const mockPrice = StripePriceFactory();
858862
const mockPrePayStepsResult = PrePayStepsResultFactory({

libs/payments/currency/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
export * from './lib/currency.constants';
66
export * from './lib/currency.error';
77
export * from './lib/currency.manager';
8+
export * from './lib/currency.config';

libs/payments/currency/src/lib/currency.manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,14 @@ export class CurrencyManager {
7272

7373
return undefined;
7474
}
75+
76+
getDefaultCountryForCurrency(currency: string) {
77+
if (
78+
currency in Object.getOwnPropertyNames(this.config.currenciesToCountries)
79+
) {
80+
return this.config.currenciesToCountries[currency][0];
81+
} else {
82+
return undefined;
83+
}
84+
}
7585
}

libs/payments/customer/src/lib/invoice.manager.spec.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
StripePromotionCodeFactory,
1616
StripeResponseFactory,
1717
StripeUpcomingInvoiceFactory,
18+
StripeAddressFactory,
1819
} from '@fxa/payments/stripe';
1920
import { TaxAddressFactory } from './factories/tax-address.factory';
2021
import { InvoicePreviewFactory } from './invoice.factories';
@@ -26,6 +27,10 @@ import {
2627
PayPalClient,
2728
PaypalClientConfig,
2829
} from '@fxa/payments/paypal';
30+
import {
31+
CurrencyManager,
32+
MockCurrencyConfigProvider,
33+
} from '@fxa/payments/currency';
2934
import { STRIPE_CUSTOMER_METADATA, STRIPE_INVOICE_METADATA } from './types';
3035

3136
jest.mock('../lib/util/stripeInvoiceToFirstInvoicePreviewDTO');
@@ -49,6 +54,8 @@ describe('InvoiceManager', () => {
4954
StripeClient,
5055
PayPalClient,
5156
PaypalClientConfig,
57+
CurrencyManager,
58+
MockCurrencyConfigProvider,
5259
MockStripeConfigProvider,
5360
InvoiceManager,
5461
],
@@ -99,6 +106,7 @@ describe('InvoiceManager', () => {
99106

100107
const result = await invoiceManager.previewUpcoming({
101108
priceId: mockPrice.id,
109+
currency: mockPrice.currency,
102110
customer: mockCustomer,
103111
taxAddress: mockTaxAddress,
104112
});
@@ -135,6 +143,7 @@ describe('InvoiceManager', () => {
135143

136144
const result = await invoiceManager.previewUpcoming({
137145
priceId: mockPrice.id,
146+
currency: mockPrice.currency,
138147
customer: mockCustomer,
139148
taxAddress: mockTaxAddress,
140149
couponCode: mockPromotionCode.code,
@@ -188,6 +197,7 @@ describe('InvoiceManager', () => {
188197
const mockInvoice = StripeInvoiceFactory({
189198
amount_due: 50,
190199
currency: 'usd',
200+
customer_shipping: { address: StripeAddressFactory() },
191201
});
192202

193203
mockedGetMinimumChargeAmountForCurrency.mockReturnValue(10);
@@ -249,6 +259,7 @@ describe('InvoiceManager', () => {
249259
mockPaymentAttemptCount
250260
),
251261
},
262+
customer_shipping: { address: StripeAddressFactory() },
252263
})
253264
);
254265
const mockPayPalCharge = ChargeResponseFactory({
@@ -280,6 +291,7 @@ describe('InvoiceManager', () => {
280291
mockCustomer.metadata[STRIPE_CUSTOMER_METADATA.PaypalAgreement],
281292
invoiceNumber: mockInvoice.id,
282293
currencyCode: mockInvoice.currency,
294+
countryCode: mockInvoice.customer_shipping?.address?.country,
283295
idempotencyKey: `${mockInvoice.id}-${mockPaymentAttemptCount}`,
284296
taxAmountInCents: mockInvoice.tax,
285297
});
@@ -312,7 +324,11 @@ describe('InvoiceManager', () => {
312324
const mockCustomer = StripeResponseFactory(
313325
StripeCustomerFactory({ metadata: {} })
314326
);
315-
const mockInvoice = StripeResponseFactory(StripeInvoiceFactory());
327+
const mockInvoice = StripeResponseFactory(
328+
StripeInvoiceFactory({
329+
customer_shipping: { address: StripeAddressFactory() },
330+
})
331+
);
316332

317333
await expect(
318334
invoiceManager.processPayPalNonZeroInvoice(mockCustomer, mockInvoice)
@@ -321,7 +337,10 @@ describe('InvoiceManager', () => {
321337
it('throws an error for an already-paid invoice', async () => {
322338
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
323339
const mockInvoice = StripeResponseFactory(
324-
StripeInvoiceFactory({ status: 'paid' })
340+
StripeInvoiceFactory({
341+
status: 'paid',
342+
customer_shipping: { address: StripeAddressFactory() },
343+
})
325344
);
326345

327346
await expect(
@@ -331,7 +350,10 @@ describe('InvoiceManager', () => {
331350
it('throws an error for an uncollectible invoice', async () => {
332351
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
333352
const mockInvoice = StripeResponseFactory(
334-
StripeInvoiceFactory({ status: 'uncollectible' })
353+
StripeInvoiceFactory({
354+
status: 'uncollectible',
355+
customer_shipping: { address: StripeAddressFactory() },
356+
})
335357
);
336358

337359
await expect(
@@ -356,6 +378,7 @@ describe('InvoiceManager', () => {
356378
mockPaymentAttemptCount
357379
),
358380
},
381+
customer_shipping: { address: StripeAddressFactory() },
359382
})
360383
);
361384
const mockPayPalCharge = ChargeResponseFactory({
@@ -384,6 +407,7 @@ describe('InvoiceManager', () => {
384407
mockCustomer.metadata[STRIPE_CUSTOMER_METADATA.PaypalAgreement],
385408
invoiceNumber: mockInvoice.id,
386409
currencyCode: mockInvoice.currency,
410+
countryCode: mockInvoice.customer_shipping?.address?.country,
387411
idempotencyKey: `${mockInvoice.id}-${mockPaymentAttemptCount}`,
388412
taxAmountInCents: mockInvoice.tax,
389413
});
@@ -421,6 +445,7 @@ describe('InvoiceManager', () => {
421445
mockPaymentAttemptCount
422446
),
423447
},
448+
customer_shipping: { address: StripeAddressFactory() },
424449
})
425450
);
426451
const mockPayPalCharge = ChargeResponseFactory({
@@ -446,6 +471,7 @@ describe('InvoiceManager', () => {
446471
mockCustomer.metadata[STRIPE_CUSTOMER_METADATA.PaypalAgreement],
447472
invoiceNumber: mockInvoice.id,
448473
currencyCode: mockInvoice.currency,
474+
countryCode: mockInvoice.customer_shipping?.address?.country,
449475
idempotencyKey: `${mockInvoice.id}-${mockPaymentAttemptCount}`,
450476
taxAmountInCents: mockInvoice.tax,
451477
});
@@ -484,6 +510,7 @@ describe('InvoiceManager', () => {
484510
),
485511
},
486512
tax: 0,
513+
customer_shipping: { address: StripeAddressFactory() },
487514
})
488515
);
489516
const mockPayPalCharge = ChargeResponseFactory({
@@ -515,6 +542,7 @@ describe('InvoiceManager', () => {
515542
mockCustomer.metadata[STRIPE_CUSTOMER_METADATA.PaypalAgreement],
516543
invoiceNumber: mockInvoice.id,
517544
currencyCode: mockInvoice.currency,
545+
countryCode: mockInvoice.customer_shipping?.address?.country,
518546
idempotencyKey: `${mockInvoice.id}-${mockPaymentAttemptCount}`,
519547
taxAmountInCents: mockInvoice.tax,
520548
});

libs/payments/customer/src/lib/invoice.manager.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
PayPalClient,
1818
PayPalClientError,
1919
} from '@fxa/payments/paypal';
20+
import { CurrencyManager } from '@fxa/payments/currency';
2021
import {
2122
InvoicePreview,
2223
STRIPE_CUSTOMER_METADATA,
@@ -36,7 +37,8 @@ import {
3637
export class InvoiceManager {
3738
constructor(
3839
private stripeClient: StripeClient,
39-
private paypalClient: PayPalClient
40+
private paypalClient: PayPalClient,
41+
private currencyManager: CurrencyManager
4042
) {}
4143

4244
async finalizeWithoutAutoAdvance(invoiceId: string) {
@@ -142,6 +144,17 @@ export class InvoiceManager {
142144
);
143145
}
144146

147+
const countryCode =
148+
invoice.customer_shipping?.address?.country ??
149+
this.currencyManager.getDefaultCountryForCurrency(
150+
invoice.currency.toUpperCase()
151+
);
152+
if (!countryCode) {
153+
throw new Error(
154+
'No valid country code could be found for invoice or currency'
155+
);
156+
}
157+
145158
// PayPal allows for idempotent retries on payment attempts to prevent double charging.
146159
const paymentAttemptCount = parseInt(
147160
invoice?.metadata?.[STRIPE_INVOICE_METADATA.RetryAttempts] ?? '0'
@@ -155,6 +168,7 @@ export class InvoiceManager {
155168
customer.metadata[STRIPE_CUSTOMER_METADATA.PaypalAgreement],
156169
invoiceNumber: invoice.id,
157170
currencyCode: invoice.currency,
171+
countryCode,
158172
idempotencyKey,
159173
...(ipaddress && { ipaddress }),
160174
...(invoice.tax !== null && { taxAmountInCents: invoice.tax }),

libs/payments/paypal/src/lib/factories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export const ChargeOptionsFactory = (
180180
amountInCents: faker.number.int({ max: 100000000 }),
181181
billingAgreementId: faker.string.uuid(),
182182
currencyCode: faker.finance.currencyCode(),
183+
countryCode: faker.finance.currencyCode(),
183184
idempotencyKey: faker.string.uuid(),
184185
invoiceNumber: faker.string.uuid(),
185186
taxAmountInCents: faker.number.int({ max: 100000000 }),

libs/payments/paypal/src/lib/paypal.client.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ describe('PayPalClient', () => {
148148
const options = {
149149
amount: faker.finance.amount(),
150150
currencyCode: faker.finance.currencyCode(),
151+
countryCode: faker.location.countryCode(),
151152
idempotencyKey: faker.string.sample(),
152153
ipaddress: faker.internet.ipv4(),
153154
billingAgreementId: faker.string.sample(),
@@ -161,6 +162,7 @@ describe('PayPalClient', () => {
161162
PAYMENTTYPE: 'instant',
162163
AMT: options.amount,
163164
CURRENCYCODE: options.currencyCode,
165+
COUNTRYCODE: options.countryCode,
164166
CUSTOM: options.idempotencyKey,
165167
INVNUM: options.invoiceNumber,
166168
IPADDRESS: options.ipaddress,
@@ -184,6 +186,7 @@ describe('PayPalClient', () => {
184186
const options = {
185187
amount: faker.finance.amount(),
186188
currencyCode: faker.finance.currencyCode(),
189+
countryCode: faker.location.countryCode(),
187190
idempotencyKey: faker.string.sample(),
188191
ipaddress: faker.internet.ipv4(),
189192
billingAgreementId: faker.string.sample(),
@@ -198,6 +201,7 @@ describe('PayPalClient', () => {
198201
PAYMENTTYPE: 'instant',
199202
AMT: options.amount,
200203
CURRENCYCODE: options.currencyCode,
204+
COUNTRYCODE: options.countryCode,
201205
CUSTOM: options.idempotencyKey,
202206
INVNUM: options.invoiceNumber,
203207
IPADDRESS: options.ipaddress,

libs/payments/paypal/src/lib/paypal.client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export class PayPalClient {
231231
const data = {
232232
AMT: options.amount,
233233
CURRENCYCODE: options.currencyCode.toUpperCase(),
234+
COUNTRYCODE: options.countryCode.toUpperCase(),
234235
CUSTOM: options.idempotencyKey,
235236
INVNUM: options.invoiceNumber,
236237
...(options.ipaddress && { IPADDRESS: options.ipaddress }),
@@ -342,6 +343,7 @@ export class PayPalClient {
342343
),
343344
billingAgreementId: options.billingAgreementId,
344345
currencyCode: options.currencyCode,
346+
countryCode: options.countryCode,
345347
idempotencyKey: options.idempotencyKey,
346348
invoiceNumber: options.invoiceNumber,
347349
...(options.ipaddress && { ipaddress: options.ipaddress }),

libs/payments/paypal/src/lib/paypal.client.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface DoReferenceTransactionOptions {
145145
invoiceNumber: string;
146146
idempotencyKey: string;
147147
currencyCode: string;
148+
countryCode: string;
148149
taxAmount?: string;
149150
ipaddress?: string;
150151
}

libs/payments/paypal/src/lib/paypal.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ChargeOptions {
2323
amountInCents: number;
2424
billingAgreementId: string;
2525
currencyCode: string;
26+
countryCode: string;
2627
idempotencyKey: string;
2728
invoiceNumber: string;
2829
ipaddress?: string;

0 commit comments

Comments
 (0)