/ tests / payments / paypal-unit.test.js
paypal-unit.test.js
  1  /**
  2   * PayPal Payment Integration — Additional Unit Tests
  3   *
  4   * Covers gaps in paypal.js not addressed by existing paypal.test.js:
  5   * - testPrice override in createPaymentOrder (lines 83-87)
  6   * - verifyPayment with null payer / no name object
  7   * - capturePayment error formatting edge cases (no details, no name)
  8   * - refundPayment with no reason (empty body path)
  9   * - refundPayment amount parsing with missing amount field
 10   * - generatePaymentMessage SMS branding and content assertions
 11   *
 12   * Uses node:test + node:assert/strict with mocked axios.
 13   */
 14  
 15  import { describe, test, mock, beforeEach } from 'node:test';
 16  import assert from 'node:assert/strict';
 17  
 18  // ---- Mock axios ----
 19  const axiosPostMock = mock.fn();
 20  const axiosGetMock = mock.fn();
 21  
 22  mock.module('axios', {
 23    defaultExport: {
 24      post: axiosPostMock,
 25      get: axiosGetMock,
 26    },
 27  });
 28  
 29  // Mock dotenv
 30  mock.module('dotenv', {
 31    defaultExport: { config: () => {} },
 32    namedExports: { config: () => {} },
 33  });
 34  
 35  // Mock country-pricing with currencySymbol for testPrice path
 36  mock.module('../../src/utils/country-pricing.js', {
 37    namedExports: {
 38      getPrice: countryCode => {
 39        const pricing = {
 40          US: {
 41            currency: 'USD',
 42            currencySymbol: '$',
 43            priceLocal: 297,
 44            priceUsd: 297,
 45            formattedPrice: '$297.00 USD',
 46            exchangeRate: 1,
 47          },
 48          AU: {
 49            currency: 'AUD',
 50            currencySymbol: 'A$',
 51            priceLocal: 447,
 52            priceUsd: 297,
 53            formattedPrice: 'A$447.00 AUD',
 54            exchangeRate: 1.505,
 55          },
 56          GB: {
 57            currency: 'GBP',
 58            currencySymbol: '\u00a3',
 59            priceLocal: 159,
 60            priceUsd: 199,
 61            formattedPrice: '\u00a3159.00 GBP',
 62            exchangeRate: 0.798,
 63          },
 64        };
 65        return pricing[countryCode] || null;
 66      },
 67    },
 68  });
 69  
 70  // Set env before importing
 71  process.env.PAYPAL_CLIENT_ID = 'test-client-id';
 72  process.env.PAYPAL_CLIENT_SECRET = 'test-client-secret';
 73  process.env.PAYPAL_MODE = 'sandbox';
 74  process.env.PAYPAL_BRAND_NAME = 'Audit&Fix';
 75  process.env.BASE_URL = 'https://test.example.com';
 76  process.env.LOGS_DIR = '/tmp/test-logs';
 77  process.env.NODE_ENV = 'test';
 78  
 79  const { createPaymentOrder, capturePayment, verifyPayment, refundPayment, generatePaymentMessage } =
 80    await import('../../src/payment/paypal.js');
 81  
 82  // ---- Helpers ----
 83  function mockAccessToken() {
 84    return { data: { access_token: 'mock-token-unit', expires_in: 3600 } };
 85  }
 86  
 87  function mockOrderResponse(id, overrides = {}) {
 88    return {
 89      data: {
 90        id,
 91        status: 'PAYER_ACTION_REQUIRED',
 92        links: [{ rel: 'payer-action', href: `https://sandbox.paypal.com/pay?token=${id}` }],
 93        ...overrides,
 94      },
 95    };
 96  }
 97  
 98  // ---- Tests ----
 99  
