From 6c13ec662c4b7aa5b252ef36d99f05cb999cdd8f Mon Sep 17 00:00:00 2001 From: waritctd <148909369+waritctd@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:14:21 +0700 Subject: [PATCH] fix(deposits): a failed pricing-request lookup no longer creates a ticket-level draft createDepositNoticeDraft swallowed a failed listForTicket and fell through to the ticket-level createDraft, so a network blip on a PCR deal built the deposit notice from the wrong source. The error now propagates: the caller's onError shows it and nothing is created. A deal with no pricing requests still takes the ticket-level route. (Release review of #1190, 2026-10-29.) Co-Authored-By: Claude Opus 5.5 --- .../deposits/DepositNoticePage.test.jsx | 15 +++++++-------- .../deposits/createDepositNoticeDraft.js | 17 ++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/frontend/src/features/deposits/DepositNoticePage.test.jsx b/frontend/src/features/deposits/DepositNoticePage.test.jsx index 040901277..a1dacfc40 100644 --- a/frontend/src/features/deposits/DepositNoticePage.test.jsx +++ b/frontend/src/features/deposits/DepositNoticePage.test.jsx @@ -386,19 +386,18 @@ describe('DepositNoticePage', () => { expect(api.pricingRequests.createDepositNoticeFromQuotation).not.toHaveBeenCalled(); }); - // A failure anywhere in the resolution step (network error, permission edge case) must - // degrade to the fallback path, never break the button or surface an unhandled rejection. - it('falls back to the ticket-level route when resolving pricing requests fails', async () => { + // A failed pricing-request lookup must NOT silently route the deal to the ticket-level engine + // (it could create the wrong kind of deposit notice): the error surfaces and nothing is created. + it('surfaces the error and creates nothing when resolving pricing requests fails', async () => { api.pricingRequests.listForTicket.mockRejectedValue(new Error('network error')); - renderDepositNoticePage(); + const showToast = vi.fn(); + renderDepositNoticePage({ showToast }); const createButton = await screen.findByRole('button', { name: /สร้างเอกสารฉบับร่าง/ }); fireEvent.click(createButton); - await waitFor(() => expect(api.depositNotices.createDraft).toHaveBeenCalledTimes(1)); - expect(api.depositNotices.createDraft).toHaveBeenCalledWith('701', { - notes: ['หมายเหตุมาตรฐาน'], - }); + await waitFor(() => expect(showToast).toHaveBeenCalledWith('error', 'network error')); + expect(api.depositNotices.createDraft).not.toHaveBeenCalled(); expect(api.pricingRequests.createDepositNoticeFromQuotation).not.toHaveBeenCalled(); }); diff --git a/frontend/src/features/deposits/createDepositNoticeDraft.js b/frontend/src/features/deposits/createDepositNoticeDraft.js index 04585029d..aba042775 100644 --- a/frontend/src/features/deposits/createDepositNoticeDraft.js +++ b/frontend/src/features/deposits/createDepositNoticeDraft.js @@ -5,21 +5,16 @@ import { api } from '../../api/index.js'; // // Prefer the pricing-request-chain endpoint (createDepositNoticeFromQuotation) when this deal has a // pricing request whose customer quotation was accepted — it sources items from the accepted -// quotation and autofills the header from the customer master + ticket project. A failure ANYWHERE -// in that lookup (network error, permission edge case, no pricing requests at all — a direct-quotation -// deal) degrades to the ticket-level route; it must never break the button. +// quotation and autofills the header from the customer master + ticket project. No pricing requests at +// all (a direct-quotation deal) takes the ticket-level route; a FAILED lookup rejects instead of guessing +// the route (release review 2026-10-29) — the caller's onError shows it and nothing is created. // depositPercent is not sent on either branch: the backend picks the new draft's percent // (DepositNoticeService.createDraftFromQuotation; findGoverningQuotation, #1170). // `noteTemplates` (already loaded) are optional. export async function createDepositNoticeDraft(ticketId, noteTemplates) { - let acceptedPricingRequestId = null; - try { - const { items: pricingRequests } = await api.pricingRequests.listForTicket(ticketId); - acceptedPricingRequestId = (pricingRequests ?? []) - .find((pr) => pr.status === 'QUOTATION_ACCEPTED')?.id ?? null; - } catch { - acceptedPricingRequestId = null; - } + const { items: pricingRequests } = await api.pricingRequests.listForTicket(ticketId); + const acceptedPricingRequestId = (pricingRequests ?? []) + .find((pr) => pr.status === 'QUOTATION_ACCEPTED')?.id ?? null; if (acceptedPricingRequestId != null) { return api.pricingRequests.createDepositNoticeFromQuotation(acceptedPricingRequestId, {}); }