Skip to content

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

Merged
waritctd merged 32 commits into
developfrom
fix/deposit-null-percent-matches-pdf
Oct 27, 2026
Merged

waritctd merged 32 commits into
developfrom
fix/deposit-null-percent-matches-pdf

Conversation

@waritctd

@waritctd waritctd commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Owner rulings

  1. (2026-10-22) A blank deposit_percent on a new-engine quotation means 30%, on the PDF and on the deposit notice.
  2. (2026-10-22, amended 2026-10-23) A legacy quotation bills the 50% its PDF prints; the legacy PDF never reads the column, so this is a constant (QuotationRenderer.LEGACY_DEPOSIT_PERCENT).
  3. (2026-10-23, supersedes "a live legacy quotation always wins") The deposit notice follows the latest quotation the customer answered, across both engines: PR-origin new-engine ACCEPTED, DEAL_DIRECT new-engine APPROVED (its terminal state), legacy ACCEPTED. An ISSUED-but-unanswered revision does not move the deposit. With nothing answered, the newest live ISSUED one (either engine) is used, as before. Lines, percent and reference come from that one quotation.
  4. (2026-10-27) A 0% quotation skips the deposit path entirely, automatically — no manual policy change by the rep. fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173's policy sync does that; this PR's 0% refusal on both routes is only the guard. Do not release this PR to main without fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173.

Sales/CRM business-logic change, authorised by the sales-flow redesign. Existing drafts keep their stored percent.

The bugs

  • createFromPricingRequest stores deposit_percent = NULL; the renderer prints NULL as 30 (four literals) but both deposit routes billed 0.50.
  • The ticket route picked lines (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-review comment). Fixed by ruling 3.

Change (21 files, +592/−525 vs develop)

  • DealQuotationRenderAdapter: DEFAULT_DEPOSIT_PERCENT = 30 replaces four literals (PDF output unchanged). QuotationRenderer: LEGACY_DEPOSIT_PERCENT = 50 used at the old :325 literal.
  • DepositNoticeRepository: one resolver findGoverningQuotation(ticketId) → GoverningQuotation(quotationId, legacy, number, depositPercent), one SQL over sales.quotation covering 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 from findDealQuotationItems(id) or quotations.findById(id); reference defaults to its number (feeds the remaining-invoice match). Deleted pickQuotation.
  • 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: 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; mockPickTicketQuotationForDepositNotice deleted; 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)

Commit pair Red evidence
4e549ef → c814986 (blank = 30) expected 0.30 but was 0.5000 (both routes)
b52a896 → 5cb369f (legacy = 50) expected 0.50 but was 0.3000; repo Optional was empty
20b5233 tests-only cleanup (Opus-approved) revised tests red on develop: 0.30 vs 0.5000, 0.30 vs 0.7000, 0.50 vs 0.4000, mock 0.5 to be 0.3
c1c67db → f4ff0c8 (resolver) main test ["10 @ 9123.31"] vs ["5 @ 18084.99"]; reference "QT-2026-0001-1" vs null; repo 0.50 vs 0.30, 0.50 vs 0.20, id 2 vs 1; service 3 assertions; vitest 5
96af714 (ticket-level legacy pin) WHERE relaxed → Expecting 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 max comment. 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).