100  describe('paypal.js — additional unit tests', () => {
101    beforeEach(() => {
102      axiosPostMock.mock.resetCalls();
103      axiosGetMock.mock.resetCalls();
104      axiosPostMock.mock.mockImplementation(async () => {
105        throw new Error('axiosPost mock not configured');
106      });
107      axiosGetMock.mock.mockImplementation(async () => {
108        throw new Error('axiosGet mock not configured');
109      });
110    });
111  
112    // ---- testPrice override (lines 83-87) ----
113  
114    describe('createPaymentOrder() — testPrice override', () => {
115      test('overrides pricing with testPrice when provided', async () => {
116        axiosPostMock.mock.mockImplementation(async url => {
117          if (url.includes('/oauth2/token')) return mockAccessToken();
118          return mockOrderResponse('ORDER_TEST_PRICE');
119        });
120  
121        const result = await createPaymentOrder({
122          domain: 'test-price.com',
123          email: 'buyer@test.com',
124          siteId: 1,
125          conversationId: 1,
126          countryCode: 'US',
127          testPrice: 1.0,
128        });
129  
130        // Verify the order was created with testPrice amount
131        const orderCall = axiosPostMock.mock.calls.find(c =>
132          c.arguments[0].includes('/v2/checkout/orders')
133        );
134        assert.ok(orderCall, 'Should make order creation call');
135        const body = orderCall.arguments[1];
136        assert.equal(body.purchase_units[0].amount.value, '1.00');
137        assert.equal(body.purchase_units[0].amount.currency_code, 'USD');
138        assert.equal(body.purchase_units[0].items[0].unit_amount.value, '1.00');
139  
140        // Result amount should reflect the test price
141        assert.equal(result.amount, 1.0);
142      });
143  
144      test('testPrice=0 is treated as testPrice override (falsy but not null)', async () => {
145        // testPrice=0 should NOT trigger override because 0 is falsy
146        // and the code checks `testPrice !== null && testPrice !== undefined`
147        // So 0 SHOULD trigger the override path
148        axiosPostMock.mock.mockImplementation(async url => {
149          if (url.includes('/oauth2/token')) return mockAccessToken();
150          return mockOrderResponse('ORDER_ZERO_PRICE');
151        });
152  
153        const result = await createPaymentOrder({
154          domain: 'zero-price.com',
155          email: 'buyer@test.com',
156          siteId: 1,
157          conversationId: 1,
158          countryCode: 'US',
159          testPrice: 0,
160        });
161  
162        const orderCall = axiosPostMock.mock.calls.find(c =>
163          c.arguments[0].includes('/v2/checkout/orders')
164        );
165        const body = orderCall.arguments[1];
166        assert.equal(body.purchase_units[0].amount.value, '0.00');
167      });
168  
169      test('testPrice=null does not override (uses default pricing)', async () => {
170        axiosPostMock.mock.mockImplementation(async url => {
171          if (url.includes('/oauth2/token')) return mockAccessToken();
172          return mockOrderResponse('ORDER_NULL_PRICE');
173        });
174  
175        const result = await createPaymentOrder({
176          domain: 'null-price.com',
177          email: 'buyer@test.com',
178          siteId: 1,
179          conversationId: 1,
180          countryCode: 'US',
181          testPrice: null,
182        });
183  
184        const orderCall = axiosPostMock.mock.calls.find(c =>
185          c.arguments[0].includes('/v2/checkout/orders')
186        );
187        const body = orderCall.arguments[1];
188        assert.equal(body.purchase_units[0].amount.value, '297.00', 'Should use default US price');
189      });
190  
191      test('testPrice=undefined does not override (uses default pricing)', async () => {
192        axiosPostMock.mock.mockImplementation(async url => {
193          if (url.includes('/oauth2/token')) return mockAccessToken();
194          return mockOrderResponse('ORDER_UNDEF_PRICE');
195        });
196  
197        const result = await createPaymentOrder({
198          domain: 'undef-price.com',
199          email: 'buyer@test.com',
200          siteId: 1,
201          conversationId: 1,
202          countryCode: 'US',
203          testPrice: undefined,
204        });
205  
206        const orderCall = axiosPostMock.mock.calls.find(c =>
207          c.arguments[0].includes('/v2/checkout/orders')
208        );
209        const body = orderCall.arguments[1];
210        assert.equal(body.purchase_units[0].amount.value, '297.00');
211      });
212  
213      test('testPrice with non-US country uses correct currency but overridden amount', async () => {
214        axiosPostMock.mock.mockImplementation(async url => {
215          if (url.includes('/oauth2/token')) return mockAccessToken();
216          return mockOrderResponse('ORDER_AU_TEST');
217        });
218  
219        await createPaymentOrder({
220          domain: 'au-test.com.au',
221          email: 'buyer@test.com',
222          siteId: 1,
223          conversationId: 1,
224          countryCode: 'AU',
225          testPrice: 5.0,
226        });
227  
228        const orderCall = axiosPostMock.mock.calls.find(c =>
229          c.arguments[0].includes('/v2/checkout/orders')
230        );
231        const body = orderCall.arguments[1];
232        // Should use AUD currency but the overridden price
233        assert.equal(body.purchase_units[0].amount.currency_code, 'AUD');
234        assert.equal(body.purchase_units[0].amount.value, '5.00');
235      });
236    });
237  
238    // ---- verifyPayment edge cases ----
239  
240    describe('verifyPayment() — edge cases', () => {
241      test('handles payer with no name object at all', async () => {
242        axiosPostMock.mock.mockImplementation(async () => mockAccessToken());
243        axiosGetMock.mock.mockImplementation(async () => ({
244          data: {
245            id: 'ORDER_NO_NAME',
246            status: 'COMPLETED',
247            payer: { email_address: 'anon@test.com' },
248            purchase_units: [
249              { amount: { value: '297.00', currency_code: 'USD' }, reference_id: 'site_1_conv_1' },
250            ],
251            create_time: '2025-01-01T12:00:00Z',
252            update_time: '2025-01-01T12:01:00Z',
253          },
254        }));
255  
256        const result = await verifyPayment('ORDER_NO_NAME');
257  
258        assert.equal(result.isPaid, true);
259        assert.equal(result.payerEmail, 'anon@test.com');
260        assert.equal(result.payerName, null, 'Should be null when no name object');
261      });
262  
263      test('handles completely null payer object', async () => {
264        axiosPostMock.mock.mockImplementation(async () => mockAccessToken());
265        axiosGetMock.mock.mockImplementation(async () => ({
266          data: {
267            id: 'ORDER_NULL_PAYER',
268            status: 'APPROVED',
269            payer: null,
270            purchase_units: [
271              { amount: { value: '159.00', currency_code: 'GBP' }, reference_id: 'site_5_conv_10' },
272            ],
273            create_time: '2025-01-01T12:00:00Z',
274            update_time: '2025-01-01T12:00:00Z',
275          },
276        }));
277  
278        const result = await verifyPayment('ORDER_NULL_PAYER');
279  
280        assert.equal(result.isPaid, false);
281        assert.equal(result.payerEmail, undefined);
282        assert.equal(result.payerName, null);
283        assert.equal(result.amount, 159);
284        assert.equal(result.currency, 'GBP');
285        assert.equal(result.referenceId, 'site_5_conv_10');
286      });
287  
288      test('returns correct createTime and updateTime', async () => {
289        axiosPostMock.mock.mockImplementation(async () => mockAccessToken());
290        axiosGetMock.mock.mockImplementation(async () => ({
291          data: {
292            id: 'ORDER_TIMES',
293            status: 'COMPLETED',
294            payer: { email_address: 'time@test.com', name: { given_name: 'Tim', surname: 'Er' } },
295            purchase_units: [
296              { amount: { value: '297.00', currency_code: 'USD' }, reference_id: 'site_1_conv_1' },
297            ],
298            create_time: '2025-06-15T08:30:00Z',
299            update_time: '2025-06-15T08:35:00Z',
300          },
301        }));
302  
303        const result = await verifyPayment('ORDER_TIMES');
304  
305        assert.equal(result.createTime, '2025-06-15T08:30:00Z');
306        assert.equal(result.updateTime, '2025-06-15T08:35:00Z');
307      });
308  
309      test('throws with generic error message when no response data', async () => {
310        axiosPostMock.mock.mockImplementation(async () => mockAccessToken());
311        axiosGetMock.mock.mockImplementation(async () => {
312          throw new Error('ECONNRESET');
313        });
314  
315        await assert.rejects(
316          () => verifyPayment('ORDER_NETWORK_FAIL'),
317          err => {
318            assert.ok(err.message.includes('PayPal verification failed'));
319            assert.ok(err.message.includes('ECONNRESET'));
320            return true;
321          }
322        );
323      });
324    });
325  
326    // ---- capturePayment error formatting edge cases ----
327  
328    describe('capturePayment() — error formatting edge cases', () => {
329      test('formats error with name but no details array', async () => {
330        axiosPostMock.mock.mockImplementation(async url => {
331          if (url.includes('/oauth2/token')) return mockAccessToken();
332          const err = new Error('Request failed');
333          err.response = {
334            data: {
335              name: 'INTERNAL_SERVER_ERROR',
336              message: 'An internal server error has occurred',
337              // No details array
338            },
339          };
340          throw err;
341        });
342  
343        await assert.rejects(
344          () => capturePayment('ORDER_NO_DETAILS'),
345          err => {
346            assert.ok(err.message.includes('PayPal capture failed'));
347            assert.ok(err.message.includes('INTERNAL_SERVER_ERROR'));
348            assert.ok(err.message.includes('An internal server error'));
349            // Should not include "undefined" from missing details
350            assert.ok(!err.message.includes('undefined'));
351            return true;
352          }
353        );
354      });
355  
356      test('formats error with no name and no details', async () => {
357        axiosPostMock.mock.mockImplementation(async url => {
358          if (url.includes('/oauth2/token')) return mockAccessToken();
359          const err = new Error('Request failed with status 500');
360          err.response = {
361            data: {
362              message: 'Something went wrong',
363            },
364          };
365          throw err;
366        });
367  
368        await assert.rejects(
369          () => capturePayment('ORDER_BARE_ERROR'),
370          err => {
371            assert.ok(err.message.includes('PayPal capture failed'));
372            assert.ok(err.message.includes('Something went wrong'));
373            return true;
374          }
375        );
376      });
377  
378      test('formats error with no response data at all (network error)', async () => {
379        axiosPostMock.mock.mockImplementation(async url => {
380          if (url.includes('/oauth2/token')) return mockAccessToken();
381          throw new Error('ETIMEDOUT');
382        });
383  
384        await assert.rejects(
385          () => capturePayment('ORDER_TIMEOUT'),
386          err => {
387            assert.ok(err.message.includes('PayPal capture failed'));
388            assert.ok(err.message.includes('ETIMEDOUT'));
389            return true;
390          }
391        );
392      });
393  
394      test('formats error with empty details array', async () => {
395        axiosPostMock.mock.mockImplementation(async url => {
396          if (url.includes('/oauth2/token')) return mockAccessToken();
397          const err = new Error('Request failed');
398          err.response = {
399            data: {
400              name: 'UNPROCESSABLE_ENTITY',
401              message: 'The requested action could not be performed',
402              details: [], // Empty array
403            },
404          };
405          throw err;
406        });
407  
408        await assert.rejects(
409          () => capturePayment('ORDER_EMPTY_DETAILS'),
410          err => {
411            assert.ok(err.message.includes('UNPROCESSABLE_ENTITY'));
412            return true;
413          }
414        );
415      });
416    });
417  
418    // ---- refundPayment edge cases ----
419  
420    describe('refundPayment() — edge cases', () => {
421      test('sends empty body when no reason provided', async () => {
422        let capturedBody = null;
423        axiosPostMock.mock.mockImplementation(async (url, body) => {
424          if (url.includes('/oauth2/token')) return mockAccessToken();
425          capturedBody = body;
426          return {
427            data: {
428              id: 'REFUND_NO_REASON',
429              status: 'COMPLETED',
430              amount: { value: '297.00', currency_code: 'USD' },
431            },
432          };
433        });
434  
435        const result = await refundPayment('CAP_NO_REASON');
436  
437        assert.equal(result.success, true);
438        assert.deepEqual(capturedBody, {}, 'Should send empty body when no reason');
439      });
440  
441      test('sends reason as note_to_payer when provided', async () => {
442        let capturedBody = null;
443        axiosPostMock.mock.mockImplementation(async (url, body) => {
444          if (url.includes('/oauth2/token')) return mockAccessToken();
445          capturedBody = body;
446          return {
447            data: {
448              id: 'REFUND_WITH_REASON',
449              status: 'COMPLETED',
450              amount: { value: '159.00', currency_code: 'GBP' },
451            },
452          };
453        });
454  
455        await refundPayment('CAP_WITH_REASON', 'Customer requested via email');
456  
457        assert.deepEqual(capturedBody, { note_to_payer: 'Customer requested via email' });
458      });
459  
460      test('handles refund response with missing amount field', async () => {
461        axiosPostMock.mock.mockImplementation(async url => {
462          if (url.includes('/oauth2/token')) return mockAccessToken();
463          return {
464            data: {
465              id: 'REFUND_NO_AMOUNT',
466              status: 'COMPLETED',
467              // No amount field
468            },
469          };
470        });
471  
472        const result = await refundPayment('CAP_NO_AMOUNT', 'test');
473  
474        assert.equal(result.success, true);
475        assert.equal(result.refund_id, 'REFUND_NO_AMOUNT');
476        assert.equal(result.amount, 0, 'Should default to 0 when amount is missing');
477        assert.equal(result.currency, undefined, 'Currency should be undefined when no amount');
478      });
479  
480      test('uses correct sandbox API URL for refund endpoint', async () => {
481        let capturedUrl = null;
482        axiosPostMock.mock.mockImplementation(async (url) => {
483          if (url.includes('/oauth2/token')) return mockAccessToken();
484          capturedUrl = url;
485          return {
486            data: {
487              id: 'REFUND_URL_TEST',
488              status: 'COMPLETED',
489              amount: { value: '100.00', currency_code: 'USD' },
490            },
491          };
492        });
493  
494        await refundPayment('CAP_URL_TEST', 'test');
495  
496        assert.ok(capturedUrl.includes('sandbox.paypal.com'), `URL was: ${capturedUrl}`);
497        assert.ok(capturedUrl.includes('/v2/payments/captures/CAP_URL_TEST/refund'));
498      });
499  
500      test('wraps PayPal refund error with descriptive message', async () => {
501        axiosPostMock.mock.mockImplementation(async url => {
502          if (url.includes('/oauth2/token')) return mockAccessToken();
503          const err = new Error('Request failed');
504          err.response = {
505            data: { message: 'CAPTURE_FULLY_REFUNDED' },
506          };
507          throw err;
508        });
509  
510        await assert.rejects(
511          () => refundPayment('CAP_ALREADY_REFUNDED'),
512          err => {
513            assert.ok(err.message.includes('PayPal refund failed'));
514            assert.ok(err.message.includes('CAPTURE_FULLY_REFUNDED'));
515            return true;
516          }
517        );
518      });
519    });
520  
521    // ---- generatePaymentMessage additional coverage ----
522  
523    describe('generatePaymentMessage() — additional coverage', () => {
524      test('SMS message includes Audit & Fix branding', async () => {
525        const msg = await generatePaymentMessage('https://paypal.com/pay', 'sms', 'mybiz.com', 'US');
526        assert.ok(msg.includes('Marcus from Audit & Fix'));
527        assert.ok(msg.includes('mybiz.com'));
528      });
529  
530      test('email message includes Marcus Webb signature', async () => {
531        const msg = await generatePaymentMessage('https://paypal.com/pay', 'email', 'mybiz.com', 'US');
532        assert.ok(msg.includes('Marcus Webb'));
533        assert.ok(msg.includes('Audit & Fix'));
534        assert.ok(msg.includes('auditandfix.com'));
535      });
536  
537      test('email message includes payment link on its own line', async () => {
538        const link = 'https://paypal.com/pay?token=UNIQUE_TOKEN';
539        const msg = await generatePaymentMessage(link, 'email', 'client.com', 'GB');
540        // Link should appear in the message
541        assert.ok(msg.includes(link));
542        // Price should use GBP
543        assert.ok(msg.includes('\u00a3159.00 GBP'));
544      });
545  
546      test('SMS message for GB uses GBP pricing', async () => {
547        const msg = await generatePaymentMessage('https://link', 'sms', 'uk-biz.co.uk', 'GB');
548        assert.ok(msg.includes('\u00a3159.00 GBP'));
549      });
550  
551      test('defaults to US pricing when countryCode is not provided', async () => {
552        // The function uses countryCode || 'US' as fallback
553        const msg = await generatePaymentMessage('https://link', 'sms', 'biz.com', undefined);
554        assert.ok(msg.includes('$297.00 USD'));
555      });
556  
557      test('email message describes the product clearly (no CRO jargon)', async () => {
558        const msg = await generatePaymentMessage('https://link', 'email', 'test.com', 'US');
559        // Should mention "Homepage Conversion Audit" not raw "CRO"
560        assert.ok(msg.includes('Homepage Conversion Audit'));
561        // Should mention it's one-time
562        assert.ok(msg.includes('one-time'));
563      });
564    });
565  
566    // ---- createPaymentOrder URL and structure checks ----
567  
568    describe('createPaymentOrder() — order structure', () => {
569      test('sets return_url and cancel_url from BASE_URL env', async () => {
570        axiosPostMock.mock.mockImplementation(async url => {
571          if (url.includes('/oauth2/token')) return mockAccessToken();
572          return mockOrderResponse('ORDER_URLS');
573        });
574  
575        await createPaymentOrder({
576          domain: 'url-test.com',
577          email: 'buyer@test.com',
578          siteId: 1,
579          conversationId: 1,
580          countryCode: 'US',
581        });
582  
583        const orderCall = axiosPostMock.mock.calls.find(c =>
584          c.arguments[0].includes('/v2/checkout/orders')
585        );
586        const body = orderCall.arguments[1];
587        const context = body.payment_source.paypal.experience_context;
588  
589        assert.equal(context.return_url, 'https://test.example.com/payment/success');
590        assert.equal(context.cancel_url, 'https://test.example.com/payment/cancel');
591        assert.equal(context.brand_name, 'Audit&Fix');
592        assert.equal(context.user_action, 'PAY_NOW');
593      });
594  
595      test('sets item category to DIGITAL_GOODS', async () => {
596        axiosPostMock.mock.mockImplementation(async url => {
597          if (url.includes('/oauth2/token')) return mockAccessToken();
598          return mockOrderResponse('ORDER_DIGITAL');
599        });
600  
601        await createPaymentOrder({
602          domain: 'digital.com',
603          email: 'buyer@test.com',
604          siteId: 1,
605          conversationId: 1,
606          countryCode: 'US',
607        });
608  
609        const orderCall = axiosPostMock.mock.calls.find(c =>
610          c.arguments[0].includes('/v2/checkout/orders')
611        );
612        const body = orderCall.arguments[1];
613        assert.equal(body.purchase_units[0].items[0].category, 'DIGITAL_GOODS');
614        assert.equal(body.purchase_units[0].items[0].quantity, '1');
615      });
616  
617      test('sets intent to CAPTURE', async () => {
618        axiosPostMock.mock.mockImplementation(async url => {
619          if (url.includes('/oauth2/token')) return mockAccessToken();
620          return mockOrderResponse('ORDER_INTENT');
621        });
622  
623        await createPaymentOrder({
624          domain: 'intent.com',
625          email: 'buyer@test.com',
626          siteId: 1,
627          conversationId: 1,
628          countryCode: 'US',
629        });
630  
631        const orderCall = axiosPostMock.mock.calls.find(c =>
632          c.arguments[0].includes('/v2/checkout/orders')
633        );
634        const body = orderCall.arguments[1];
635        assert.equal(body.intent, 'CAPTURE');
636      });
637  
638      test('includes domain in item description', async () => {
639        axiosPostMock.mock.mockImplementation(async url => {
640          if (url.includes('/oauth2/token')) return mockAccessToken();
641          return mockOrderResponse('ORDER_DESC');
642        });
643  
644        await createPaymentOrder({
645          domain: 'my-special-domain.com',
646          email: 'buyer@test.com',
647          siteId: 1,
648          conversationId: 1,
649          countryCode: 'US',
650        });
651  
652        const orderCall = axiosPostMock.mock.calls.find(c =>
653          c.arguments[0].includes('/v2/checkout/orders')
654        );
655        const body = orderCall.arguments[1];
656        assert.ok(body.purchase_units[0].description.includes('my-special-domain.com'));
657        assert.ok(body.purchase_units[0].items[0].description.includes('my-special-domain.com'));
658      });
659  
660      test('returns exchangeRate from pricing in result', async () => {
661        axiosPostMock.mock.mockImplementation(async url => {
662          if (url.includes('/oauth2/token')) return mockAccessToken();
663          return mockOrderResponse('ORDER_RATE');
664        });
665  
666        const result = await createPaymentOrder({
667          domain: 'rate-test.com',
668          email: 'buyer@test.com',
669          siteId: 1,
670          conversationId: 1,
671          countryCode: 'AU',
672        });
673  
674        assert.equal(result.exchangeRate, 1.505);
675        assert.equal(result.currency, 'AUD');
676        assert.equal(result.amountUsd, 297);
677      });
678  
679      test('amount breakdown matches item total', async () => {
680        axiosPostMock.mock.mockImplementation(async url => {
681          if (url.includes('/oauth2/token')) return mockAccessToken();
682          return mockOrderResponse('ORDER_BREAKDOWN');
683        });
684  
685        await createPaymentOrder({
686          domain: 'breakdown.com',
687          email: 'buyer@test.com',
688          siteId: 1,
689          conversationId: 1,
690          countryCode: 'GB',
691        });
692  
693        const orderCall = axiosPostMock.mock.calls.find(c =>
694          c.arguments[0].includes('/v2/checkout/orders')
695        );
696        const body = orderCall.arguments[1];
697        const pu = body.purchase_units[0];
698  
699        // Top-level amount, breakdown item_total, and item unit_amount should all match
700        assert.equal(pu.amount.value, pu.amount.breakdown.item_total.value);
701        assert.equal(pu.amount.value, pu.items[0].unit_amount.value);
702        assert.equal(pu.amount.currency_code, pu.amount.breakdown.item_total.currency_code);
703        assert.equal(pu.amount.currency_code, pu.items[0].unit_amount.currency_code);
704      });
705    });
706  });