Skip to content

fix(sales): a no-deposit quotation puts the deal on the no-deposit payment path - #1173

Merged
waritctd merged 9 commits into
developfrom
fix/zero-deposit-quotation-sets-policy
Oct 27, 2026
Merged

waritctd merged 9 commits into
developfrom
fix/zero-deposit-quotation-sets-policy

Conversation

@waritctd

Copy link
Copy Markdown
Owner

What and why

Owner ruling (Ploy, 2026-10-22): a quotation that says ไม่รับมัดจำ (deposit percent 0, full payment) puts the deal on the no-deposit payment path automatically.

Before this change, the deal's deposit action 409'd on a 0% quotation (#1166's guard, kept), but sales.ticket.deposit_policy stayed REQUIRED. The import-request and final-payment gates stayed closed until someone called waiveDeposit by hand with a typed reason. A max-effort review also found that DepositNoticeService.issue() never checked the amount, so a 0-baht DRAFT made through the ticket route (which reads the quotation's 0% per ticket) could be issued as a numbered notice.

Contract / business-logic changes (sales/CRM, authorised under the sales-flow redesign)

  1. Policy sync at confirmation. The first real customer confirmation (TicketService.applyCustomerConfirmation, reached from OrderConfirmationService.confirmOrder and DealQuotationService.confirmOrderFromDirectQuotation) now sets deposit_policy = NOT_REQUIRED when the confirmed quotation's deposit_percent is exactly 0 and the policy is still REQUIRED. It writes a system reason ใบเสนอราคา <number> ระบุไม่รับมัดจำ (ชำระเต็มจำนวน) (deposit_policy_set_by = the confirming user) and the same POLICY_CHANGED event waiveDeposit writes. CREDIT_CUSTOMER and WAIVED are never overwritten. It does not go through waiveDeposit, whose write-access and reason checks are for people.
    • Payment track: the policy is written right after the null -> CUSTOMER_CONFIRMED advance. That edge is the same on both paths (PaymentTrack), so every later advance reads NOT_REQUIRED and walks the bypass path (CUSTOMER_CONFIRMED -> AWAITING_FINAL_PAYMENT -> FULLY_PAID). requireImportRequestIssuable and canConfirmFinalPaymentNow already admit bypass + CUSTOMER_CONFIRMED.
    • Only the new engine carries a percent. A legacy CustomerQuotation confirmation passes none, so it does not sync.
    • Java API: TicketService.confirmCustomer(ticketId, actor) keeps its signature (the HTTP route POST /api/tickets/{id}/confirm-customer is unchanged). A new overload confirmCustomer(ticketId, quotationNumber, quotationDepositPercent, actor) is added. confirmCustomerForPromotedQuotation now takes the quotation's number and percent; its only caller is confirmOrderFromDirectQuotation.
  2. Shared refusal on the policy. DepositNoticeService.createDraftFromQuotation (both POST /api/tickets/{id}/deposit-notice/draft and POST /api/pricing-requests/{id}/deposit-notice) answers 409 ดีลนี้ไม่ต้องเก็บมัดจำ (นโยบายมัดจำ <policy>) จึงสร้างใบแจ้งรับมัดจำไม่ได้ when DepositPolicy.bypassesDepositNotice(policy). This is checked after the role, owner (fix(sales): owner check on the ticket-route deposit-notice draft #1171), status and lifecycle checks. fix(sales): pricing-request มัดจำ draft accepts the new-engine ACCEPTED quotation of that pricing request #1166's percent guard on the PR route is kept as a second check for deals confirmed before this change. A full-payment DEAL_DIRECT deal therefore cannot draft a deposit notice on either route, which is intended: it has no deposit.
  3. issue() refuses a 0-baht notice. POST /api/deposit-notices/{id}/issue answers 409 ใบแจ้งรับมัดจำนี้มียอดมัดจำ 0 บาท จึงออกเอกสารไม่ได้ when depositAmount is null or ≤ 0. This is checked next to paymentTrackReady and before a document number is taken.
  4. No dead advertisement: canCreateDepositNotice already returns false on a bypass policy. No change.
  5. Mock (mockApi.js): the same sync on pricingRequests.confirmOrder and dealQuotations.promoteToDeal, the policy 409 on depositNotices.createDraft and pricingRequests.createDepositNoticeFromQuotation, and the amount 409 on depositNotices.issue. Messages match Java.

The deal page needs no UI change: DealDepositPanel already renders a bypass policy as "ข้ามขั้นตอนนี้ — นโยบายมัดจำคือ … ()".

1. Files changed

File Change
backend/src/main/java/th/co/glr/hr/ticket/TicketService.java Policy sync in applyCustomerConfirmation. New 4-arg confirmCustomer overload; the 2-arg form delegates to it. confirmCustomerForPromotedQuotation takes the quotation's number and percent.
backend/src/main/java/th/co/glr/hr/orderconfirmation/OrderConfirmationService.java confirmOrder passes the accepted new-engine quotation's number and percent to confirmCustomer.
backend/src/main/java/th/co/glr/hr/dealquotation/DealQuotationService.java confirmOrderFromDirectQuotation passes the direct quotation's number and percent.
backend/src/main/java/th/co/glr/hr/deposit/DepositNoticeService.java Bypass-policy 409 in createDraftFromQuotation. 0-baht 409 in issue(). One stale comment updated.
frontend/src/api/mockApi.js Mirrors of all three changes (syncNoDepositPolicy, requireDepositNoticePath, issue amount check).
backend/src/test/java/th/co/glr/hr/dealquotation/DealQuotationOutcomeIntegrationTest.java 5 new tests and 4 small helpers, appended after the deposit-notice section.
backend/src/test/java/th/co/glr/hr/dealquotation/DealQuotationConfirmOrderIntegrationTest.java 1 new test (end of the happy-path section) and a zeroDepositDirectDraft() helper.
backend/src/test/java/th/co/glr/hr/ticket/PaymentTrackIntegrationTest.java depositNoticeServiceIssue_onWaivedPolicyDeal_… now drafts while the deal is REQUIRED and waives afterwards, because createDraft now refuses a WAIVED deal. Same assertion: issue() 409s on a bypass policy.
frontend/src/api/mockApi.depositNotices.test.js 2 tests appended to the new-engine describe, 1 new describe at the end.
frontend/src/api/mockApi.promoteToDeal.test.js 1 test appended to the promoteToDeal describe.

2. Commands run

# backend, targeted only (never full verify), Docker/Testcontainers path, always clean
./mvnw -B -q clean test -Dtest.fork.count=1 -Dtest='DealQuotationOutcomeIntegrationTest,DealQuotationConfirmOrderIntegrationTest'          # red run
./mvnw -B -q clean test -Dtest.fork.count=1 -Dtest='DealQuotationOutcomeIntegrationTest,DealQuotationConfirmOrderIntegrationTest,PaymentTrackIntegrationTest,DepositNoticeServiceTest,TicketServiceTest,OrderConfirmationIntegrationTest'   # green
./mvnw -B -q clean test -Dtest.fork.count=1 -Dtest='<24 classes that draft/issue deposit notices or confirm orders>'   # regression, see below
./mvnw -B -q clean test -Dtest.fork.count=1 -Dtest='<the new/revised test methods>'   # once per mutation, then once clean after restoring
# frontend
npx vitest run src/api/mockApi.depositNotices.test.js src/api/mockApi.promoteToDeal.test.js src/api/contract.test.js
npx vitest run src/features/tickets/TicketDetailPage.test.jsx src/features/deposits/DepositNoticePage.test.jsx src/features/pricingRequests/PricingRequestDetailPage.test.jsx src/api/mockApi.dealQuotationOutcome.test.js src/api/mockApi.storedImportRequestRollup.test.js
npx eslint src/api/mockApi.js src/api/mockApi.depositNotices.test.js src/api/mockApi.promoteToDeal.test.js

3. Tests / build results

The integration tests ran on real Postgres (Testcontainers, Docker up), with -Dtest.fork.count=1 and clean every time. Full mvnw verify was not run locally; CI gates it.

  • Red run (tests only, before any production change): 4 new Java tests failed on assertions: expected: "NOT_REQUIRED" but was: "REQUIRED" ×2, and Expecting code to raise a throwable ×2 (a ticket-route draft on a WAIVED deal was created; a 0-baht notice was issued and numbered). The two wrong-way-round tests passed, as expected. 4 new mock tests failed on assertions (expected 'REQUIRED' to be 'NOT_REQUIRED' ×2, promise resolved … instead of rejecting ×2).
  • Green, after rebasing onto develop 38e228ae (fix(sales): owner check on the ticket-route deposit-notice draft #1171): DealQuotationOutcomeIntegrationTest 39/0/0, DealQuotationConfirmOrderIntegrationTest 32/0/0, PaymentTrackIntegrationTest 14/0/0, OrderConfirmationIntegrationTest 13/0/0, DepositNoticeServiceTest 48/0/0, TicketServiceTest 210/0/0. Total 356 pass, 0 fail, 0 error.
  • Regression set (pre-rebase; every class that drafts or issues a deposit notice, confirms an order, or quotes 0%): BillingNoteService, CommissionAutoCreate, CommissionDealLinkage, DealQuotationController, DealQuotationIntegration, DealQuotationSearchOrigins, DepositNoticeAccountNotification, DepositNoticeIssueGuard, DepositNoticeIssueRenderOutsideTransaction, RemainingInvoiceService, FinanceDeal, FinancePayableVatAndDue, ImportRequest ×5 + StoredImportRequest, InventoryDeliveryFulfilment, DerivedFulfilmentRollup, ProcurementService, DealTrackingAndActivity, SalesRouteWalk, StageFactGate. That is 24 classes, 573 pass, 0 fail, 0 error.
  • Mutation checks: see section 6. Each guard was removed in turn, only its tests went red, and the code was restored, rebuilt with clean, and run green.
  • Frontend: mockApi.depositNotices + mockApi.promoteToDeal + contract.test.js 30/30 after the rebase. Related UI and mock suites (TicketDetailPage, DepositNoticePage, PricingRequestDetailPage, mockApi.dealQuotationOutcome, mockApi.storedImportRequestRollup) 471/471 before the rebase. eslint on the changed files is clean. npm run build was not run (no build-affecting change).

4. Authz evidence

No authz change. The policy sync runs inside each confirmation path's existing gate (the owner for confirmOrder; requireEditAccess for the direct confirmation). The new 409s refuse a state; they do not widen or narrow who may act.

5. Known risks

6. Test coverage (test-first)

Tests were committed first (test(sales): … (RED)) and run red before any production code changed.

Real Postgres (Testcontainers, through the real services and repositories)

Test Pins Red before the fix Guard that turns it red (mutation)
DealQuotationOutcomeIntegrationTest.confirmOrder_zeroDepositQuotation_setsNotRequired_opensTheImportGate_andBothDraftRoutes409 PR-origin 0% quotation + confirmOrder → policy NOT_REQUIRED, reason names the quotation and ไม่รับมัดจำ, exactly one POLICY_CHANGED event naming it, payment CUSTOMER_CONFIRMED, requireImportRequestIssuable passes, ticket route 409 on the policy, PR route 409, zero deposit rows expected: "NOT_REQUIRED" but was: "REQUIRED" sync removed; draft guard removed
…confirmOrder_thirtyPercentQuotation_leavesTheDepositPolicyRequired (wrong-way-round) 30% → REQUIRED, reason null, no POLICY_CHANGED, import gate still 409 green (correct before and after) sync ignoring the percent
…confirmOrder_zeroDepositQuotationOnACreditCustomerDeal_keepsCreditCustomer (wrong-way-round) CREDIT_CUSTOMER + 0% → stays CREDIT_CUSTOMER, reason unchanged, only the waiver's event green (correct before and after) sync overwriting any policy
…depositDraft_onAWaivedDeal_bothRoutes409OnThePolicy_andWriteNothing 30% deal waived after confirm → ticket route AND PR route 409 ไม่ต้องเก็บมัดจำ, zero rows (30% keeps #1166's percent guard out of the way) Expecting code to raise a throwable (ticket route drafted) draft guard removed
…issue_zeroAmountDraft_409_staysDraft_andConsumesNoNumber REQUIRED deal on a 0% quotation (policy forced back via jdbc = a deal confirmed before this fix) drafts a 0-baht notice on the ticket route; issue() → 409 0 บาท, row still DRAFT, doc_number null, document_sequence unchanged, payment still CUSTOMER_CONFIRMED Expecting code to raise a throwable (issue numbered the 0-baht notice) issue amount guard removed
DealQuotationConfirmOrderIntegrationTest.zeroDepositDirectQuotation_confirmsOntoTheNoDepositPath_andFinalPaymentIsNoLongerBlocked DEAL_DIRECT 0% + confirmOrderFromDirectQuotation → NOT_REQUIRED, reason names the quotation, one POLICY_CHANGED, ticket-route draft 409, confirmFinalPayment (account) walks the bypass path to FULLY_PAID expected: "NOT_REQUIRED" but was: "REQUIRED" sync removed; draft guard removed
PaymentTrackIntegrationTest.depositNoticeServiceIssue_onWaivedPolicyDeal_… (revised) Still pins issue()'s bypass-policy 409; the draft is now made while REQUIRED and the deal waived afterwards green (revision keeps it valid on both sides) —

Mutation results (each guard removed in turn; only the new/revised methods run, clean each time):

  • sync removed → red: the PR-origin sync test, the DEAL_DIRECT sync test. Nothing else.
  • sync ignoring the percent → red: the 30% wrong-way-round test, plus the existing quotationFirstGhost_…_andDepositDraftUnlocks (its 50% direct deal gets flipped, so its deposit draft 409s). Both pin "a deposit-bearing quotation keeps REQUIRED".
  • sync overwriting any policy → red: the CREDIT_CUSTOMER test only.
  • draft guard removed → red: the PR-origin sync test, the waived-deal test, the DEAL_DIRECT test (all three assert a draft 409).
  • issue amount guard removed → red: the 0-baht issue test only.
  • restored → all green.

Mock (VITE_USE_MOCKS=true, not evidence about Java behaviour)

Test Red before the fix Mutation
mockApi.depositNotices.test.js › confirmOrder on a 0% quotation sets depositPolicy NOT_REQUIRED, and both draft routes 409 expected 'REQUIRED' to be 'NOT_REQUIRED' red on sync-off and on draft-guard-off
› a 30% quotation leaves REQUIRED; once waived, both draft routes 409 on the policy promise resolved … instead of rejecting red on draft-guard-off
› mockApi.depositNotices.issue refuses a 0-baht notice › 409s, and the draft stays DRAFT with no number promise resolved … instead of rejecting red on issue-guard-off
mockApi.promoteToDeal.test.js › a 0% (ไม่รับมัดจำ) quotation confirms the deal onto depositPolicy NOT_REQUIRED expected 'REQUIRED' to be 'NOT_REQUIRED' red on sync-off

Deliberately not covered

  • Legacy CustomerQuotation confirmations: they carry no percent and never sync. No test, because there is no 0% legacy quotation to drive.
  • A later accepted revision to 0% after the first confirmation: it does not sync (see known risks). Not pinned either way.
  • The HTTP layer (controllers): the change sits in services, and the routes and DTOs are unchanged.
  • Concurrency of the sync with a parallel waiveDeposit: both run under the confirmation's own locks and transaction. Not exercised.
  • Production data: deals confirmed before this ships are not backfilled here.

🤖 Generated with Claude Code

@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 4:00pm UTC

Request Review

waritctd added a commit that referenced this pull request Oct 23, 2026
…e is compare-and-set; no deposit button on a 0% quotation (RED)

Review follow-ups on #1173, written before the fix:

- F1 (real Postgres, DealQuotationConfirmOrderIntegrationTest): a confirmed 0% DEAL_DIRECT deal
  whose approved revision says 50% goes back to REQUIRED (reason names the revision, one more
  POLICY_CHANGED) and the ticket-route draft bills 0.50; the reverse (50% -> 0% revision) moves to
  NOT_REQUIRED. Wrong-way-round: a human CREDIT_CUSTOMER and a human NOT_REQUIRED are kept, with
  no event for the attempt.
- F2 (real Postgres, TicketRepositoryIntegrationTest): syncDepositPolicyFromQuotation is a
  compare-and-set. 0 rows when the policy is not the expected one, when a person set it
  (deposit_policy_set_by not null), or when payment_status is not CUSTOMER_CONFIRMED.
- Mock: the same follow in dealQuotations.approve; a person's NOT_REQUIRED is kept.
- F4: canCreateDepositNoticeFromQuotation refuses an accepted 0% quotation; the PR page hides
  the button.

TicketRepository gains syncDepositPolicyFromQuotation as a stub that does today's unconditional
write, so the tests compile and fail on their assertions. The fix commit replaces it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
waritctd added a commit that referenced this pull request Oct 23, 2026
…n, as a compare-and-set

Review follow-ups on #1173 (owner: "do what makes sense for CRM"). Sales/CRM business-logic
change, authorised under the sales-flow redesign.

- F1: TicketService#followQuotationDepositPolicy is the one place the system writes the policy.
  0% -> NOT_REQUIRED, any deposit -> REQUIRED, only while payment_status is CUSTOMER_CONFIRMED and
  only on a policy no person set (deposit_policy_set_by IS NULL; waiveDeposit always records its
  actor). It runs at the first confirmation (as before) and now also when a DEAL_DIRECT quotation
  is approved (DealQuotationService#approve), so a revision that adds a deposit no longer strands
  the deal and a revision to 0% leaves the deposit path. WAIVED, CREDIT_CUSTOMER and a typed
  NOT_REQUIRED are never touched. PRICING_REQUEST origin is not wired: R8 allows one accepted
  quotation per deal, so its policy is fixed at confirmOrder.
- F2: TicketRepository#syncDepositPolicyFromQuotation is a compare-and-set on the expected policy,
  set_by IS NULL and CUSTOMER_CONFIRMED; 0 rows writes no event. The system write now leaves
  deposit_policy_set_by NULL (the event still names the actor).
- F3 (comments only, no test): OrderConfirmationService's "no path syncs" note, the
  confirmOrderFromDirectQuotation step list, and confirmCustomerForPromotedQuotation's effects list.
- F4: canCreateDepositNoticeFromQuotation takes the page's new-engine quotation and refuses an
  accepted 0% one, so the PR page no longer offers a button the server 409s.
- Mock: followQuotationDepositPolicy replaces syncNoDepositPolicy (confirmOrder, promoteToDeal and
  now dealQuotations.approve); setDepositPolicy records depositPolicySetBy (not served).

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

Copy link
Copy Markdown
Owner Author

Independent Opus review of the follow-up commits d0dc513 + e87ad7e (2026-10-23): APPROVE WITH NOTES, none blocking. Notes 1 and 4 applied as 174d7a9 + 831fb0e. Still DRAFT pending two owner rulings (notes 2, 3) and the pre-merge /code-review.

  • Test-first confirmed: with d0dc513's production files, 5 backend assertion failures (two …policyFollows… got the wrong policy; three repo CAS tests expected 0 rows, got 1) and 4 frontend assertion failures. No compile/fixture errors.
  • Green at e87ad7e (clean, fork 1): TicketRepositoryIntegrationTest 34, DealQuotationConfirmOrderIntegrationTest 36, DealQuotationOutcomeIntegrationTest 39, vitest 366/366.
  • Compare-and-set IS pinned: removing AND deposit_policy = :expected turns 4 tests red (repo CAS, two confirm-order event counts, the 30%-stays-REQUIRED outcome test). The stray uncommitted removal found in the worktree was that probe, now dropped.
  • Checked: POLICY_CHANGED only on a 1-row update; concurrent waiver wins; only waiveDeposit and the sync write deposit_policy; mock keeps the same rules incl. human-set-wins.

Notes needing an owner ruling:
2. TicketService.java:970 returns early on a null percent (as ruled), but DEAL_DIRECT quotations can carry null (DealQuotationService:353). After #1170 (null = 30%), a deal confirmed at 0% then revised to a null-percent quotation stays NOT_REQUIRED while the quotation bills 30%: the stranded-deal shape F1 fixed, narrower.
3. Policy follows APPROVAL, not customer acceptance. Manager approves a 0% revision, customer rejects it: deal stays NOT_REQUIRED, the 50% original is already SUPERSEDED, waiveDeposit cannot set REQUIRED. Only a fresh approved quotation gets it back.

Info: a revision to 0% after a non-zero confirmation now syncs to NOT_REQUIRED; a stale 50% draft is then refused by issue() on the bypass policy.
Not covered: null-percent revision at service level; a real concurrent CAS loser; service-level approval before confirmation / after notice issued (repo level only); mock off-CUSTOMER_CONFIRMED and mock CREDIT_CUSTOMER kept.

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 27, 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 and others added 9 commits October 27, 2026 21:49
…it payment path (RED)

Real-Postgres and mock tests for the owner ruling of 2026-10-22, written before the fix:

- confirmOrder / confirmOrderFromDirectQuotation on a 0% (ไม่รับมัดจำ) quotation sets
  deposit_policy NOT_REQUIRED with a POLICY_CHANGED event naming the quotation, opens the
  import-request and final-payment gates, and both deposit-draft routes 409.
- Wrong-way-round: a 30% quotation leaves REQUIRED; a CREDIT_CUSTOMER deal stays CREDIT_CUSTOMER.
- A WAIVED deal 409s on both draft routes on the policy.
- issue() refuses a 0-baht DRAFT: stays DRAFT, no document number consumed.

PaymentTrackIntegrationTest's issue-on-WAIVED test now drafts while the deal is still REQUIRED
and waives afterwards, because createDraft is about to refuse a bypass-policy deal.

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

Owner ruling 2026-10-22. Sales/CRM payment-track change, authorised under the sales-flow redesign.

- TicketService.applyCustomerConfirmation: at the first real confirmation, a confirmed quotation
  with deposit_percent 0 moves a REQUIRED deal to NOT_REQUIRED (system reason naming the
  quotation, same POLICY_CHANGED event waiveDeposit writes). CREDIT_CUSTOMER / WAIVED are kept.
  The import-request and final-payment gates then take the bypass path without a manual waiver.
  confirmCustomer gains a 4-arg overload; confirmCustomerForPromotedQuotation takes the
  quotation's number and percent. OrderConfirmationService.confirmOrder passes the accepted
  new-engine quotation's; DealQuotationService.confirmOrderFromDirectQuotation passes the direct one's.
- DepositNoticeService.createDraftFromQuotation: 409 on a bypass policy, for both draft routes.
- DepositNoticeService.issue: 409 on a 0-baht (or null-amount) draft before any number is taken.
- mockApi.js mirrors all three.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e is compare-and-set; no deposit button on a 0% quotation (RED)

Review follow-ups on #1173, written before the fix:

- F1 (real Postgres, DealQuotationConfirmOrderIntegrationTest): a confirmed 0% DEAL_DIRECT deal
  whose approved revision says 50% goes back to REQUIRED (reason names the revision, one more
  POLICY_CHANGED) and the ticket-route draft bills 0.50; the reverse (50% -> 0% revision) moves to
  NOT_REQUIRED. Wrong-way-round: a human CREDIT_CUSTOMER and a human NOT_REQUIRED are kept, with
  no event for the attempt.
- F2 (real Postgres, TicketRepositoryIntegrationTest): syncDepositPolicyFromQuotation is a
  compare-and-set. 0 rows when the policy is not the expected one, when a person set it
  (deposit_policy_set_by not null), or when payment_status is not CUSTOMER_CONFIRMED.
- Mock: the same follow in dealQuotations.approve; a person's NOT_REQUIRED is kept.
- F4: canCreateDepositNoticeFromQuotation refuses an accepted 0% quotation; the PR page hides
  the button.

TicketRepository gains syncDepositPolicyFromQuotation as a stub that does today's unconditional
write, so the tests compile and fail on their assertions. The fix commit replaces it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n, as a compare-and-set

Review follow-ups on #1173 (owner: "do what makes sense for CRM"). Sales/CRM business-logic
change, authorised under the sales-flow redesign.

- F1: TicketService#followQuotationDepositPolicy is the one place the system writes the policy.
  0% -> NOT_REQUIRED, any deposit -> REQUIRED, only while payment_status is CUSTOMER_CONFIRMED and
  only on a policy no person set (deposit_policy_set_by IS NULL; waiveDeposit always records its
  actor). It runs at the first confirmation (as before) and now also when a DEAL_DIRECT quotation
  is approved (DealQuotationService#approve), so a revision that adds a deposit no longer strands
  the deal and a revision to 0% leaves the deposit path. WAIVED, CREDIT_CUSTOMER and a typed
  NOT_REQUIRED are never touched. PRICING_REQUEST origin is not wired: R8 allows one accepted
  quotation per deal, so its policy is fixed at confirmOrder.
- F2: TicketRepository#syncDepositPolicyFromQuotation is a compare-and-set on the expected policy,
  set_by IS NULL and CUSTOMER_CONFIRMED; 0 rows writes no event. The system write now leaves
  deposit_policy_set_by NULL (the event still names the actor).
- F3 (comments only, no test): OrderConfirmationService's "no path syncs" note, the
  confirmOrderFromDirectQuotation step list, and confirmCustomerForPromotedQuotation's effects list.
- F4: canCreateDepositNoticeFromQuotation takes the page's new-engine quotation and refuses an
  accepted 0% one, so the PR page no longer offers a button the server 409s.
- Mock: followQuotationDepositPolicy replaces syncNoDepositPolicy (confirmOrder, promoteToDeal and
  now dealQuotations.approve); setDepositPolicy records depositPolicySetBy (not served).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…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>
…it notice that cannot exist

Review note 4 on the follow-up. No test: copy-only.

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
…policy follows the quotation; pin the whole 0% walk

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
@waritctd

Copy link
Copy Markdown
Owner Author

Rebased onto develop (incl. #1170, 50cc572). Ploy rulings applied: 0% skips the deposit path automatically; blank new-engine % = 30 in the policy sync (effectiveDepositPercent); DEAL_DIRECT lock-after-confirm = separate PR. Clash fixes: 0-baht draft via jdbc; each route's exact 0% message pinned, plus #1170's quotation guards on a still-REQUIRED deal (mutation-checked). Whole 0% walk pinned in zeroDepositDirectQuotation_confirmsOntoTheNoDepositPath_andFinalPaymentIsNoLongerBlocked. Opus review: APPROVE WITH NOTES (all applied). /code-review medium: no verified blocker; follow-ups: PR-page button vs waived policy, Thai label for policy in the 409. Prod backfill check pending.

🤖 Generated with Claude Code

@waritctd
waritctd marked this pull request as ready for review October 27, 2026 18:05
@waritctd
waritctd merged commit 124c46c into develop Oct 27, 2026
7 checks passed
waritctd added a commit that referenced this pull request Oct 27, 2026
…issue save is refused before the lines; update is sales-only

- DepositNoticeService#issue: a stored percent outside a whole 1-100 (0%, 0.3330, 1.5) is 409
  before numbering; a 0-baht draft is 409 (same lines as #1173, so the two merge clean).
- DepositNoticeRepository#update: honours expectedUpdatedAt (AND updated_at = :expected, null
  skips it) and checks the header row count; 0 rows -> 409 "ถูกแก้ไขโดยผู้ใช้อื่น" while still a
  draft, else requireDraft's message, both before the item DELETE/INSERT. Dead null -> 0.50
  fallback removed (the service always sets the percent).
- DepositNoticeService#update: requireRole(SALES_ROLES) first, like create/issue; the
  whole-number rule skips a stored percent sent back unchanged.
- DepositNoticeDraftRequest: @DecimalMin exclusive with a Thai message.

GREEN: DepositNoticeServiceTest 50, DepositNoticeControllerTest 10,
DepositNoticeIssueGuardIntegrationTest 6. Mutations, each red on its named test: % re-check
narrowed to signum, amount check off, stamp predicate dropped, row count ignored, legacy-echo
skip removed, role gate removed, @DecimalMin back to inclusive English, stored-lines sum
switched to unitPrice x qty.

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
…s; one notice per deal in the UI; the deal page confirms before issuing

DepositNoticePage: a net-0 line counts as 0 (?? not ||); the stored % is rounded to the whole %
the field shows; money rounds per step (round2, now exported from quotationMeta); a blank
เพิ่มรายการ row is left out of the save and the issue checks; a cleared % sends null; the
draft's updatedAt goes as expectedUpdatedAt and a 409 reloads the draft and toasts the server
message; the issue gate has its own whole 1–100 % rule focused on the % field; the confirm
body is DepositIssueSummary, shared with DealDepositPanel.
DealDepositPanel: ออกเอกสาร opens a ConfirmDialog with the stored %, deposit, VAT and total; an
ISSUED notice says the deal already has its notice. canCreateDepositNoticeFromQuotation takes
the deal's notices and refuses once one is ISSUED; PricingRequestDetailPage step 6 reads them
(existing listByTicket) and links the ISSUED number instead of offering the create.
Mock: @Valid-shaped % checks before the lookups, sales-only + owner on update and issue, the
stored-% floor, the legacy echo, the stale stamp (updatedAt strictly increases), issue's %
and amount checks (amount lines as in #1173), no new draft on either route while one is
ISSUED, and the ส่วนลดพิเศษ fold for a flat adjustment.

GREEN: DepositNoticePage 15, mockApi.depositNotices 13, mockApi.promoteToDeal 11,
pricingRequestMeta 75, PricingRequestDetailPage 277, TicketDetailPage 169, contract +
serverContract + quotationMeta. ESLint clean. Mutations, each red on its named test: ?? -> ||,
stamp dropped, 409 reseed dropped, cleared % -> 0, blank-row filter off, % rule off,
populateForm rounding off, all per-step rounding off, mock owner check off (update and issue),
mock validation moved after the lookups, mock stamp off, mock reject leaks the %, mock
one-notice guard off, mock fold off, predicate ignores notices, PR page ignores notices, panel
line removed, panel issues without the dialog.

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
…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

This branch was successfully deployed

1 active deployment
Preview — b46d6aff 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