Commit pair Item Red evidence
4a40ed1 → c605dfb 1 owner check, ticket route (same lines as #1171, cherry-picked so the develop merge is clean) Expecting … ApiException: สร้างใบแจ้งรับมัดจำได้เฉพาะดีลที่อนุมัติแล้วเท่านั้น (409 leaked status, not 403); mock promise resolved … instead of rejecting
fe13f89 → 80ea939 4 0% governing quotation, ticket route :317 Expecting code to raise a throwable. (ticket route drafted 0.00); mock promise resolved … instead of rejecting
92ab24b → 0f71b68 7 reference only with governing lines expected: null but was: "QT-2026-1"
4e3e973 (tests only) 5, 6 vacuous repo fixtures both SQL mutations below passed 11/11 before; now red
9dae1de (tests only) 11 vacuous mock fixture dropping governing.quotation.depositPercent ?? passed 12/12 before; now expected 0.3 to be 0.4
439a656 → 0ac4f4e 8 dead "% มัดจำ" input expected <input type="number" …> to be null

Third-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.

Commit pair Item Red evidence
3bff49a → 7a31851 3 mock tie-break across both quotation arrays expected 'QT-2026-0011-1' to be 'QT-2026-0008-1' (legacy won)
e1bc8ab → 01ced6b dead PRICING_REQUEST+APPROVED arm (no writer produces it; every doc_status writer checked) Expecting an empty Optional but was containing value: GoverningQuotation[quotationId=3, legacy=false, number=QTD1-3, depositPercent=0.10]
c2fc558 (pin) Java-constant guard strips comments commented-out constant + real = 35: old guard green, new expected 30 to be 35
be22496 (pin) mock reference default (0f71b68) reverting the mock line: expected 'QT-2026-0003-1' to be ''
49f1762 (tests only) duplicate tests deleted each guarded mutation still killed by a remaining test (see Coverage)
6becc08, eb15c14, 91b43d1 comments no test: comments only
a668ffb 10, 12 comments (no test: comments only) —

Coverage

Test Pins
DealQuotationOutcomeIntegrationTest#createDepositNotice_newEngineAccepted_whenASiblingPricingRequestHasAnOlderLegacyIssuedQuotation_bothRoutesDraftThisQuotationsLinesAndPercent (param null → 0.30, 20 → 0.20) the reproduced bug: both routes draft the ACCEPTED new-engine quotation's lines, own percent (blank → 30) and number, never the older legacy ISSUED sibling's (which stores 70 to prove the column is not read); every line field equal across the two routes; one DEPOSIT_NOTICE_DRAFTED_FROM_QUOTATION event on the new-engine branch
…#createDepositNotice_legacyAcceptedNullPercent_whenASiblingPricingRequestHasANewerFortyPercentIssuedQuotation_bothRoutesBillTheAnsweredLegacyQuotationsFifty answered legacy beats a newer unanswered new-engine one on both routes: 0.50, legacy lines, legacy number
…#createDepositNotice_newEngineAcceptedZeroDeposit_bothRoutes409AndNoDraft a 0% quotation: both routes 409 with the same message (quotation number included), nothing written
…#createDraft_ticketRoute_byNonOwningSales_refusedAndWritesNothing ticket route: non-owner 403 on an unapproved deal (owner checked before status) and on a confirmed one; nothing written; owner still drafts
…#createDepositNoticeFromQuotation_newEngine_nonOwnerSales_cannotReach, …IssuedNotAccepted_still409, …WithAnOpenRevisionDraft_stillDraftsFromTheAcceptedOne PR route: non-owner 403; ISSUED-only 409; ACCEPTED row beats a newer open revision
DepositNoticeRepositoryDepositPercentIntegrationTest (12) legacy ACCEPTED > ISSUED, always 0.50 whatever is stored; non-live and ticket-level legacy rows excluded; answered legacy beats a newer unanswered v2; nothing answered → newest ISSUED in both insertion orders; DEAL_DIRECT APPROVED (inserted first) beats a newer legacy ISSUED; DEAL_DIRECT APPROVED beats an older ACCEPTED one (approval = answer); PR-origin ACCEPTED (inserted first) beats a newer ISSUED; NULL → 0.30; DEAL_DIRECT DRAFT/CANCELLED and PRICING_REQUEST APPROVED excluded; no quotation → empty
DepositNoticeServiceTest (47) percent from the governing row, a typed percent ignored, 0.50 with none; lines + percent + number from a governing new-engine row; legacy governing lines; approved_price lines win over the governing quotation's, reference stays null, percent still the governing 0.40
DepositNoticeDealQuotationAutofillIntegrationTest (2) line mapping through findGoverningQuotation + findDealQuotationItems on real Postgres (seq order, discount labels); non-live statuses (SUPERSEDED, PENDING_APPROVAL, …) excluded
DealQuotationRenderAdapterV3Test#thaiDocument_note2_nullDepositPercent… the PDF prints NULL as 30
vitest mockApi.depositNotices.test.js (13) create routes ignore a typed percent; PR route and ticket route both bill a 40% quotation at 0.4; the reference is the quotation number only when its lines are used (caller lines → '', 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)
vitest PricingRequestDetailPage.test.jsx offers "สร้างใบแจ้งยอดเงินรับมัดจำ" … no "% มัดจำ" input; the button posts {}

Mutations, each killed by a named test (implementer; earlier rounds by two reviewers): v2 default 30→50; DEFAULT_DEPOSIT_PERCENT 30→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 (:350 409 instead of 403); ticket-route 0% guard off, Java and mock; mock drops governing.quotation.depositPercent ?? (expected 0.3 to be 0.4); PR page sends depositPercent again (expected … to be called with [501, {}]).

Not covered: quotation_id vs timestamp divergence (only pre-2026-09-30 data can mix a legacy/PR quotation and a DEAL_DIRECT one on a ticket; DirectQuotationLocks blocks it since); a caller-supplied reference overriding the default; req.items (as opposed to approved_price) winning over the governing lines is not pinned separately (same branch of buildItemsFromRequest); 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 through api; 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.depositPercent stays 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 26 new CreateDepositNoticeFromQuotationRequest(null) test call sites stay untouched. Request-body fields are not in docs/api/api-surface.json or docs/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), from backend/:

