Skip to content

fix(sales): pricing-request มัดจำ draft accepts the new-engine ACCEPTED quotation of that pricing request - #1166

Merged
waritctd merged 20 commits into
developfrom
fix/deposit-notice-new-engine-pcr
Oct 22, 2026
Merged

waritctd merged 20 commits into
developfrom
fix/deposit-notice-new-engine-pcr

Conversation

@waritctd

@waritctd waritctd commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Why

QA finding 2026-10-18 on the develop stack: a deal quoted through the new engine (POST /pricing-requests/{id}/deal-quotations, the only stage-advancing quotation path since #1153) reaches ขั้น 10 but cannot get a ใบแจ้งยอดเงินรับมัดจำ from the UI. Both DealDepositPanel and DepositNoticePage call POST /api/pricing-requests/{id}/deposit-notice, which read only LEGACY CustomerQuotation rows and answered 409 "ยังไม่มีใบเสนอราคาที่ลูกค้ายอมรับสำหรับคำขอราคานี้" for every such deal.

What changes (sales/CRM: API behaviour change, stated explicitly)

Contract changes on POST /api/pricing-requests/{id}/deposit-notice (OrderConfirmationService#createDepositNoticeFromQuotation):

  1. With no ACCEPTED legacy quotation, it now falls back to THIS pricing request's ACCEPTED PRICING_REQUEST-origin (new-engine) quotation. Reference = that quotation's number; items = that quotation's sales.quotation_item lines; percent = that quotation's own deposit_percent (null → the existing per-ticket lookup, then 0.50).
  2. A 0% ("ไม่รับมัดจำ") new-engine quotation answers 409 ใบเสนอราคา <no.> ระบุไม่มีมัดจำ (ชำระเต็มจำนวน) จึงออกใบแจ้งยอดเงินรับมัดจำไม่ได้ instead of drafting a 0-baht notice.
  3. ISSUED-only still 409s with the original message. The legacy-quotation branch and the ticket-level route POST /api/tickets/{id}/deposit-notice/draft keep their previous behaviour (same per-ticket percent derivation, no 0% guard).

Files changed

  • backend/.../dealquotation/DealQuotationRepository.java — new findAcceptedForPricingRequest (ACCEPTED, newest by id; "latest by id" is wrong because an open revision DRAFT can outrank the accepted row); corrected the false "no revision chain" Javadoc on findLatestForPricingRequest.
  • backend/.../deposit/DepositNoticeRepository.java — findDealQuotationItems(quotationId) extracted from findDealQuotationItemsForDeposit (same SQL/mapper); findDealQuotationDepositPercent Javadoc corrected (engines are exclusive per pricing request, not per ticket).
  • backend/.../deposit/DepositNoticeService.java — createDraftFromQuotation(ticketId, req, Integer sourceDepositPercent, actor); createDraft delegates with null; itemsFromDealQuotation(quotationId).
  • backend/.../orderconfirmation/OrderConfirmationService.java — the new-engine branch + 0% guard described above.
  • backend/src/test/.../DealQuotationOutcomeIntegrationTest.java — 6 new cases (below) + fixture overloads.
  • frontend/src/api/mockApi.js — mirrors the PR route's new-engine branch, the 0% refusal, a findDealQuotationItems mapper, and the ticket-level new-engine item fallback.
  • frontend/src/api/mockApi.depositNotices.test.js — 4 new cases (below), new-engine drive helpers.
  • (Plan doc added in a32b02b was removed again in 06182e8 — the PR body is the handoff.)

Commands run

  • cd backend && ./mvnw -B -q -Dtest=DealQuotationOutcomeIntegrationTest -Dtest.fork.count=1 test (real Postgres via Testcontainers)
  • cd frontend && npx vitest run src/api/mockApi.depositNotices.test.js src/api/contract.test.js && npx eslint src/api/mockApi.js src/api/mockApi.depositNotices.test.js
  • Full mvnw verify / npm test / npm run build: CI only (targeted runs locally).
  • Clean-review pass (2 refactor commits, 3f3c3d4 + 59f623e: mock mapper reads the stored row, test fixture shrinks, Javadoc only). /code-review max run; findings carried into Known risks.
  • Real-stack walk on the QA stack (jar at 06182e8, DB glr_qa_stages, deal 55): deal page สร้างใบแจ้งยอดเงินรับมัดจำ → draft with reference QT-2026-0036-1, 30% (the quotation's own), ออกเอกสาร → GLRD69004 ISSUED, no 4xx.

Tests / build results

  • DealQuotationOutcomeIntegrationTest: 33/33 on 59f623e (integration tests RAN, real Postgres).
  • vitest deposit-notices + contract: 15/15; eslint clean.
  • CI on 59f623e: see checks (the earlier audit failure on 1d390bf was an npm advisory, not this change; develop's vite bump is merged in at b26e95c).

Authz evidence

No authz change. The PR route still runs requireOwner before any lookup; …_nonOwnerSales_cannotReach (real DB) pins 403 + no row for a non-owning sales user on the new-engine case. Note (pre-existing, not touched here, from the max review): the ticket-level draft route POST /api/tickets/{id}/deposit-notice/draft has no owner/viewer check — any sales user can draft on another rep's deal. Unverified by any test; needs its own fix.

Known risks (from /code-review max, all confirmed; none fixed here)

  1. NULL deposit_percent on an ACCEPTED new-engine quotation bills 0.50 while the PDF printed 30% (renderer null→30). New on this route (develop 409'd), pre-existing on the ticket route. Needs an owner ruling: shared null→30 constant, or submit() refusing a null percent.
  2. On a mixed ticket (legacy ACCEPTED on PR-A, new-engine ISSUED 30% on PR-B) the percent silently comes from PR-B's row — pre-existing per-ticket lookup behaviour; the corrected Javadoc now says so. Untested direction.
  3. findDealQuotationItems copies raw qty/final_unit_price: EN per-sqm rows (pieces × per-sqm price), EN/USD rows (THB + 7% VAT hardcoded), NET-mode double rounding, and DEAL_DIRECT ADJUSTMENT rows (qty −1 → PUT 400) — all pre-existing on the ticket route, now reachable through this route for ACCEPTED PR-origin quotations.
  4. recordOutcome has no open-revision guard (ACCEPTED Q1 + ISSUED Q2 possible); findForPricingRequest still picks newest-by-id and hides Q1's outcome controls once a revision exists — pre-existing since 84ebf7e (develop only).
  5. 11 of 12 hand-wired OrderConfirmationService test instances never call wireDealQuotationRepository; an unwired instance answers the original 409 on the new-engine branch.
  6. Mock divergences: the mock PR route still bills payload.depositPercent ?? 0.5 when the chosen quotation has no percent (Java ignores the payload), and its ticket-route 0% check keys on the cross-array picker.
  7. Not independently deployable with feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation #1168: a combined real-stack walk showed that after a designer acceptance no later party can accept (R8), so feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation #1168's next-party flow dead-ends — feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation #1168 is held for a ruling; this PR is safe alone.

Test coverage

Backend (DealQuotationOutcomeIntegrationTest, real Postgres):

  • …_newEngineAcceptedQuotation_createsTheSameDraftTheTicketRouteWould — reference = accepted number, items/percent equal the ticket route's, one DEPOSIT_NOTICE_DRAFTED_FROM_QUOTATION event. Red before fix (409).
  • …_newEngineAcceptedWithAnOpenRevisionDraft_stillDraftsFromTheAcceptedOne — Q2 DRAFT outranks Q1 by id; draft references Q1. Red before fix.
  • …_newEngineAccepted_whenASiblingPricingRequestHasALegacyIssuedQuotation_draftsThisQuotationsLinesAndPercent — sibling legacy row (forged deposit_percent = 70) supplies neither lines nor percent; draft at 0.30. Red before fix (sibling's percent).
  • …_newEngineAcceptedZeroDeposit_409AndNoDraft — 0% + fullPaymentTerm → 409, no row. Red before fix.
  • …_newEngineIssuedNotAccepted_still409 and …_newEngine_nonOwnerSales_cannotReach — regression pins, green on develop too (ISSUED-only already 409'd; requireOwner fires before any lookup).
    Mock (mockApi.depositNotices.test.js): reference/items/percent/discount label from the accepted new-engine quotation (red before: 409); ticket-level createDraft reads the same new-engine lines (red before: no items); ISSUED-only still 409 (pin); 0% → 409 and no draft (red before: drafted).
    Deliberately NOT covered: legacy-ACCEPTED + v2 ISSUED sibling carrying a percent (pre-existing behaviour, risk 2); EN/USD/NET-rounding/ADJUSTMENT item shapes (risk 3, pre-existing); the ticket-level owner gap (pre-existing, needs its own authz PR); UI e2e (verified by the hand walk above, not by e2e-real).

🤖 Generated with Claude Code

waritctd and others added 15 commits October 20, 2026 00:03
…otation (reproduces the 409)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…engine ACCEPTED quotation

The route read only legacy CustomerQuotation rows, so every deal quoted through
DealQuotationService.createFromPricingRequest got 409 at ขั้น 10. It now falls back
to the pricing request's new-engine ACCEPTED quotation and lets createDraft source
items and percent exactly as the ticket-level route does. ISSUED-only still 409s;
owner gate unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est's accepted quotation (reproduces I-1/I-2)

Two red tests against d247e6a:
- createDepositNoticeFromQuotation_newEngineAcceptedWithAnOpenRevisionDraft_stillDraftsFromTheAcceptedOne
  (I-2): an open revision draft outranks the accepted quotation by id, so the
  "latest for this pricing request" lookup 409s although Q1 is accepted.
- createDepositNoticeFromQuotation_newEngineAccepted_whenASiblingPricingRequestHasALegacyIssuedQuotation_draftsThisQuotationsLines
  (I-1): with items = null, createDraft picks a sibling pricing request's legacy
  ISSUED quotation first, so the draft carries PR-A's lines under PR-B's number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of THIS pricing request, not the ticket's latest

Review round 1 on d247e6a:
- I-2: the new-engine lookup took the pricing request's LATEST quotation and
  filtered for ACCEPTED, so an open revision draft (higher id) hid an accepted
  Q1 and the route 409'd. New DealQuotationRepository#findAcceptedForPricingRequest
  selects the ACCEPTED row directly.
- I-1: items = null let createDraft source lines ticket-wide, legacy first, so a
  sibling pricing request's legacy ISSUED quotation supplied the lines under this
  pricing request's number. Items now come from the accepted quotation itself
  (DepositNoticeService#itemsFromDealQuotation -> new
  DepositNoticeRepository#findDealQuotationItems, extracted unchanged from
  findDealQuotationItemsForDeposit, which now delegates to it).
- The percent is still createDraft's ticket-scoped, legacy-first pick: a known
  limitation shared with the ticket route, stated in the Javadoc.
- Javadoc corrections: m-1 (changelog prose dropped), m-2 (percent paragraph
  names the legacy branch), m-3 (PRICING_REQUEST origin does have revisions),
  m-4 (engine exclusivity is per pricing request, so legacy-first is a real
  preference on a mixed ticket).

Tests (red on d247e6a, green here):
- createDepositNoticeFromQuotation_newEngineAcceptedWithAnOpenRevisionDraft_stillDraftsFromTheAcceptedOne
- createDepositNoticeFromQuotation_newEngineAccepted_whenASiblingPricingRequestHasALegacyIssuedQuotation_draftsThisQuotationsLines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EPTED quotation (reproduces the mock 409)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mirror the new-engine quotation branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n picks; pin the ticket-level fallback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine; fix the fallback-case comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m narration

Comment-only. The dealQuotations field and setter docs now name the deposit
route's new-engine branch as a second user (and that an unwired instance answers
409 there). Drops two narration comments the calls already say, states the 409
rule directly, rewords findDealQuotationItems' circular Javadoc, and reflows one
ragged wrap in findDealQuotationDepositPercent's Javadoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e commit sha from the test comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n — implementation plan

The plan the branch was executed from (subagent-driven). Its "items = null" shape
for the new-engine branch was superseded during review by a pricing-request-scoped
lookup with items from that quotation; the commits and their tests carry the final
shape, the plan carries the reasoning and the coverage list. No test: docs-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…icing request's quotation (reproduces the sibling's percent)

createDepositNoticeFromQuotation_newEngineAccepted_whenASiblingPricingRequestHasALegacyIssuedQuotation_draftsThisQuotationsLinesAndPercent
(renamed from ..._draftsThisQuotationsLines): the new-engine quotation carries
30%, the sibling pricing request's legacy ISSUED row carries 70%, and the draft
must bill 0.30. Red on a32b02b: 0.7000, because createDraft's ticket-scoped,
legacy-first percent lookup reads the sibling's row.

No code path writes deposit_percent on a legacy row today, so the test seeds it
through jdbc; without the seed the lookup already falls through to this
quotation's own percent.

The issuedNewOriginQuotation/acceptedNewOriginQuotation/upsertWithValidityDays
helpers gain a deposit-percent overload; the existing call shapes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pted new-engine quotation's own

The new-engine drive can now set the quotation's depositPercent before submit;
the first new-engine case sets 30 and expects the draft to carry 0.3. Red on the
current mock: 0.5, because the route takes payload.depositPercent ?? 0.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quotation percent, not the ticket's

Cloud review: the new-engine branch took reference and items from this pricing
request's ACCEPTED quotation, but the percent still came from createDraft's
ticket-scoped, legacy-first lookup, so a sibling pricing request's legacy row
carrying a percent would be billed under this quotation's number.

- DepositNoticeService#createDraftFromQuotation(ticketId, req, sourceDepositPercent,
  actor): a non-null source percent is the draft's percent; createDraft keeps its
  signature and delegates with null (per-ticket lookup, unchanged).
- OrderConfirmationService#createDepositNoticeFromQuotation passes the chosen
  new-engine quotation's own depositPercent. The legacy branch passes null:
  CustomerQuotationDto carries no percent, so it keeps today's derivation.
- mockApi pricingRequests.createDepositNoticeFromQuotation mirrors it: the
  new-engine branch bills newEngine.depositPercent / 100; the legacy branch and
  the ticket-level createDraft are unchanged.

Test: createDepositNoticeFromQuotation_newEngineAccepted_whenASiblingPricingRequestHasALegacyIssuedQuotation_draftsThisQuotationsLinesAndPercent
(red 0.7000, now 0.30) and the mock new-engine case (red 0.5, now 0.3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d; qualify the percent Javadoc

The 70% legacy seed now asserts it updated exactly one row, so the test cannot
pass vacuously if the seed misses. The createDepositNoticeFromQuotation Javadoc
now says the source quotation's percent wins only when it sets one; a null
percent still falls back to the per-ticket lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 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 22, 2026 7:43am UTC

Request Review

waritctd and others added 5 commits October 22, 2026 12:37
…gine quotation

A 0% ("ไม่รับมัดจำ", full payment) quotation has nothing to bill. The new-engine branch
of createDepositNoticeFromQuotation passed depositPercent 0 straight into the draft,
producing a 0-baht notice that nothing downstream refuses to issue (issue() reads the
ticket's deposit_policy, which no path syncs from the quotation). Now 409 with the
quotation number. Mock mirrored.

Test-first: DealQuotationOutcomeIntegrationTest
.createDepositNoticeFromQuotation_newEngineAcceptedZeroDeposit_409AndNoDraft (real DB,
red: "Expecting code to raise a throwable" → green, class 33/33) and
mockApi.depositNotices.test.js "rejects a 0% (full-payment) new-engine quotation with
409 and creates no draft" (red: promise resolved with a draft → green, file 9/9).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the handoff

CLAUDE.md retired handoff docs in 2026-07; this was the first file under docs/superpowers
and already described the superseded first design. No test: docs-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ine deposit test fixtures

mockDepositNoticeItemsFromDealQuotation built the whole deal-quotation DTO to read six
fields that come straight off the stored row (mockDealQuotationItemDto only adds ceo* /
priceChangedFromCeo on top of the spread). The test drops a redundant id after a spread,
two conditional spreads the mock already gates on != null, and folds five single-field
expects into one toMatchObject. No behaviour or message change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of each reason

Comment-only. The "accepted, not latest" reasoning now lives once, on
DealQuotationRepository#findAcceptedForPricingRequest; the resolved-columns paragraph moves
onto findDealQuotationItems, the method that actually reads them; createDraft's inline
comment stops restating createDraftFromQuotation's Javadoc; one misplaced clause in
createDepositNoticeFromQuotation's Javadoc moves next to the sentence it qualifies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 59f623ed Deployed Oct 22, 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