Repository navigation
release: develop → main — sales/CRM stack with per-role lock release (backend image v2026-10-22, V193–V199.1) - #1169
Conversation
…ckend feat(sales): quotation ↔ deal linking slice 1 — list every origin, un-hide quotation-first deals, confirm-order
A deal's ช่องทางดีล (sales.ticket.entry_channel) now decides which stages it may visit. Owner ruling 2026-09-30; design in .design/deal-route-staging/. DESIGNER_LED S1..S9 in full off-route: none OWNER_DIRECT S1 S2 S3 S5 S6 ... off-route: S4 (no designer to quote) BUYER_DIRECT S1 S2 S3 S8 S9 ... off-route: S4 S5 S6 S7 S3/SPEC_APPROVED is KEPT on all three routes: it means "the party who specifies has agreed the spec", and only its wording differs per party (a frontend concern). That is what lets one stage list serve every route. Rung 6 of TicketService.requireStageMoveAllowed refuses a manual move whose TARGET is off-route (409), naming the remedy. It is last on purpose so a deal missing its deposit reports the FACT gate rather than blaming the route, and it tests the TARGET only, never the current stage — a deal may legitimately be sitting on an off-route stage after its channel was corrected, and gating the current stage would strand it with no legal move. autoAdvanceStage no-ops silently on an off-route target rather than throwing, so it can never fail an unrelated caller. DealRoute.ROUTE_VARIABLE pins the only stages a route may ever exclude (S4-S8) with a class-load check, so an OPERATIONAL advance (ORDER_RECEIVED, DEPOSIT_RECEIVED, PROCUREMENT, DELIVERED, CLOSED_PAID) can never vanish there with its fact already written. TWO DELIBERATE REVERSALS, stated rather than smuggled: - V144 ruled entry_channel "NOT a behavioural flag: nothing reads entry_channel to decide anything". It is behavioural now, by owner ruling. - SalesRouteWalkIntegrationTest.routeB asserted SPEC_APPROVED never happens on an owner-direct deal. It does now; S4 is the only party-axis exclusion. UNSPECIFIED, unknown and null gate NOTHING. Pre-V144 rows were never backfilled and read DESIGNER_LED whether chosen or defaulted, and GLA-156 removed the in-portal way to correct a channel — gating them would brick legacy and quotation-first deals with no remedy. DESIGNER_LED's empty off-route set also makes this a no-op for the entire back catalogue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A rep could not tell which of three routes a deal was on, and the UI actively misinformed them: every deal showed a 15-rung ladder including stages it would never visit, and the progress denominator was always 15. - Route line: "เส้นทาง · <ชื่อเส้นทาง> · ขั้นที่ p จาก t". Nothing renders when the channel is UNSPECIFIED/unknown/absent — a route is never invented. - Route ribbon: one pip per ON-ROUTE stage (11 for buyer-direct, 15 for designer-led). Off-route stages are absent from it, and that absence is the feature — the path becomes countable at a glance. - Pips carry the ROUTE ORDINAL (1..N), not the business S-number, so the ribbon and the "ขั้นที่ 4 จาก 11" line can never disagree. The business number still identifies a stage in the header, list and แก้ไขสถานะ modal. - PhaseSummary's denominator is the route's length. It was (stageIndex+1)/catalog.stages.length, so a buyer-direct deal at S8 read "ขั้นที่ 8 จาก 15" and filled the bar 53% when its route is 11 stages and S8 is its 4th. That is a bug fix, not a polish pass. - Off-route group "ไม่อยู่ในเส้นทางนี้ (N)" renders the server's blockedReason verbatim, following UpdateStageModal's blocked list: a refused path explains itself rather than being silently dimmed. Status is text, never colour alone. - dealStageLabel(code, entryChannel) words S3 per party. One-argument calls are unchanged, so the nine existing call sites keep working. - nextOnRoute skips off-route stages. onRoute absent/undefined is treated as on-route, so this degrades to today's behaviour against an older backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he panel These three called dealStageLabel(code) with one argument, so on an owner-direct deal the header chip read "ผู้ออกแบบอนุมัติสเปค" while the panel three inches below read "เจ้าของตกลงตามสเปคแล้ว" — on the same screen. TicketDetailPage:1233 was worse than a label mismatch. `next` came from the route-blind nextStageIn, and addStageActions only advertises ADVANCE_STAGE for stages whose decision is `allowed`. Off-route stages come back allowed:false, so on an owner-direct deal at S3 the code targeted S4, S4 was never advertised, and the "เลื่อนไป" item DISAPPEARED — a rep simply lost the ability to advance from the overflow menu. The comment above that block says the item must not "invite a click it's about to reject"; it now keeps that promise. The list also gains a route marker on the row and the mobile card, so a rep can tell a designer-led deal from a buyer-direct one while scanning rather than having to open it. Nothing renders for UNSPECIFIED. DealStateHeader had no test file at all; one is added. Measured before accepting: "ผู้ซื้อ/ผู้รับเหมาตกลงตามสเปคแล้ว" is 190px against the existing widest label at 188px, so it is tied, not longer, and wraps to two lines in the 174px chip column with no overflow at 140/160/174/200px. No new chip, no truncation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s read
The route gate refuses an off-route move with a message ending
"— แก้ช่องทางดีลก่อน". That remedy did not exist anywhere in the portal: GLA-156
removed EntryChannelControl and listed POST /api/tickets/{}/entry-channel as
unreachable from the UI. A rep was refused, told to fix the channel, and could
not. Design §13.1a: the control ships in the SAME slice as the gate.
Placed contextually — inline on each off-route row in the stepper group and on
each off-route row in UpdateStageModal's blocked list — NOT as a permanent panel
row, because GLA-156 stripped that panel to "stage, owner, next step" on purpose.
Inline rather than a dialog, since the modal case would otherwise nest a modal.
Three rules taken from the server, never recomputed (the service comments say the
advertisement and the 400 must not disagree):
- UNSPECIFIED is valid STORED but rejected as INPUT, so only the three real
channels are offered.
- The reason field appears only when the advertised action's requiredFields
includes 'note'. Both DESIGNER_LED and UNSPECIFIED count as UNSTATED after
V144's non-backfill, so a legacy deal's FIRST correction needs no note.
- The control renders only when SET_ENTRY_CHANNEL is advertised.
Deliberately NOT offered on rows blocked for other reasons (a missing fact, a
permission gate) — changing the channel cannot clear those, and a remedy that
does nothing is worse than none. A test pins the exclusion.
Save is disabled with a visible reason why, never silently dead. On success the
ticket, actions, list and dashboard invalidate, so the route, ribbon, denominator
and now-unblocked stage refresh without a reload.
docs/api/ui-reachable.json regenerated via UI_REACHABLE_UPDATE=1 (300 -> 301).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deal list still read "ขั้นตอน 8/15" for a buyer-direct deal whose own page said "ขั้นที่ 4 จาก 11" — the same 15-denominator lie, one screen earlier. The panel's fix does not transfer: it reads onRoute from the per-deal /actions payload, and a list row has none. Fetching one per row would be an N+1 on the hottest sales endpoint. So the backend serves the route instead, as PaymentTrack.path(policy) already does for payments (design §7): a "routes" map of channel -> DealRoute.path(channel), stage CODES only, on an endpoint the client already caches with staleTime: Infinity. UNSPECIFIED maps to all 15. Mirroring the table in frontend logic was rejected: stageCatalog.js forbids growing derivation there, and a hand-copied table drifts. DealStageMetaController had no test; nine are added, including that every route keeps the tail from NEGOTIATION onward and that anything a route drops sits inside DealRoute.ROUTE_VARIABLE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Until now the mock omitted onRoute, so under VITE_USE_MOCKS=true every stage read as on-route: the whole route feature was INVISIBLE in mock mode, and any mock-driven test of it would have passed while proving nothing. That is the third failure shape in CLAUDE.md's table — "mock OMITS a field the feature keys on" — and the one that only surfaces in prod. Mirrors DealRoute, with the route branch placed after the fact gate so the mock follows the same ladder order as TicketService: a deal missing its deposit reports the FACT gate, not the route. Scope is deliberately one function. The GLA-136 quotationOnly branch in it is untouched, since the quotation<->deal-link session is deleting it. This is plumbing evidence only. Route ENFORCEMENT is proved against the real Java service by DealRouteGateIntegrationTest — a mock can validate plumbing, it can never validate an algorithm it mirrors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design behind this branch, with all ten owner rulings recorded (R1-R10, 2026-09-30) and the reasoning that produced them. §12 enumerates the full transition case space — 15 stages, 210 ordered pairs per route, 630 triples — because the question "can a rep jump from any stage to any stage?" deserved an answer with numbers. Even with no route at all only 42 of 210 moves were ever free; the gate takes buyer-direct to 112 of 210 refused. §12.1 is the nine-rung precedence ladder. Eight rungs already existed; the route is rung 6, and that framing is what keeps this a small change. §13 records what the hard gate forces: the remedy must ship in the same slice, the owning rep must be able to correct the channel, and UNSPECIFIED must gate nothing. §13.3 records the recommendation I made against the ruling, and why the inline remedy makes the objection largely dissolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI's lint-and-build failed on jsx-a11y/aria-role: DealStateHeader takes `role`
as a component prop (the VIEWER's role — sales/ceo/hr), but the rule reads any
literal `role="..."` as the DOM ARIA attribute and rejects "sales" as invalid.
Production never hit this because TicketDetailPage passes `role={role}`, an
expression the rule cannot statically evaluate. The test now does the same via a
named constant, which also documents why.
Caught only by CI: this was the lint run skipped locally under the
targeted-tests-only rule. `npm run lint` and `npm run build` now both pass, and
DealStateHeader.test.jsx is 23/23.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One-click sign-in for the nine accounts the backend's demo profile seeds
(db/migration-demo V21 + V139), shown only when import.meta.env.DEV is true
and VITE_USE_MOCKS is not, so it is dead-code-eliminated from production
builds (verified: no "Demo@2026" in dist). Sends ordinary email+password
logins, never a mock { role }.
Tests: LoginPage.devQuickLogin.test.jsx (13) - per-persona credentials,
absent when DEV=false (mutation-checked), absent in mock mode, disabled
while loading. Not covered: that the seeded accounts exist in the DB.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…al (backend)
Owner ruling 2026-09-30: import gets its own per-deal surface and must
never be handed the whole deal or its pricing.
API/authz changes (stated explicitly):
- NEW GET /api/import/deals/{id} — import-only DTO (customer/project,
items, per-factory import-requests + steps, delivery status read-only,
comments). No pricing/cost/margin fields. Roles import+ceo, row-scoped
to import's list scope (TicketRepository.isInImportScope) -> 403.
- GET /api/tickets/{id} now 403s role import (paired with the new page;
NOT independently deployable until the frontend repoints import).
- projectForRole strips item cost fields and non-COMMENTED events for
import (closes the comment-response cost leak).
- comment() is row-scoped to import's scope (no out-of-scope commenting).
- Attachments: import may read/download all four types (PO,
SIGNED_QUOTATION, INVOICE, OTHER) for in-scope deals; upload still 403.
Tests: real-DB integration through the Java service (ImportDealAuthz*,
ImportCommentAndDocumentAuthz*, ImportDealDtoNoPricingFields*), wrong-way-
round, per-guard mutation-checked. api-surface.json +1 (362).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(tickets): route the deal pipeline by entry channel
… (unreviewed) Paused mid-verification under host load. Owner rulings not yet applied (off-route note, channel pre-fills recipient, เสนอแก่ labels). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, TicketDetailPage.test.jsx: both sides appended a describe block whose last test ended inside the conflict; kept both and restored the closer the shared suffix only supplied once. File: 142/142. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner ruling 2026-09-30: import's view is procurement-only. Null amountPayable/amountPaid/amountOutstanding for role import on every TicketSummaryDto it receives — projectForRole (comment response) and the worklist list/listPage paths. Applied after the query, so ordering, paging and counts are unaffected; non-import roles unchanged. Tests: 4 wrong-way-round cases added (import summary money null on comment + both list paths; positive controls keep them), mutation-checked N1-N5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ging merge develop's deal-route-staging rework dropped the quotation_only=FALSE (PIPELINE_ONLY) filter from findSummaries/countSummaries and removed the constant. isInImportScope mirrored the list, so it followed suit: predicate is now id + appendRoleScope(import), matching the merged list exactly. The quotation_only flag is cleared on promote, so a container never reaches import's stage/PCR scope anyway. Full import authz suite + edited existing tests green on the merged tree (ImportDeal* 20+15+4, TicketServiceTest 201, TicketScope 23, TicketIaAuthzMatrix 44, DealDocumentRegister 10, TicketAccessPolicy 15). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayer + mock routes/hrApi (importDeals.get) + queryKeys (importDeal) + mockApi mirror (cost-free, assembled from the mock seed) + serverContract. 7 mock tests; contract + serverContract + api folder green (402). UI slice consumes it next (and must drop the temporary UNREACHABLE_FROM_UI entry). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lfilment tab Frontend-only: relabel S12-S17 to Yang's wording (codes unchanged), add the ถึงไทย X/N โรงงาน rollup chip, and make ImportRequestFactoryCard a compact card with document controls in a collapsed <details>. Backend, schema, mockApi and routes untouched; all data-testids preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ills recipient Owner rulings 2026-09-30 on top of the slice-2 checkpoint: - #1 off-route recipient: allow + inline note. QuotationRecipientField reads the deal's route from the served stage catalog (stageCatalog.routeForChannel) and, when the chosen recipient's quote stage is off it, shows "ผู้รับนี้ไม่อยู่ใน เส้นทางของดีล — ขั้นของดีลจะไม่ขยับ" (13px, info tone, role=status). No block, no modal. New deal: the channel being chosen; picked deal / existing DRAFT: the deal's own entryChannel. - #2 channel pre-fills recipient on the new-deal card (DESIGNER_LED/OWNER_DIRECT/ BUYER_DIRECT -> DESIGNER/OWNER/BUYER), re-filling only until the rep picks one. ?ticket= keeps the stage preselect and falls back to the channel mapping. - Fix found in the browser: submit/approve/reject/revise/reorder/cancel on a direct quotation now also invalidate ['tickets'], so the deal page CTA keyed on liveDirectQuotation is not left stale. confirm-order still calls the promote-to-deal alias (untouched). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c + legacy actions
Frontend slice of the import-own-page feature:
- New /import/deals/:ticketId (ImportDealPage) on GET /api/import/deals/{id}:
header, per-factory ใบขอซื้อ tracker (shared ImportRequestFactoryCard),
delivery read-only, items, comments, attachments. No pricing anywhere.
- Cut import off /tickets/:id (canViewWholeDeal); RequireAccess redirects
import to /import/deals/:id. Worklist/overview/pricing links repointed.
- Legacy pre-per-factory actions (issue/shipping/goods-received) surfaced on
the page for legacy deals only, test-first.
- Sync: every per-factory + comment + legacy mutation invalidates importDeal
alongside storedImportRequests/ticketDetail/ticketActions/list.
- Hallmark polish (existing tokens only): status strip, focal tracker panel.
- api layer wired (importDeals.get + queryKey + mock mirror), serverContract
UNREACHABLE line removed, ui-reachable.json regenerated (302).
- Mock tightened: tickets.get 403s import; comment/attachments row-scoped.
lint 0, npm test green (3603), build clean. Browser-verified: two-way sync
(advance on import page <-> deal tab), cost-free page, import redirected off
the deal page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…page + worklist) Yang's clean per-factory tracker (FactoryProgressBar) now shows the expected arrival window next to the step chip — ETA <from>–<to>, derived from the factory's lead time (issueDate + leadTimeMin/Max, mock and backend both derive expectedArrivalFrom/To). Before issue it falls back to the raw lead-time span. Read-only: sales/CEO/import all see it. Wired the arrival + lead-time fields through ImportRequestFactoryCard's FactoryProgressBar row (ImportFulfilmentPage already passed the full row). New FactoryProgressBar.test.jsx (5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ื้อ; one pricing route per deal Review finding (QuotationEditorPage): "already confirmed → ไปที่ดีล" was decided from ticket.quotationOnly === false, which since #1086 is provenance only and false on every flow-B deal — hiding the confirm on an APPROVED direct quotation whose order was never confirmed. It now keys on the backend's own precondition, the deal's status === 'draft' (confirmOrderFromDirectQuotation 409s otherwise; confirming flips it to quotation_issued). Unknown status (403 / absent) keeps the confirm offered. The action, its dialog and toasts read "ยืนยันคำสั่งซื้อ" (IA §7/§8, D3), matching the deal page CTA. Endpoint call unchanged (promote-to-deal alias). Owner ruling 2026-09-30 — one pricing route per deal, both directions. While a deal holds a LIVE pricing request (any status but CANCELLED/SUPERSEDED), no direct quotation may be started: - quotationMeta.hasLivePricingRequest + LIVE_PRICING_REQUEST_BLOCK_MESSAGE (one definition) and a blocking livePricingRequest checklist entry. - Deal page hides ใบเสนอราคาตรง (and while pricing requests load). - /quotations/new?ticket= shows an inline notice and disables บันทึกร่าง with the reason, reading the deal's requests via the existing pricingRequests.listForTicket. - mockApi dealQuotations.create 409s (after the recipient 400, before N6); test-only seams setPricingRequestStatusForTests / retireLivePricingRequestsForTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one system) Visual layer only, tokens-only, motion-cut, no behavior/testid/logic change. - New shared ImportStatusStrip (fulfilment status · ถึงไทย X/N · ส่งมอบ N%) used on both the deal tab and the import page. - FactoryProgressBar: clean header (name · step chip · ETA · updated), refined 6-step bar (larger current dot + ring, aria-current), aligned action cluster; new presentational props bare/titleExtra. - ImportRequestFactoryCard: single frame, bar owns the header for ISSUED rows (no double name), de-boxed email row, consistent block order. - DealFulfilmentPanel + ImportDealPage: consistent STEP_BOX framing, tracker leads, supporting sections quieted. - Latent fixes surfaced: advance <select> was stretching full-width (added w-auto); StatusBadge tones 'blue'/'green' (no CSS) -> 'info'/'success'. Tests: 3612 green, lint 0, build 0. 375px checked (scrollWidth==clientWidth at 320/375/414). New ImportStatusStrip.test.jsx (4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Measured contrast (all text >=4.5:1, no colour change) + surgical sizing/ alignment fixes only: unified chip heights (step/ETA/status-strip chips to 26px), advance <select> height to match buttons, step-bar label wrap fix (min-w 600), mobile touch targets >=44px (details summary, footer edit, lead-time save), whitespace-nowrap on three deal-tab buttons, error-link now button-shaped. Tokens-only, motion-cut, no behavior/testid/logic change. 3612 tests green, lint 0, build 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… page Import asked to download each factory's ใบขอซื้อ (F-SM-001) from their own page without opening the details expander. ImportRequestFactoryCard gets an optional showInlineDownload prop (default false, so the deal tab is unchanged); ImportDealPage passes it, rendering a visible 'ดาวน์โหลด PDF' per issued factory card (internal copy — import's working reference; both copies remain in the details). Advancing steps is already import's own control. New ImportDealPage test; targeted tests + build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ations/new Slice 2 made the new-quotation page open on the "ดีล" step with เลือกดีลที่มีอยู่ selected; the customer typeahead now lives in the สร้างดีลใหม่ branch, so this spec timed out waiting for it (also after its own reload, which resets the choice). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontend feat(sales): quotation ↔ deal linking slice 2 (frontend) — flow A, recipient, one pricing route, ยืนยันคำสั่งซื้อ
…overage The route-coverage sweep flagged import's /tickets/:id as 'denied but rendered' because RequireAccess redirects import off the whole-deal page to its own /import/deals/:id (intended; pinned in RequireAccess.test.jsx) rather than showing the AccessDenied heading. A denied route that lands on a DIFFERENT path has refused cleanly, so only a same-URL render now counts as a leak. No product-code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(import): import's own per-deal page (view-only deal, per-factory IR tracker) — backend + frontend
…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>
feat(sales): deal form is one page — accordion sections, no ผู้ติดต่อ; "+ เพิ่ม…" first in every dropdown
…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>
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.2.1 to 8.3.4. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.3.4/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.3.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
…nd/vite-8.3.4 chore(deps-dev): bump vite from 8.2.1 to 8.3.4 in /frontend
Bumps [@vitest/mocker](https://github.com/vitest-dev/vitest/tree/HEAD/packages/mocker) to 4.1.12 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together. Updates `@vitest/mocker` from 4.1.11 to 4.1.12 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.12/packages/mocker) Updates `vitest` from 4.1.11 to 4.1.12 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.12/packages/vitest) --- updated-dependencies: - dependency-name: "@vitest/mocker" dependency-version: 4.1.12 dependency-type: indirect - dependency-name: vitest dependency-version: 4.1.12 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
…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>
fix(sales): pricing-request มัดจำ draft accepts the new-engine ACCEPTED quotation of that pricing request
…lock Owner ruling (Ploy, 2026-10-22): sales and sales_manager are exempt from the lock; import reaches only งานนำเข้า + คิวขอราคา (and the queue's /import/deals drill-down); account only การเงิน. Wrong-way-round cases first: every other sales-group path and nav item stays hidden for import/account, and sales still cannot reach payroll, the HR registers, /finance or /fulfilment. The older lock tests that used a sales fixture as 'a locked role' now use import. Red before the implementation: 6 failures, all assertion failures on the missing behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t get a per-role allowlist UI scoping only (the backend enforces every endpoint as before). SELF_SERVICE_EXEMPT_ROLES gains sales and sales_manager, so PATH_GUARDS alone decides what they see. import and account stay locked and pass through RELEASED_PATHS_BY_ROLE (import: /fulfilment, /pricing-requests, /import/deals; account: /finance), applied by canAccessPath and the sidebar filter with the same pathname-prefix rule as SELF_SERVICE_PATHS. The lock flag and its default direction are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keeps image.url in step with the image built from this release head (develop → main, V193–V199.1). The Render dashboard decides what runs; this is the record of what should. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… and price-import hidden under the lock Opus review: the /fulfilment and /leave App tests asserted the access-denied heading was absent before any route had rendered (auth.me still pending), so they passed with the path locked too; they now await a positive marker first. The sidebar tests also assert แคตตาล็อกสินค้า (account) and นำเข้าราคา (import) stay hidden — both pass their own show: condition and are removed only by the lock filter. Mutation-checked: dropping /fulfilment from import's allowlist turns exactly these two tests red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Backend image ready — deploy this BEFORE merging.
Opus review of the lock-release diff: no blocker. Two test gaps fixed in Deploy order: Render → gl-r-erp → Settings → Build → Existing Image → |
…nd/multi-456d42d8d4 chore(deps): bump @vitest/mocker and vitest in /frontend
What this is
Owner decision (Ploy, 2026-10-22): full develop → main release of the sales/CRM stack, with the production self-service lock opened per role.
mainwas fully contained indevelop(0 commits ahead), so this is develop as of51b5e906(after #1166) plus three release commits.Per-role lock release (owner ruling, same day):
sales,sales_manager: exempt from the lock; they see exactly whatPATH_GUARDSalready grants (the sales stack, never payroll / HR registers / การเงิน / งานนำเข้า).import: stays locked, may reach ONLY/fulfilment(งานนำเข้า),/pricing-requests(คิวขอราคา) and the queue's per-deal/import/deals/:id.account: stays locked, may reach ONLY/finance(การเงิน).VITE_SELF_SERVICE_ONLYand its locked-by-default direction are untouched. This is UI scoping; the backend enforces every endpoint as before.Merging this PR deploys the frontend to Vercel immediately. The backend only changes when the Render service's image is switched. The frontend here calls endpoints and columns the current prod image (
v2026-10-14) does not have, so:ghcr.io/waritctd/glr-hr-backend:v2026-10-22from this branch head (scripts/build-push-backend-image.sh v2026-10-22, amd64, licensed fonts baked in).gl-r-erp→ Settings → Build → Existing Image → setv2026-10-22→ deploy. Flyway applies V193, V194, V197, V198, V199, V199.1 at boot. Prodhr.flyway_schema_historywas checked read-only before this PR: contiguous through 192.5, all success, so every pending version is ABOVE the applied max (no silent out-of-order skip). No newdb/migration-demofiles. Everyone is logged out by the deploy (known).…192.5,193,194,197,198,199,199.1and a fresh quotation PDF embeds AngsanaNew/Tahoma.Rollback: switch the Render image back to
v2026-10-14(migrations are additive; see each file) and revert the merge on main.Files changed (release commits only; the develop content is 287 commits, 469 files — see the develop PR history)
frontend/src/app/permissions.js—SELF_SERVICE_EXEMPT_ROLES+ sales roles;RELEASED_PATHS_BY_ROLE+isReleasedPath;canAccessPathhonours it.frontend/src/components/layout/AppShell.jsx— sidebar filter honoursisReleasedPath.frontend/src/app/permissions.test.js,components/layout/AppShell.test.jsx,App.test.jsx— tests (below).render.yaml—image.urlpinned tov2026-10-22.Commands run
npx vitest run src/app/permissions.test.js src/components/layout/AppShell.test.jsx src/App.test.jsx→ 125/125npx eslinton the changed files → cleannpm run buildon the release head (see checks)tdyzcqzxmhtxpbouewud).Authz evidence
No backend authz change. The lock is frontend scoping; every endpoint keeps its controller role checks. Frontend "cannot reach" cases are pinned wrong-way-round (import → /tickets, /finance, /catalog, /quotations, /commissions; account → /tickets, /fulfilment, /pricing-requests, /quotations; sales → /payroll, /hr, /employees, /finance, /fulfilment).
Known risks
SELF_SERVICE_ONLY=trueis covered byApp.test.jsxonly.auditCI check is red on develop itself (@vitest/mockeradvisory, dependabot chore(deps): bump @vitest/mocker and vitest in /frontend #1167 fixes it) — unrelated to this change.Test coverage
permissions.test.js—releases the sales stack to sales and sales_manager, nothing beyond their guards;releases import only งานนำเข้า and คิวขอราคา;releases account only การเงิน;matches on the pathname…(released paths with query strings);locks every role except…revised (sales roles now exempt); activity-log admin tests useimportas the locked fixture; quotation-release leak test keeps the qc grantee.AppShell.test.jsx— sales gets รายการดีล/ใบเสนอราคา/แคตตาล็อก back and no งานการเงิน/งานนำเข้า; import shows คิวขอราคา + งานนำเข้า and nothing else from the sales group; account shows งานการเงิน only.App.test.jsx— import lands on EmployeeSelfService, is refused /tickets and /quotations (a page its role permits), reaches /fulfilment; sales lands on SalesOverview.🤖 Generated with Claude Code