./mvnw -B clean test -Dtest.fork.count=1 -Dtest='DealQuotationOutcomeIntegrationTest,DepositNoticeRepositoryDepositPercentIntegrationTest,DepositNoticeServiceTest,DepositNoticeControllerTest,OrderConfirmationIntegrationTest,DealQuotationConfirmOrderIntegrationTest,DepositNoticeDealQuotationAutofillIntegrationTest,PaymentTrackIntegrationTest,RemainingInvoiceServiceIntegrationTest'

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/:

npx vitest run src/api/mockApi.depositNotices.test.js src/api/contract.test.js src/features/pricingRequests/PricingRequestDetailPage.test.jsx

3 files, 294 tests passed (mockApi.depositNotices.test.js 12, incl. the Java-constant guard; contract.test.js; PricingRequestDetailPage.test.jsx 276).

Rebased onto develop c6f065c (0 behind). Trial merges (git merge-tree --write-tree, at a668ffb): With #1173 — textual conflict only in mockApi.depositNotices.test.js, BUT the merged tree has 2 red ITs (semantic clash, Opus-verified): #1173's issue_zeroAmountDraft_409_staysDraft_andConsumesNoNumber drafts its 0-baht notice through the ticket route, which this PR now refuses; and this PR's …ZeroDeposit_bothRoutes409AndNoDraft sees #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 plus DepositNoticePage.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 from frontend/: the 51 files that import ./mockApi.js plus PricingRequestDetailPage, DepositNoticePage, DealDocumentRegister, TicketDetailPage tests — 55 files, 1204 tests passed.

Not run: full mvnw verify, npm test, npm run build (CI).

