Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions frontend/src/features/deposits/DepositNoticePage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
17 changes: 6 additions & 11 deletions frontend/src/features/deposits/createDepositNoticeDraft.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {});
}
Expand Down
Loading