|
1 | 1 | import { resetEnvMock, setEnv } from '@sim/testing' |
2 | | -import { afterEach, beforeEach, describe, expect, it } from 'vitest' |
| 2 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
3 | 3 | import { QuickBooksBlock } from '@/blocks/blocks/quickbooks' |
4 | 4 | import { |
5 | 5 | quickbooksCreateBillPaymentTool, |
@@ -52,7 +52,10 @@ const itemLine = { |
52 | 52 | } |
53 | 53 |
|
54 | 54 | beforeEach(() => setEnv({ QUICKBOOKS_ENV: 'sandbox' })) |
55 | | -afterEach(resetEnvMock) |
| 55 | +afterEach(() => { |
| 56 | + vi.unstubAllGlobals() |
| 57 | + resetEnvMock() |
| 58 | +}) |
56 | 59 |
|
57 | 60 | describe('QuickBooks purchasing reader', () => { |
58 | 61 | const listParams: QuickBooksReadPurchasingTransactionsParams = { |
@@ -447,6 +450,135 @@ describe('QuickBooks purchasing mutation bodies', () => { |
447 | 450 | }) |
448 | 451 | }) |
449 | 452 |
|
| 453 | +describe('QuickBooks BillPayment account compatibility', () => { |
| 454 | + const params: QuickBooksCreateBillPaymentParams = { |
| 455 | + ...authParams, |
| 456 | + vendorId: '30', |
| 457 | + totalAmount: 25, |
| 458 | + paymentType: 'check', |
| 459 | + paymentAccountId: '35', |
| 460 | + billAllocations: [{ billId: '12', amount: 25 }], |
| 461 | + requestId: 'sanitized-request-id', |
| 462 | + } |
| 463 | + |
| 464 | + function accountResponse(account: Record<string, unknown>): Response { |
| 465 | + return Response.json({ Account: account, time: 'test-time' }) |
| 466 | + } |
| 467 | + |
| 468 | + function billPaymentResponse(payType: 'Check' | 'CreditCard'): Response { |
| 469 | + return Response.json({ |
| 470 | + BillPayment: { Id: '44', SyncToken: '0', PayType: payType }, |
| 471 | + time: 'test-time', |
| 472 | + }) |
| 473 | + } |
| 474 | + |
| 475 | + it.each([ |
| 476 | + ['check', 'Bank', 'Check'], |
| 477 | + ['credit_card', 'Credit Card', 'CreditCard'], |
| 478 | + ] as const)( |
| 479 | + 'validates the account before creating a %s payment', |
| 480 | + async (paymentType, accountType, payType) => { |
| 481 | + const fetchMock = vi |
| 482 | + .fn() |
| 483 | + .mockResolvedValueOnce( |
| 484 | + accountResponse({ Id: '35', SyncToken: '0', Active: true, AccountType: accountType }) |
| 485 | + ) |
| 486 | + .mockResolvedValueOnce(billPaymentResponse(payType)) |
| 487 | + vi.stubGlobal('fetch', fetchMock) |
| 488 | + |
| 489 | + await expect( |
| 490 | + quickbooksCreateBillPaymentTool.directExecution!({ ...params, paymentType }) |
| 491 | + ).resolves.toMatchObject({ |
| 492 | + success: true, |
| 493 | + output: { recordId: '44', record: { PayType: payType } }, |
| 494 | + }) |
| 495 | + |
| 496 | + expect(fetchMock).toHaveBeenCalledTimes(2) |
| 497 | + expect(new URL(fetchMock.mock.calls[0][0] as URL).pathname).toBe( |
| 498 | + '/v3/company/123456789/account/35' |
| 499 | + ) |
| 500 | + const mutationUrl = new URL(fetchMock.mock.calls[1][0] as URL) |
| 501 | + expect(mutationUrl.pathname).toBe('/v3/company/123456789/billpayment') |
| 502 | + expect(mutationUrl.searchParams.get('requestid')).toBe('sanitized-request-id') |
| 503 | + expect(JSON.parse(fetchMock.mock.calls[1][1].body as string)).toMatchObject({ |
| 504 | + PayType: payType, |
| 505 | + }) |
| 506 | + } |
| 507 | + ) |
| 508 | + |
| 509 | + it.each([ |
| 510 | + ['check', 'Credit Card', 'Bank'], |
| 511 | + ['credit_card', 'Bank', 'Credit Card'], |
| 512 | + ] as const)( |
| 513 | + 'rejects a %s payment when the account is %s without mutating', |
| 514 | + async (paymentType, accountType, expectedType) => { |
| 515 | + const fetchMock = vi |
| 516 | + .fn() |
| 517 | + .mockResolvedValue( |
| 518 | + accountResponse({ Id: '35', SyncToken: '0', Active: true, AccountType: accountType }) |
| 519 | + ) |
| 520 | + vi.stubGlobal('fetch', fetchMock) |
| 521 | + |
| 522 | + await expect( |
| 523 | + quickbooksCreateBillPaymentTool.directExecution!({ ...params, paymentType }) |
| 524 | + ).rejects.toThrow(`require a QuickBooks ${expectedType} account`) |
| 525 | + expect(fetchMock).toHaveBeenCalledTimes(1) |
| 526 | + } |
| 527 | + ) |
| 528 | + |
| 529 | + it('rejects inactive and mismatched account records without mutating', async () => { |
| 530 | + const fetchMock = vi |
| 531 | + .fn() |
| 532 | + .mockResolvedValueOnce( |
| 533 | + accountResponse({ Id: '35', SyncToken: '0', Active: false, AccountType: 'Bank' }) |
| 534 | + ) |
| 535 | + .mockResolvedValueOnce( |
| 536 | + accountResponse({ Id: '99', SyncToken: '0', Active: true, AccountType: 'Bank' }) |
| 537 | + ) |
| 538 | + vi.stubGlobal('fetch', fetchMock) |
| 539 | + |
| 540 | + await expect(quickbooksCreateBillPaymentTool.directExecution!(params)).rejects.toThrow( |
| 541 | + 'payment account is inactive' |
| 542 | + ) |
| 543 | + await expect(quickbooksCreateBillPaymentTool.directExecution!(params)).rejects.toThrow( |
| 544 | + 'different payment account' |
| 545 | + ) |
| 546 | + expect(fetchMock).toHaveBeenCalledTimes(2) |
| 547 | + }) |
| 548 | + |
| 549 | + it('preserves bounded QuickBooks fault guidance from the account preflight', async () => { |
| 550 | + vi.stubGlobal( |
| 551 | + 'fetch', |
| 552 | + vi |
| 553 | + .fn() |
| 554 | + .mockResolvedValue( |
| 555 | + Response.json( |
| 556 | + { Fault: { Error: [{ code: '3200', Message: 'Authentication failed' }] } }, |
| 557 | + { status: 401, headers: { intuit_tid: 'tracking-id' } } |
| 558 | + ) |
| 559 | + ) |
| 560 | + ) |
| 561 | + |
| 562 | + await expect(quickbooksCreateBillPaymentTool.directExecution!(params)).rejects.toThrow( |
| 563 | + 'Reconnect the QuickBooks credential' |
| 564 | + ) |
| 565 | + }) |
| 566 | + |
| 567 | + it('propagates cancellation and does not create a payment', async () => { |
| 568 | + const controller = new AbortController() |
| 569 | + const fetchMock = vi.fn().mockImplementationOnce(() => { |
| 570 | + controller.abort(new Error('cancelled')) |
| 571 | + return accountResponse({ Id: '35', SyncToken: '0', Active: true, AccountType: 'Bank' }) |
| 572 | + }) |
| 573 | + vi.stubGlobal('fetch', fetchMock) |
| 574 | + |
| 575 | + await expect( |
| 576 | + quickbooksCreateBillPaymentTool.directExecution!(params, controller.signal) |
| 577 | + ).rejects.toThrow('cancelled') |
| 578 | + expect(fetchMock).toHaveBeenCalledTimes(1) |
| 579 | + }) |
| 580 | +}) |
| 581 | + |
450 | 582 | describe('QuickBooks purchasing block', () => { |
451 | 583 | it('does not force array-valued wand prompts through JSON-object generation', () => { |
452 | 584 | for (const id of ['purchasingLines', 'billAllocations']) { |
|
0 commit comments