Authz evidence

  • Ticket route POST /api/tickets/{id}/deposit-notice/draft (DepositNoticeService#createDraft): requireRole(sales) → requireTicketOwner → requireApprovedTicket → lifecycle. Real-Postgres pin createDraft_ticketRoute_byNonOwningSales_refusedAndWritesNothing: another sales rep gets 403 ไม่มีสิทธิ์เข้าถึงรายการนี้ before approval (not the status 409) and after order confirmation; 0 rows in sales.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.
  • PR route POST /api/pricing-requests/{id}/deposit-notice: requireOwner in OrderConfirmationService (unchanged), then the same owner check inside createDraftFromQuotation (same 403). Pin createDepositNoticeFromQuotation_newEngine_nonOwnerSales_cannotReach.
  • Mock depositNotices.createDraft: hasRole('sales') then owner, pinned by ticket-level createDraft refuses a sales rep who does not own the deal and creates nothing (403 for sales2, 403 for ceo on a missing ticket, never 404).
  • Read paths (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-review of 91b43d1 → fix pass 97cd8d1

Verdict: no new bug in the range. Fixed (tests/comments/mock only):

Commit Item Evidence
2280d34 every non-live (origin, status) pair pinned (aNewerNonLiveStatusNeverBeatsALiveIssuedQuotation, parameterised) + mixedTicket_aNewerAcceptedQuotationBeatsAnOlderDealDirectApprovedOne mutations red: PENDING_APPROVAL on PRICING_REQUEST, SUPERSEDED on DEAL_DIRECT, APPROVED ranked above ACCEPTED
97cd8d1 mock reference null (not '') when the caller supplies lines, as Java; isDealDirectOrigin reused; seed id bump beside the seeds; ONE_ITEM named expected '' to be null → green; consumers apply ?? ''
407e9b1 revision-after-confirm sentence moved to findGoverningQuotation; DD ACCEPTED contradiction fixed no test: comment-only

RepoDepositPercent now 42 tests (was 12; the parameterised cases). /code-review low of 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_item kept the confirmed lines; pre-existing) → separate follow-up PR. Deferred: CHECK tying APPROVED to DEAL_DIRECT (count prod first); delete the unread depositPercent DTO field; mock item fallback includes legacy rows (Java does not).

Reviews

  • Opus on the low-fix pass eb15c14: APPROVE WITH NOTES. Re-ran every red commit and four mutations against the deleted tests; built the old-vs-new SQL truth table on real Postgres (48 combinations: only PRICING_REQUEST+APPROVED differs, and no writer produces it); confirmed the rebase tree equals a merge of a668ffb onto develop. Notes applied: Autofill Javadoc (91b43d1), this body.
  • Third /code-review a668ffb: 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, a GoverningQuotation from the PR route and itemsFromGoverning beside itemsFromDealQuotation deferred.
  • Opus on the code-review fix pass a668ffb: APPROVE WITH NOTES (notes applied in this body: fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173 semantic clash, counts).
  • Opus confirmation of a723a61: APPROVE WITH NOTES. Opus on the tests-only cleanup 20b5233: APPROVE WITH NOTES. Opus on the resolver f4ff0c8: APPROVE WITH NOTES (notes folded: ticket-level legacy pin, Javadoc). /code-review of 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

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2

@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
demo-glr Ready Ready Preview Oct 27, 2026 2:18pm UTC

Request Review

@waritctd waritctd changed the title test(sales): null-percent quotation — deposit notice must bill the PDF's 30% (RED, awaits ruling) fix(sales): blank deposit percent on a new-engine quotation bills the 30% the PDF prints Oct 22, 2026
@waritctd waritctd changed the title fix(sales): blank deposit percent on a new-engine quotation bills the 30% the PDF prints fix(sales): a blank deposit percent bills what that engine's PDF printed (new-engine 30%, legacy 50%) Oct 22, 2026
@waritctd
waritctd marked this pull request as ready for review October 22, 2026 15:05
waritctd added a commit that referenced this pull request Oct 23, 2026
…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>
@waritctd

Copy link
Copy Markdown
Owner Author

Independent Opus confirmation review of head a723a61 (2026-10-23): APPROVE WITH NOTES, none blocking.

Notes (follow-ups):

  1. Low: only the TH tile render path pins "blank prints 30%" (DealQuotationRenderAdapterV3Test:1241); the EN / non-tile / hide-project render tests stay green with the default at 40. All four sites use the constant, so no live risk.
  2. Low: the sibling-legacy-ISSUED test landed in the fix commit c814986, not the test commit; verified it fails pre-fix (0.30 vs 0.7000).
  3. Info (already in the body): ticket route on a deal with a legacy ISSUED + v2 ACCEPTED quotation bills the legacy 50% with legacy lines — same row for percent and lines now, but still the quotation the customer did not accept. Owner to confirm.
  4. Info: legacy PDF never reads the stored percent (always prints 50); nothing writes that column on legacy rows.
  5. Info, predates PR: ticket route on an explicit 0% v2 quotation still drafts 0.00 (fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173's area); DepositNoticePage.jsx:472 previews 0.5 for it.

@waritctd

Copy link
Copy Markdown
Owner Author

/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.

  1. P2, CONFIRMED by running — ticket route on a mixed ticket bills the un-accepted legacy quotation. Fixture: PR-A legacy quotation ISSUED (percent NULL, 5 @ 18,084.99), PR-B new-engine quotation ACCEPTED at 20% (10 @ 9,123.31), order confirmed.

    Route develop PR head
    pricing-request route (B) 0.20, B's lines 0.20, B's lines
    ticket route createDraft(ticketId) 0.20, legacy lines 0.50, legacy lines

    Correct deposit 18,246.62; develop already billed 18,084.99 (legacy lines × B's percent); PR head bills 45,212.48. DepositNoticeRepository#findDealQuotationDepositPercent tries legacy first and COALESCE(…,50) no longer falls through; pickQuotation already preferred legacy ISSUED for items. Reachability: normal UI takes the pricing-request route when a PR is QUOTATION_ACCEPTED (DepositNoticePage.jsx:271-284); the ticket route is hit on a listForTicket failure, a direct POST (DepositNoticeController:61), or a DEAL_DIRECT deal after the legacy PR is superseded (stale legacy ISSUED rows are never retired: DealQuotationRepository:1491-1497 sweeps only v2 origins; supersedeSupersededChainQuotations only from CustomerQuotationService:383). State arises from pre-V187 data. Smallest fix: rank ACCEPTED across both engines before ISSUED, in one query, and make pickQuotation / findDealQuotationItemsForDeposit follow the same order so lines and percent come from the same quotation.

  2. Test bloat + coverage hole (fix in progress, tests-only commit to follow). New ITs at :391/:405 and the mock case at :266 duplicate existing fixtures (:252, mock :246); :427 copies :354 with 30→null; :447 is suspected to pass with the whole PR reverted (both percents NULL → develop's 0.50 fallback). And because every explicit-percent fixture uses 30 == the new default, no test pins that a quotation's own percent beats the default (OrderConfirmationService:489 set to the constant passes CI; same in the mock at :225).

  3. Mock drift opened by this PR (mockApi.js:13319, :16241): mock ticket route still ?? 0.5 (Java 0.30 for null) and mock legacy branch honours the typed percent (Java always 50). MOCK_DEFAULT_DEPOSIT_PERCENT has no guard against the Java constant (pattern: dealTrackingMeta.test.js).

  4. Dead "% มัดจำ" input PricingRequestDetailPage.jsx:1231 (sent at :1507, ignored by the backend) now shows 0.5 beside a button that drafts 30%; DealDepositPanel already removed its twin. QuotationDocumentView.jsx:285 still shows null as "-" (known follow-up).

  5. Design notes (owner call): (a) freeze the default at submit (DealQuotationRepository#submit:1259 COALESCE(deposit_percent,30)) + print-invariant backfill, making the reader-side defaults unnecessary; (b) the null→30 rule is now in 5 Java sites + SQL + JS — one effectiveDepositPercent() accessor; (c) legacy COALESCE(deposit_percent,50) reads the v2-only V165 column — legacy PDF never reads it, so a constant LEGACY_DEPOSIT_PERCENT = 50 and an existence check is the faithful mirror; the kept tests legacyChain_prefersAccepted… (0.30) / fallsBackToIssued… (0.20) pin values the legacy PDF never prints (latent; nothing writes the column).

Interplay with #1173 (already on that PR): a null-percent revision after a 0% confirmation stays NOT_REQUIRED while this PR bills 30%.

@waritctd waritctd changed the title fix(sales): a blank deposit percent bills what that engine's PDF printed (new-engine 30%, legacy 50%) 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 %) Oct 23, 2026
@waritctd

Copy link
Copy Markdown
Owner Author

/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.

  1. Pre-existing, widened — DepositNoticeService:104 the ticket-route draft (createDraft → createDraftFromQuotation(…, null, …)) checks role, status and lifecycle but never deal ownership: any sales user can POST on another rep's deal and read their governing quotation lines/prices/number and the customer tax id/address (GET on the same deal is 403), or plant a DRAFT that the owner is then offered to issue. Every sibling write path is owner-gated (fix(sales): owner check on the ticket-route deposit-notice draft #1171 did the quotation route). Fix: requireTicketOwner in the ticket route.
  2. Owner call — data, not code: notices created before this deploys (DRAFT or ISSUED) keep the 0.50 that prod stored for blank-percent new-engine quotations whose PDF printed 30%; no backfill, no delete endpoint, both screens hide "create" while a doc exists. Check on prod: SELECT d.* FROM sales.deposit_notice d JOIN sales.quotation q ON q.ticket_id=d.ticket_id WHERE d.status IN ('DRAFT','ISSUED') AND q.origin IN ('PRICING_REQUEST','DEAL_DIRECT') AND q.doc_status IN ('ACCEPTED','ISSUED','APPROVED') AND q.deposit_percent IS NULL AND d.deposit_percent <> 0.30. A one-statement backfill of DRAFT rows is cheap; ISSUED rows need a decision. feat(sales): sales may change the deposit % on a draft notice; the amount follows #1176 (editable %) is the UI way out once merged.
  3. Owner call — root cause stays: a live new-engine quotation still stores NULL, so NULL→30 is taught reader by reader (adapter ×4, repository, OrderConfirmationService, mock ×2), and fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173's followQuotationDepositPolicy returns early on NULL (its note 2, already ruled: blank after 0% means 30). Deeper fix: COALESCE(deposit_percent, 30) in the approve UPDATE (DealQuotationRepository:1288-1298) + backfill; then most of these readers go away.
  4. Pre-existing, now more reachable — DepositNoticeService:144 a 0% governing new-engine quotation yields a 0.00 notice on the ticket route (the PR route 409s), and DEAL_DIRECT deals only have the ticket route; the deal is then stuck (confirmDepositPaid 409, waiveDeposit 409). fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path #1173 covers it but is unmerged. Fix here: refuse a 0% governing quotation in the ticket route like OrderConfirmationService:481.
  5. Test vacuity — DepositNoticeRepositoryDepositPercentIntegrationTest:155 the DEAL_DIRECT-APPROVED-is-answered fixture inserts the DD row last, so quotation_id DESC alone picks it; "DD APPROVED ranked level with ISSUED" survives every test. Swap insert order + one fixture with an ACCEPTED row older than a DD APPROVED row.
  6. Test vacuity — :146 newest-first ISSUED fallback tested only with the v2 row newer; an engine-preferring tiebreak survives. Add the reverse order.
  7. DepositNoticeService:145 the reference default stamps the governing quotation's number even when the lines came from req.items or legacy approved_price rows; the remaining-invoice match keys on that reference, so a notice now qualifies Q's remaining invoice with non-Q lines where develop blocked it. Default the reference only when the lines came from governing; assert it in createDraft_legacyApprovedPriceItemsWinOverQuotationChainWhenBothExist.
  8. Owner call — PricingRequestDetailPage.jsx:4100 the live "% มัดจำ" input (prefilled 0.5, posted, ignored) is now more misleading (blank bills 30). Delete input + state + payload key + OrderConfirmationService:493-495 pass-through + the test pin, or leave for the follow-up.
  9. QuotationDocumentView.jsx:285 "มัดจำ -" for NULL (known follow-up; ?? 30 or the approve-time freeze).
  10. Owner design note — TicketRepository:896 Javadoc claims the deposit bills the newer DEAL_DIRECT APPROVED one; true only on the ticket route (the UI sends any deal with a QUOTATION_ACCEPTED PR to the PR route). And: DEAL_DIRECT APPROVED is an INTERNAL approval; the customer's answer on a direct deal is the confirm-order event (DEAL_PROMOTED_FROM_QUOTATION), which the pick ignores. Only pre-lock mixed tickets can hit it. Reword the Javadoc at minimum.
  11. Mock vacuity — mockApi.depositNotices.test.js:232 the only non-null new-engine fixture is 50 (== legacy and the no-quotation fallback), so dropping governing.quotation.depositPercent ?? passes 11/11. Use 40; add one ticket-route set-percent case.
  12. Comments contradict code — DepositNoticeService:132-135, DealDepositPanel.jsx:319, DepositNoticeServiceTest:931, DepositNoticePage.jsx:826 still say "LOCKED to the quotation's own deposit_percent"; "always one row" is false for req.items/approved_price lines.
  13. mockApi.js:1463 new governing picker duplicates mockPickLiveQuotationForItemFallback and compares ids across two sequences (mock "latest" can flip vs Java on mixed tickets).
  14. DepositNoticeRepository:140 the 50/30 rule in six Java + two mock sites; Integer sourceDepositPercent doubles as a route flag. Shape: createDraftFromQuotation(ticketId, req, GoverningQuotation source, actor).
  15. PR body: coverage table names a renamed test twice and claims a 50→0.50 case (now ", 0.30" / "20, 0.20"); ServiceTest count is 45 not 47; c814986 carried its own pin; no "Commands run"/"Authz evidence"/lint lines.

waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
… 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
waritctd added a commit that referenced this pull request Oct 23, 2026
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
waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
…'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
waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
…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
waritctd added a commit that referenced this pull request Oct 23, 2026
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
waritctd added a commit that referenced this pull request Oct 23, 2026
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
waritctd and others added 17 commits October 27, 2026 15:40
…'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
waritctd and others added 3 commits October 27, 2026 21:14
…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
@waritctd
waritctd merged commit 50cc572 into develop Oct 27, 2026
7 checks passed
waritctd added a commit that referenced this pull request Oct 27, 2026
…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>
waritctd added a commit that referenced this pull request Oct 27, 2026
…er rebase

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXmTJDVga17Rn5az4YBjZ2
waritctd added a commit that referenced this pull request Oct 27, 2026
…'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
waritctd added a commit that referenced this pull request Oct 27, 2026
…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
waritctd added a commit that referenced this pull request Oct 27, 2026
…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

This branch was successfully deployed

1 active deployment
Preview — 97cd8d10 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant