Repository navigation
fix(sales): the deposit notice bills what the customer's latest answered quotation printed (blank new-engine % = 30, legacy = 50, one row for lines and %) - #1170
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…e button A null percent is not ไม่รับมัดจำ: the PDF prints it as 30% (#1170), so only an explicit 0% hides the button. Review note 1 on the follow-up; a falsy check would have passed every existing test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Independent Opus confirmation review of head a723a61 (2026-10-23): APPROVE WITH NOTES, none blocking.
Notes (follow-ups):
|
|
/code-review (2026-10-23, head a723a61): 10 finders → 5 themes after dedup; the money one reproduced on a real DB. Verdict: fix items 1–2 before merge; 3–5 are owner calls / follow-ups.
Interplay with #1173 (already on that PR): a null-percent revision after a 0% confirmation stays NOT_REQUIRED while this PR bills 30%. |
|
/code-review max on head d1294ba: 44 candidates → 16 verified + 2 sweep. Verdict: do NOT merge yet. Fix 1, 4, 5, 6, 7, 11, 12, 15 here (owner check, 0% refusal, reference default, three test-vacuity gaps, comments, body); 2, 3, 8, 10 need an owner ruling; 9, 13, 14 follow-ups.
|
…p, before the status check Code-review item 1 on #1170. Same tests as #1171 (1e4ef01 + 4bec63d, already on develop), cherry-picked so this branch carries identical lines and merges into develop cleanly: a non-owning rep gets 403 on an unapproved deal and on a confirmed one, nothing is written, and the owner still drafts. Mock: the same pin plus the sales-only role gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
… deal status Code-review item 1 on #1170. createDraftFromQuotation now calls requireTicketOwner right after the role check, before requireApprovedTicket, so a non-owning rep gets 403 and never the 409 that tells them whether another rep's deal is approved. Any sales user could otherwise draft on another rep's deal and read its governing quotation's lines, prices, number and the customer's tax id and address. The pricing-request route already ran requireOwner; the second check there gives the same 403. Same lines as #1171 (416dc3b + 93a7ed4, on develop), cherry-picked so the merge is clean. Mock createDraft mirrors the role and owner gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Code-review item 4 on #1170. The existing 0% test now also calls the ticket route on the same deal and pins the PR route's 409 message (with the quotation number) on both routes, and that nothing is written. Red today: the ticket route drafts a 0.00 notice. Mock test revised the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…it draft Code-review item 4 on #1170. The ticket route drafted a 0.00 notice for a ไม่รับมัดจำ quotation (the PR route already 409s), and a DEAL_DIRECT deal has only the ticket route, so the deal then stuck (confirmDepositPaid and waiveDeposit 409). createDraftFromQuotation now refuses when the governing quotation's percent is 0, with the PR route's own message, moved to DepositNoticeService.zeroDepositRefusal so both routes share one string. Scoped to the governing row's percent so #1173's deposit-policy guard composes with it. A legacy row is never 0 (its percent is the constant 50). Mock mirrors it with mockZeroDepositRefusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…'s number as reference Code-review item 7 on #1170. When legacy approved_price rows supply the lines, the draft still bills the governing quotation's percent but must not carry its number: the remaining-invoice match keys on the reference, so a notice with non-Q lines would qualify Q's remaining invoice. The fixture's governing row is now a new-engine 0.40 one so the percent assertion cannot pass on the 0.50 no-quotation fallback. Red today: reference "QT-2026-1". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…om the governing quotation Code-review item 7 on #1170. The ticket route stamped the governing quotation's number as the reference even when req.items or legacy approved_price rows supplied the lines; the remaining-invoice match keys on that reference, so such a notice qualified that quotation's remaining invoice with lines that were not its own. buildItemsFromRequest now returns only the caller's or approved_price lines; itemsFromGoverning supplies the governing quotation's, and only then does the reference default to its number. The percent still always comes from the governing quotation. Mock mirrors it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…insertion order, can satisfy Code-review items 5 and 6 on #1170. Tests-only; production unchanged. - The DEAL_DIRECT APPROVED row is now inserted before the legacy ISSUED one, and a new fixture puts a legacy ACCEPTED row before a DEAL_DIRECT APPROVED one (the approval governs: ruling 2026-10-23). Ranking DEAL_DIRECT APPROVED level with ISSUED used to pass all 11 tests; it now fails both ("expected 1L but was 2L", "expected 2L but was 1L"). - The nothing-answered fallback is checked in both insertion orders. An engine-preferring tiebreak (ORDER BY ..., (origin IS NULL), quotation_id DESC) used to pass; it now fails ("expected 4L but was 3L"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…nt case Code-review item 11 on #1170. Tests-only. The only non-null new-engine fixture was 50, equal to the legacy percent and the no-quotation fallback, so dropping `governing.quotation.depositPercent ??` from the mock ticket route passed 12/12. The fixture is now 40 and the same test drafts on the ticket route too; that mutation now fails ("expected 0.3 to be 0.4"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…d sends no percent Code-review item 8 on #1170 (owner ruling 2026-10-23: delete the dead input). The test pinned that the payload carries a depositPercent; it now pins that the input is absent and the body is {}. Red today: the input is there, prefilled 0.5, beside a button whose draft bills 30%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Code-review item 8 on #1170 (owner ruling 2026-10-23). The input was prefilled 0.5 and posted as depositPercent, which the backend never reads, beside a button whose draft bills the accepted quotation's own percent (a blank one 30%). Deleted the input, its state and the payload key; the page now posts {} like DepositNoticePage. OrderConfirmationService no longer copies request.depositPercent() into the draft request. The field stays on CreateDepositNoticeFromQuotationRequest, unread, so an older frontend build that still sends it is not refused, and so the ~36 test call sites across 12 classes stay as they are. Request-body fields are not in either API digest (verb + path only), so no digest changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Code-review items 10 and 12 on #1170. No test: comments and Javadoc only. - "LOCKED to the quotation's own deposit_percent" (DepositNoticeService, DealDepositPanel, DepositNoticePage, DepositNoticeServiceTest) now says what the code does: the percent is what the source quotation's PDF printed (legacy 50, new-engine its own, blank 30), a client-supplied percent is never read on create, 0.50 only with no live quotation, and update keeps the stored one. - "lines, percent and reference always from one row" (DepositNoticeService, DepositNoticeRepository, mock) now says: the percent always; the lines and reference unless req.items or approved_price rows supply the lines. - TicketRepository#findPricingChainFallbackItems: the DEAL_DIRECT-wins case is the ticket route's, on pre-lock data only, and the approval counts as the answer by owner ruling. - DepositNoticeRepositoryDepositPercentIntegrationTest's fixture note pointed at a Javadoc that no longer says what it claimed; the autofill IT now names itemsFromGoverning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…'s number as reference Code-review item 7 on #1170. When legacy approved_price rows supply the lines, the draft still bills the governing quotation's percent but must not carry its number: the remaining-invoice match keys on the reference, so a notice with non-Q lines would qualify Q's remaining invoice. The fixture's governing row is now a new-engine 0.40 one so the percent assertion cannot pass on the 0.50 no-quotation fallback. Red today: reference "QT-2026-1". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…om the governing quotation Code-review item 7 on #1170. The ticket route stamped the governing quotation's number as the reference even when req.items or legacy approved_price rows supplied the lines; the remaining-invoice match keys on that reference, so such a notice qualified that quotation's remaining invoice with lines that were not its own. buildItemsFromRequest now returns only the caller's or approved_price lines; itemsFromGoverning supplies the governing quotation's, and only then does the reference default to its number. The percent still always comes from the governing quotation. Mock mirrors it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…insertion order, can satisfy Code-review items 5 and 6 on #1170. Tests-only; production unchanged. - The DEAL_DIRECT APPROVED row is now inserted before the legacy ISSUED one, and a new fixture puts a legacy ACCEPTED row before a DEAL_DIRECT APPROVED one (the approval governs: ruling 2026-10-23). Ranking DEAL_DIRECT APPROVED level with ISSUED used to pass all 11 tests; it now fails both ("expected 1L but was 2L", "expected 2L but was 1L"). - The nothing-answered fallback is checked in both insertion orders. An engine-preferring tiebreak (ORDER BY ..., (origin IS NULL), quotation_id DESC) used to pass; it now fails ("expected 4L but was 3L"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…nt case Code-review item 11 on #1170. Tests-only. The only non-null new-engine fixture was 50, equal to the legacy percent and the no-quotation fallback, so dropping `governing.quotation.depositPercent ??` from the mock ticket route passed 12/12. The fixture is now 40 and the same test drafts on the ticket route too; that mutation now fails ("expected 0.3 to be 0.4"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…d sends no percent Code-review item 8 on #1170 (owner ruling 2026-10-23: delete the dead input). The test pinned that the payload carries a depositPercent; it now pins that the input is absent and the body is {}. Red today: the input is there, prefilled 0.5, beside a button whose draft bills 30%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Code-review item 8 on #1170 (owner ruling 2026-10-23). The input was prefilled 0.5 and posted as depositPercent, which the backend never reads, beside a button whose draft bills the accepted quotation's own percent (a blank one 30%). Deleted the input, its state and the payload key; the page now posts {} like DepositNoticePage. OrderConfirmationService no longer copies request.depositPercent() into the draft request. The field stays on CreateDepositNoticeFromQuotationRequest, unread, so an older frontend build that still sends it is not refused, and so the ~36 test call sites across 12 classes stay as they are. Request-body fields are not in either API digest (verb + path only), so no digest changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Code-review items 10 and 12 on #1170. No test: comments and Javadoc only. - "LOCKED to the quotation's own deposit_percent" (DepositNoticeService, DealDepositPanel, DepositNoticePage, DepositNoticeServiceTest) now says what the code does: the percent is what the source quotation's PDF printed (legacy 50, new-engine its own, blank 30), a client-supplied percent is never read on create, 0.50 only with no live quotation, and update keeps the stored one. - "lines, percent and reference always from one row" (DepositNoticeService, DepositNoticeRepository, mock) now says: the percent always; the lines and reference unless req.items or approved_price rows supply the lines. - TicketRepository#findPricingChainFallbackItems: the DEAL_DIRECT-wins case is the ticket route's, on pre-lock data only, and the approval counts as the answer by owner ruling. - DepositNoticeRepositoryDepositPercentIntegrationTest's fixture note pointed at a Javadoc that no longer says what it claimed; the autofill IT now names itemsFromGoverning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…ference blank 0f71b68 changed the mock (the reference defaults to the governing quotation's number only when that quotation supplied the lines) with no vitest. The 40% ticket-route case now drafts once with no items (reference = the quotation's number) and once with its own lines (reference blank, percent still the quotation's 40%). Reverting the mock's reference line to `payload.reference ?? governing?.quotation.number ?? ''` turns it red: "expected 'QT-2026-0003-1' to be ''". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Mirrors DepositNoticeRepositoryDepositPercentIntegrationTest#mixedTicket_aDealDirectApprovedQuotationBeatsAnOlderAcceptedOne: an accepted legacy quotation, then (after cancelling the pricing request, the seam that stages pre-lock data) a newer DEAL_DIRECT approval at 40%. Both are answers, so the newer one must govern the ticket-route draft. Red on the current mock, which ranks ids from two separate sequences (legacy from 9, new-engine from 3): "expected 'QT-2026-0011-1' to be 'QT-2026-0008-1'" (the legacy number won). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…test" spans both engines Review #3 finding 3 on #1170. mockCustomerQuotations (ids from 9) and mockDealQuotations (ids from 3) had separate counters, so mockGoverningQuotationForDepositNotice broke a rank tie on ids that did not follow creation order and a mixed-engine ticket billed the legacy quotation. Both arrays now draw ids from mockQuotationSeq, as Java has one sales.quotation_id; it starts past both seed sets. The ponytail note now names the one overlap left (literal deal seeds 1-2 on ticket 18 vs legacy seeds 1-2 on ticket 5, never compared). The zero-deposit refusal moves above the resolver's comment, so that comment sits on the function it describes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…clarations Review #3 cleanup on #1170. javaIntConstant matched the raw Java source, so `// public static final int DEFAULT_DEPOSIT_PERCENT = 30;` still passed (checked: green with the declaration commented out). It now runs apiSurface.js's stripComments first. Mutation: commenting out DEFAULT_DEPOSIT_PERCENT (//) and LEGACY_DEPOSIT_PERCENT (/* */) turns both cases red with "could not find int …"; both Java files restored byte-identical to HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
… the deposit pick (RED) Review #3 cleanup on #1170 (dead PRICING_REQUEST + APPROVED arm). No writer puts a PRICING_REQUEST-origin row in APPROVED: DealQuotationRepository#approve lands that origin on ISSUED, both recordOutcome writers allow only ACCEPTED/REJECTED/REVISION_REQUESTED, markQuotationStatus only SENT/ACCEPTED/REJECTED, every INSERT writes DRAFT or ISSUED, nothing updates origin, and no migration writes doc_status = 'APPROVED'. The ranking fixture drops its PRICING_REQUEST APPROVED row (now ...BeatsANewerIssuedOne), and the non-live test (v2Origins_ignoreNonLiveStatuses_includingAPricingRequestApproved) pins that such a row is not a candidate. Red on the current SQL: "Expecting an empty Optional but was containing value: GoverningQuotation[quotationId=3, legacy=false, number=QTD1-3, depositPercent=0.10]". Still 12 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…n the deposit pick Review #3 cleanup on #1170. findGoverningQuotation admitted a PRICING_REQUEST-origin APPROVED row and ranked it last (ELSE 2), a state no writer produces (see the RED commit). The WHERE now lists the three origins separately with APPROVED on DEAL_DIRECT only, and the ORDER BY is just answered (ACCEPTED or APPROVED) before ISSUED. Every reachable row keeps its rank; only the unreachable PRICING_REQUEST APPROVED row drops out. The mock resolver drops its APPROVED: 2 rank the same way (no vitest: the state cannot be reached through the mock api either, approve sends that origin to ISSUED). DepositNoticeRepositoryDepositPercentIntegrationTest: 12/12 green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Review #3 cleanups on #1170, comment-only (no test: comment-only). - CreateDepositNoticeFromQuotationRequest.depositPercent (and the same sentence in OrderConfirmationService): the field is not kept to spare stale clients. Spring Boot 4.1's JsonMapper disables FAIL_ON_UNKNOWN_PROPERTIES (no override in application.yml), so deleting it would refuse nobody; keeping its bounds still 400s a value outside 0-1. - DealDepositPanel: the JSX comment above the create button cited the ticket route's findGoverningQuotation, but the button calls the pricing-request route. The reason it sends no percent now sits on createNoticeMutation, naming the route it calls. - TicketRepository#findPricingChainFallbackItems: the DEAL_DIRECT divergence is not pre-lock only; a confirmed DEAL_DIRECT quotation stays revisable (DealQuotationService #requireRevisableStatus) and its approved revision then governs a ticket-route draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…sts already pin Review #3 cleanup on #1170 (test overlap). - DealQuotationOutcomeIntegrationTest#createDepositNoticeFromQuotation_newEngineAcceptedQuotation_createsTheSameDraftTheTicketRouteWould: its null-percent, reference and lines checks are the null row of createDepositNotice_newEngineAccepted_whenASiblingPricingRequestHasAnOlderLegacyIssuedQuotation_bothRoutesDraftThisQuotationsLinesAndPercent. Its two checks nothing else made move into that test: every line field equal across the routes (recursive comparison), and the new-engine branch writing DEPOSIT_NOTICE_DRAFTED_FROM_QUOTATION once. - DepositNoticeDealQuotationAutofillIntegrationTest: dealDirectApprovedQuotation_isReturned_… (DEAL_DIRECT APPROVED is a candidate: both mixedTicket_aDealDirectApproved… repository tests; its line mapping: pricingRequestIssuedQuotation_returnsItemsInSeqOrderWithDiscountLabels, the same origin-agnostic findDealQuotationItems) and acceptedPricingRequestQuotation_winsOverAnIssuedOne_… (pricingRequestOrigin_anAcceptedQuotationBeatsANewerIssuedOne, which puts the ACCEPTED row first so only the rank can pick it). The mapping test and the non-live test stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…equest test comment The comment still named mockCustomerQuotationSeq, which this branch renamed to mockQuotationSeq (one id sequence for both mock quotation arrays). Comment-only (no test: comment-only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…y test pins every status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
a668ffb to
91b43d1
Compare
…TED-beats-older-APPROVED ranking Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…tion; fix its ACCEPTED contradiction (no test: comment-only) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…he lines; reuse isDealDirectOrigin; seed id bump beside the seeds; test tidy-ups Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…e button A null percent is not ไม่รับมัดจำ: the PDF prints it as 30% (#1170), so only an explicit 0% hides the button. Review note 1 on the follow-up; a falsy check would have passed every existing test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er rebase Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…'s quotation guards on a REQUIRED deal; pin mock blank percent as 30 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…from #1170/#1173's hunks No behaviour change: requireNoIssuedDepositNotice now sits above buildMockDoc (it was adjacent to #1170's rewrite of mockPickTicketQuotationForDepositNotice), and the new predicate and step-6 tests open their describe blocks instead of sharing #1173's insertion points. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
…import does not collide with #1170's No behaviour change (DepositNoticeIssueGuardIntegrationTest 7, DepositNoticeServiceTest 51, DepositNoticeControllerTest 10 green with clean). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
Owner rulings
deposit_percenton a new-engine quotation means 30%, on the PDF and on the deposit notice.QuotationRenderer.LEGACY_DEPOSIT_PERCENT).Sales/CRM business-logic change, authorised by the sales-flow redesign. Existing drafts keep their stored percent.
The bugs
createFromPricingRequeststoresdeposit_percent = NULL; the renderer prints NULL as 30 (four literals) but both deposit routes billed 0.50.pickQuotation, legacy first) and percent (findDealQuotationDepositPercent, legacy first) in two hand-synced lookups. On a mixed ticket (PR-A legacy ISSUED never accepted, PR-B new-engine ACCEPTED at 20%) it billed the legacy lines; the first version of this PR made it worse (20% → 50%). Reproduced on a real DB (see the/code-reviewcomment). Fixed by ruling 3.Change (21 files, +592/−525 vs develop)
DealQuotationRenderAdapter:DEFAULT_DEPOSIT_PERCENT = 30replaces four literals (PDF output unchanged).QuotationRenderer:LEGACY_DEPOSIT_PERCENT = 50used at the old:325literal.DepositNoticeRepository: one resolverfindGoverningQuotation(ticketId)→GoverningQuotation(quotationId, legacy, number, depositPercent), one SQL oversales.quotationcovering both engines (APPROVED is live on DEAL_DIRECT only: a PRICING_REQUEST approval lands on ISSUED,DealQuotationRepository#approve),ORDER BY answered-first (ACCEPTED/APPROVED) → ISSUED, quotation_id DESC LIMIT 1; percent set in the mapper (legacy → 50 constant, never reads the column; v2 → own or 30). Ticket-level legacy rows (pricing_request_id IS NULL) excluded, as before. Deleted:findDealQuotationDepositPercent,findDealQuotationItemsForDeposit,firstOrNull.DepositNoticeService#createDraftFromQuotation: ticket route resolves the governing quotation once; lines fromfindDealQuotationItems(id)orquotations.findById(id); reference defaults to its number (feeds the remaining-invoice match). DeletedpickQuotation.OrderConfirmationService#createDepositNoticeFromQuotation: new-engine branch passes the quotation's own percent or 30; legacy branch passes 50 (no ticket-wide lookup).TicketRepository#findPricingChainFallbackItems: Javadoc only (display pick; names the cases where deal page and deposit pick differ).MOCK_DEFAULT_DEPOSIT_PERCENT; create routes ignore a typed percent (legacy 0.5; new-engine own-or-30); ticket route == PR route on percent and reference;mockPickTicketQuotationForDepositNoticedeleted; one quotation id sequence across both mock arrays, so "latest" spans both engines; a guard test parses both Java constants from the sources (comments stripped).Test-first (every pair red → green on real Postgres,
clean, fork 1)expected 0.30 but was 0.5000(both routes)expected 0.50 but was 0.3000; repoOptional was empty0.30 vs 0.5000,0.30 vs 0.7000,0.50 vs 0.4000, mock0.5 to be 0.3["10 @ 9123.31"] vs ["5 @ 18084.99"]; reference"QT-2026-0001-1" vs null; repo0.50 vs 0.30,0.50 vs 0.20, id 2 vs 1; service 3 assertions; vitest 5Expecting an empty Optional but was containing GoverningQuotation[…legacy=true…]Code-review fixes (head d1294ba → a668ffb)
Items 1, 4, 5, 6, 7, 8, 10, 11, 12, 15 of the
/code-review maxcomment. Owner rulings 2026-10-23: existing 50% notices are left (no backfill); freeze-at-approval is a separate PR; the dead "% มัดจำ" input is deleted; DEAL_DIRECT APPROVED stays the answer (Javadoc reworded only).Expecting … ApiException: สร้างใบแจ้งรับมัดจำได้เฉพาะดีลที่อนุมัติแล้วเท่านั้น(409 leaked status, not 403); mockpromise resolved … instead of rejecting:317 Expecting code to raise a throwable.(ticket route drafted 0.00); mockpromise resolved … instead of rejectingexpected: null but was: "QT-2026-1"governing.quotation.depositPercent ??passed 12/12 before; nowexpected 0.3 to be 0.4expected <input type="number" …> to be nullThird-review low fixes + rebase (head a668ffb → 91b43d1)
Rebased onto develop (0 behind;
git range-diff: every re-applied patch identical, so the SHAs above are pre-rebase). 4a40ed1/c605dfb5 were dropped by git as already on develop via #1171.expected 'QT-2026-0011-1' to be 'QT-2026-0008-1'(legacy won)doc_statuswriter checked)Expecting an empty Optional but was containing value: GoverningQuotation[quotationId=3, legacy=false, number=QTD1-3, depositPercent=0.10]= 35: old guard green, newexpected 30 to be 35expected 'QT-2026-0003-1' to be ''Coverage
DealQuotationOutcomeIntegrationTest#createDepositNotice_newEngineAccepted_whenASiblingPricingRequestHasAnOlderLegacyIssuedQuotation_bothRoutesDraftThisQuotationsLinesAndPercent(paramnull → 0.30,20 → 0.20)DEPOSIT_NOTICE_DRAFTED_FROM_QUOTATIONevent on the new-engine branch…#createDepositNotice_legacyAcceptedNullPercent_whenASiblingPricingRequestHasANewerFortyPercentIssuedQuotation_bothRoutesBillTheAnsweredLegacyQuotationsFifty…#createDepositNotice_newEngineAcceptedZeroDeposit_bothRoutes409AndNoDraft…#createDraft_ticketRoute_byNonOwningSales_refusedAndWritesNothing…#createDepositNoticeFromQuotation_newEngine_nonOwnerSales_cannotReach,…IssuedNotAccepted_still409,…WithAnOpenRevisionDraft_stillDraftsFromTheAcceptedOneDepositNoticeRepositoryDepositPercentIntegrationTest(12)DepositNoticeServiceTest(47)DepositNoticeDealQuotationAutofillIntegrationTest(2)findGoverningQuotation+findDealQuotationItemson real Postgres (seq order, discount labels); non-live statuses (SUPERSEDED, PENDING_APPROVAL, …) excludedDealQuotationRenderAdapterV3Test#thaiDocument_note2_nullDepositPercent…mockApi.depositNotices.test.js(13)'', still 0.4); a DEAL_DIRECT approval made after an accepted legacy quotation governs a mixed-engine ticket (number + 0.4); ticket == PR route on a blank percent (0.3) and reference; ticket route owner 403 + role gate before lookup; 0% → 409 on both routes; Java-constant guard (MOCK_DEFAULT_DEPOSIT_PERCENT==DEFAULT_DEPOSIT_PERCENT,MOCK_LEGACY_DEPOSIT_PERCENT==LEGACY_DEPOSIT_PERCENT; a commented-out declaration fails it)PricingRequestDetailPage.test.jsxoffers "สร้างใบแจ้งยอดเงินรับมัดจำ" …{}Mutations, each killed by a named test (implementer; earlier rounds by two reviewers): v2 default 30→50;
DEFAULT_DEPOSIT_PERCENT30→40; own percent ignored (Java and mock); ORDER BY without the answered key; ISSUED ranked level with ACCEPTED; DEAL_DIRECT APPROVED ranked level with ISSUED (expected 1L but was 2L,expected 2L but was 1L); engine-preferring tiebreak(origin IS NULL)(expected 4L but was 3L); legacy row reads the column; lines from the old legacy-first pick; reference default removed; reference default ignores who supplied the lines (expected: null but was: "QT-2026-1"); ticket-level legacy rows admitted; owner check after the status check (:350409 instead of 403); ticket-route 0% guard off, Java and mock; mock dropsgoverning.quotation.depositPercent ??(expected 0.3 to be 0.4); PR page sendsdepositPercentagain (expected … to be called with [501, {}]).Not covered:
quotation_idvs timestamp divergence (only pre-2026-09-30 data can mix a legacy/PR quotation and a DEAL_DIRECT one on a ticket;DirectQuotationLocksblocks it since); a caller-supplied reference overriding the default;req.items(as opposed toapproved_price) winning over the governing lines is not pinned separately (same branch ofbuildItemsFromRequest); a DEAL_DIRECT 0% quotation is not driven end to end (the 0% guard reads the governing row's percent, whatever its origin, and is pinned on a PR-origin 0% row); the mock's PRICING_REQUEST APPROVED narrowing (unreachable throughapi; a test would need a seam that writes an impossible state); the reverse mock tie-break order, DEAL_DIRECT older than legacy (the locks forbid it; the Java tests cover both orders).Kept on purpose:
CreateDepositNoticeFromQuotationRequest.depositPercentstays on the DTO, unread: deleting it would refuse no client (unknown JSON fields are ignored), while its bounds still answer a stale client sending a value outside 0–1 with 400; keeping it also leaves the 26new CreateDepositNoticeFromQuotationRequest(null)test call sites stay untouched. Request-body fields are not indocs/api/api-surface.jsonordocs/api/ui-reachable.json(verb + path only), so neither digest changes.Commands run
Local Postgres (
TEST_DB_URL=jdbc:postgresql://localhost:5432/impl1170cr_it, dropped after), frombackend/:BUILD SUCCESS, 193 tests, 0 failures: Outcome 36 (34
@Test+ 1@ParameterizedTest× 2), RepoDepositPercent 12, Service 47 (45@Test+ 2@org.junit.jupiter.api.Test), Controller 8, OrderConfirmation 13, ConfirmOrder 31, Autofill 4, PaymentTrack 14, RemainingInvoice 28.From
frontend/:3 files, 294 tests passed (
mockApi.depositNotices.test.js12, incl. the Java-constant guard;contract.test.js;PricingRequestDetailPage.test.jsx276).Rebased onto develop c6f065c (0 behind). Trial merges (
git merge-tree --write-tree, at a668ffb): With #1173 — textual conflict only inmockApi.depositNotices.test.js, BUT the merged tree has 2 red ITs (semantic clash, Opus-verified): #1173'sissue_zeroAmountDraft_409_staysDraft_andConsumesNoNumberdrafts its 0-baht notice through the ticket route, which this PR now refuses; and this PR's…ZeroDeposit_bothRoutes409AndNoDraftsees #1173's policy-guard message first. Both are test-only fixes on #1173 when it rebases (jdbc-insert the 0-baht draft; relax/order the message assertion). Both guards stay: the 0% guard covers deals confirmed before #1173's sync. With #1175 — clean. With #1176 — the five known comment conflicts plusDepositNoticePage.jsx(the deposit-% comment both PRs reword; keep #1176's).The run above is at a668ffb (pre-rebase). After the rebase onto develop (head eb15c14; 91b43d1 adds one test Javadoc line), the same setup:
DepositNoticeRepositoryDepositPercentIntegrationTest, DepositNoticeServiceTest, DealQuotationOutcomeIntegrationTest, DepositNoticeDealQuotationAutofillIntegrationTest, OrderConfirmationIntegrationTest, InventoryDeliveryFulfilmentIntegrationTest, TicketRepositoryIntegrationTest— 147 tests, 0 failures (Outcome 35, RepoDepositPercent 12, Service 47, Autofill 2, OrderConfirmation 13, InventoryDelivery 8, TicketRepository 30). Vitest fromfrontend/: the 51 files that import./mockApi.jsplusPricingRequestDetailPage,DepositNoticePage,DealDocumentRegister,TicketDetailPagetests — 55 files, 1204 tests passed.Not run: full
mvnw verify,npm test,npm run build(CI).Authz evidence
POST /api/tickets/{id}/deposit-notice/draft(DepositNoticeService#createDraft):requireRole(sales)→requireTicketOwner→requireApprovedTicket→ lifecycle. Real-Postgres pincreateDraft_ticketRoute_byNonOwningSales_refusedAndWritesNothing: another sales rep gets 403ไม่มีสิทธิ์เข้าถึงรายการนี้before approval (not the status 409) and after order confirmation; 0 rows insales.deposit_notice; the owner then drafts 1 row. Same lines as fix(sales): owner check on the ticket-route deposit-notice draft #1171 on develop.POST /api/pricing-requests/{id}/deposit-notice:requireOwnerinOrderConfirmationService(unchanged), then the same owner check insidecreateDraftFromQuotation(same 403). PincreateDepositNoticeFromQuotation_newEngine_nonOwnerSales_cannotReach.depositNotices.createDraft:hasRole('sales')then owner, pinned byticket-level createDraft refuses a sales rep who does not own the deal and creates nothing(403 forsales2, 403 forceoon a missing ticket, never 404).listByTicket,getById, preview, download) unchanged:requireTicketViewer.Lint
npx eslint src/features/pricingRequests/PricingRequestDetailPage.jsx src/features/pricingRequests/PricingRequestDetailPage.test.jsx src/features/deposits/DepositNoticePage.jsx src/features/tickets/DealDepositPanel.jsx src/api/mockApi.js src/api/mockApi.depositNotices.test.js— 0 problems./code-reviewof 91b43d1 → fix pass 97cd8d1Verdict: no new bug in the range. Fixed (tests/comments/mock only):
aNewerNonLiveStatusNeverBeatsALiveIssuedQuotation, parameterised) +mixedTicket_aNewerAcceptedQuotationBeatsAnOlderDealDirectApprovedOnenull(not'') when the caller supplies lines, as Java;isDealDirectOriginreused; seed id bump beside the seeds;ONE_ITEMnamedexpected '' to be null→ green; consumers apply?? ''findGoverningQuotation; DD ACCEPTED contradiction fixedRepoDepositPercent now 42 tests (was 12; the parameterised cases).
/code-review lowof 91b43d1..97cd8d1: no findings.Ploy 2026-10-27: a DEAL_DIRECT quotation locks after the order is confirmed (finding 1: a revision re-billed the deposit while
ticket_itemkept the confirmed lines; pre-existing) → separate follow-up PR. Deferred: CHECK tying APPROVED to DEAL_DIRECT (count prod first); delete the unreaddepositPercentDTO field; mock item fallback includes legacy rows (Java does not).Reviews
/code-reviewa668ffb: core change holds. Finding 1 (0% deal stuck on the ticket route) ruled by Ploy 2026-10-27, ruling 4 above → fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173. Finding 2 → fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173's rebase. Low 3 and the cleanups fixed in this pass; low 4/5 documented; the 3× ticket load, aGoverningQuotationfrom the PR route anditemsFromGoverningbesideitemsFromDealQuotationdeferred./code-reviewof a723a61 (earlier head): item 1 (mixed ticket) is fixed by the resolver; items 2 (test bloat / own-percent gap) fixed in 20b5233; item 3 (mock drift) fixed in the resolver commit; item 4 (PricingRequestDetailPage"% มัดจำ" input,QuotationDocumentView"-") and item 5 (freeze-at-submit design) remain follow-ups.Known risks / behaviour changes
mockApi.depositNotices.test.js(keep both) AND itsissue refuses a 0-baht noticemock test relies on create honouringdepositPercent: 0, which the mock no longer does — rewrite via a 0% quotation orupdate(feat(sales): sales may change the deposit % on a draft notice; the amount follows #1176). fix(sales): remaining invoice sources the accepted new-engine quotation when no deposit notice was issued #1175: merges clean but itsfindAcceptedDealQuotationLinesmust treat DEAL_DIRECT APPROVED as answered (follow-up on that PR). feat(sales): sales may change the deposit % on a draft notice; the amount follows #1176: five comment/Javadoc conflicts; keep feat(sales): sales may change the deposit % on a draft notice; the amount follows #1176's wording, point it atfindGoverningQuotation.🤖 Generated with Claude Code
https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2