Commit graph

4 commits

Author SHA1 Message Date
9632c1048c
fix: resolve Phase 6c gate findings (2 high, 5 low) across api, web, lambda, mobile
Gemini scanner sweep (token-bypass), GPT-4.1 cross-review, and the
6-detector /sh-security-review fan-out ran against 6acfdab..HEAD; every
confirmed finding fixed:

HIGH (deployment blockers, logic detector):
- CONC-L1: suggestions lambda's bulk line-item PUT sent no
  proposalVersion — every AI suggestion job would 422 and be silently
  swallowed. Now fetches the proposal's rowVersion, echoes it, and
  retries once with a fresh token on 409. Pytest updated (38 green).
- CONC-L2: mobile admin surface (update/approve/send/revise, bulk line
  items) sent no tokens — the entire mobile admin workflow would 422.
  Tokens threaded through mobile api layer + workspace/line-item
  screens with 409 refetch handling. tsc clean.

MEDIUM-adjacent (scanner):
- VendorProposalsController: the VendorTotalCost write on Proposal now
  bumps Version (was a silent lost-update path bypassing the guard).
- FilesController: GeneratePDF audit staged into the same SaveChanges.

LOW (detectors):
- 409 envelope is schema-validated client-side
  (proposalConcurrencyConflictSchema.safeParse) and id-checked before
  seeding the react-query cache; malformed state degrades to
  invalidation (INJ-409-01/WEB-CONC-L1).
- ProposalConcurrencyException.CurrentState typed ProposalResponse?
  so an EF entity can never serialize into the 409 body (SC-1).
- Guard caller contract documented + GuardedEndpointAuthorizationTests
  reflection tripwire: guard-reaching endpoints must stay admin-gated
  (AUTHZ-CG-01).
- Pre-check currentState now loads display navigations so both 409
  paths return the same shape (CONC-L3).
- Save chain's trailing getById failure no longer misreports a
  committed save; falls back to invalidation (CONC-L4).

Also caught during fix verification: the handler's manual currentState
serialization lacked JsonStringEnumConverter — enums would serialize
as numbers, client schema validation would reject every guarded 409,
and the state would always be discarded. Now matches the MVC pipeline
and is pinned by a wire test.

193 xUnit / 70 vitest / 38 pytest green; mobile + shared tsc clean;
Playwright smoke 2/2.
2026-07-13 21:20:12 -04:00
d6404edefc
feat(contracts+web): thread concurrency tokens and 409 conflict handling through the domain layer
Phase 6b of the SHOC-alignment plan — client side of the optimistic-concurrency
wire contract shipped in 6a.

shared/api-contracts (additive):
- rowVersion: string on ProposalListItem, ProposalDetail, and LineItem
- proposalVersion?: string on UpdateProposalRequest and
  BulkUpdateLineItemsRequest; new ProposalVersionRequest
- ConcurrencyConflict<T> { message, currentState } — the SHOC ADR 0004
  409 envelope (guarded path; the unguarded fallback carries no state)
- matching zod schemas, all kept under the satisfies z.ZodType<T> coupling

web:
- lib/api/errors.ts: ConflictError carrying the server's reloaded
  currentState; client.ts interceptor throws it on 409 (WEB-M2 401
  handling untouched)
- guarded mutations read the token from the cached proposal detail at
  mutate time; the save flow chains rotated tokens (PUT response token
  into the bulk replace) and ends with a detail refetch so
  approve-after-save never sends a stale version
- 409 recovery in the admin use-cases: write currentState into the
  detail cache, invalidateProposalViews() (stale-queue invariant holds
  on the failure path too), and toast the conflict instead of the
  generic failure message
- e2e smoke mock payloads carry rowVersion; vitest coverage for the
  interceptor ConflictError paths, token threading/rotation, and 409
  cache recovery

Verified: shared typecheck, web tsc/vitest (69)/build/prettier/Playwright
smoke, mobile tsc (create-only, no changes needed).
2026-07-13 21:01:46 -04:00
1f55a59445
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
  invalidateProposalViews (detail + line items + lists + stats + admin
  dashboard) — approving no longer leaves a stale queue for the
  5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
  rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
  path normalizes empty->null to match the update path — customer
  emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
  '12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
  autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
  before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
  ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
  contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
  ['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
  TablePagination out-of-range flash on page change

Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
  useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
  toUpdateLineItemEntry); tests moved to the live save path
  (useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
  copies (one leaked its timer on unmount, two hardcoded 300ms);
  DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
  onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
  deduplicated from 3 copies to the tsconfig paths mapping

Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
e6e2c60c2f
feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).

Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
2026-07-13 17:58:19 -04:00