Files
clienthub/.planning/phases/09-quote-builder-client-acceptance/09-02-PLAN.md
T
simone a98fe7a9f3 docs(09): phase 9 planning complete — 3 plans in 2 waves (schema + admin builder + public quote page)
Phase 9 breaks into two parts:
- Wave 0: Schema foundation (Phase 8) — offer_phases, quotes, quote_items tables + type system
- Wave 1: Admin Quote Builder (/admin/quotes/new) — form, server action, price validation
- Wave 2: Public Quote Page (/quote/[token]) — multistep form, acceptance, rate limiting

Plans:
- 09-01: Database schema (offer_phases, quotes, quote_items) + Drizzle migration + query layer types
- 09-02: Admin builder UI (two-column form, client/offer selection, live preview) + createQuote action
- 09-03: Public quote page (token-gated route, middleware rate limit 3/min, multistep wizard, acceptQuote action)

Key constraints per CLAUDE.md:
- quote_items NEVER exposed to client API (public query layer enforces PublicQuoteView type)
- Server-side price recalculation prevents tampering (client-side total validated against item sum)
- Immutability enforced: accepted_at timestamp immutable once set (DB constraint + app check)
- Token: nanoid 21-char (~122-bit entropy), unique, rate-limited at middleware

Estimated execution: 4-5 days. Wave 0 can run in parallel with Wave 1 (no blocking).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 07:06:47 +02:00

11 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
09 02 execute 1
09-01
src/app/admin/quotes/new/page.tsx
src/app/admin/quotes/new/actions.ts
src/components/admin/quotes/QuoteBuilderForm.tsx
src/components/admin/quotes/OfferSelector.tsx
src/components/admin/quotes/PriceOverrideInput.tsx
src/components/admin/quotes/QuotePreview.tsx
src/lib/quote-actions.ts
true
QUOTE-01
truths artifacts key_links
Admin can select a client and offer on /admin/quotes/new
Two-column form shows inputs on left and live preview on right
Form submit calls createQuote server action with validated data
Server action recalculates total and rejects if mismatch
On success, public link /quote/[token] is displayed for copy
path provides exports
src/lib/quote-actions.ts createQuote server action with price validation
createQuote
getOfferWithPhases
path provides min_lines
src/components/admin/quotes/QuoteBuilderForm.tsx Form state management and submission 80
path provides contains
src/app/admin/quotes/new/page.tsx Admin quote builder page at /admin/quotes/new QuoteBuilderForm
from to via pattern
src/app/admin/quotes/new/page.tsx src/components/admin/quotes/QuoteBuilderForm.tsx import and render import.*QuoteBuilderForm
from to via pattern
src/components/admin/quotes/QuoteBuilderForm.tsx src/lib/quote-actions.ts server action call await createQuote
from to via pattern
src/lib/quote-actions.ts src/db/schema.ts quote insert and validation db.insert.*quotes
Implement the admin Quote Builder UI (`/admin/quotes/new`) with two-column form (left: inputs, right: live preview) and server actions for creating quotes. Admin selects client, offer, and overrides pricing; saves generates nanoid token and creates public link.

Purpose: Enable admins to compose quotes in 2-3 clicks, validate pricing server-side, and generate shareable public links for clients.

Output:

  • /admin/quotes/new page with form and live preview
  • Server action createQuote() validates and saves quote + quote_items to DB
  • Generated public link /quote/[token] shareable via email (Phase 12)
  • Quote saved as draft; state can be updated to "sent" when link shared

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/09-quote-builder-client-acceptance/09-RESEARCH.md @.planning/phases/09-quote-builder-client-acceptance/09-01-SUMMARY.md

Key Interfaces (from Phase 8 schema)

Quote Header:

type Quote = {
  id: string;
  client_id: string | null;
  offer_micro_id: string;
  token: string;
  state: "draft" | "sent" | "viewed" | "accepted" | "rejected";
  accepted_total: string;
  accepted_at: Date | null;
  client_email: string | null;
  client_notes: string | null;
  created_at: Date;
  updated_at: Date;
};

Quote Item (line):

type QuoteItem = {
  id: string;
  quote_id: string;
  offer_phase_id: string;
  service_id: string | null;
  quantity: string;
  unit_price: string;
  subtotal: string;
  custom_label: string | null;
};

OfferMicro (from Phase 5):

type OfferMicro = {
  id: string;
  macro_id: string;
  internal_name: string;
  public_name: string;
  transformation_promise: string | null;
  duration_months: number;
};

OfferPhase (from Phase 8):

type OfferPhase = {
  id: string;
  micro_id: string;
  title: string;
  description: string | null;
  sort_order: number;
  created_at: Date;
};
Task 1: Create server action for quote creation (createQuote) src/lib/quote-actions.ts Create a new server action file with the core quote creation logic. Key responsibilities:
  • Validate input via Zod schema (client, offer, items array)
  • Verify client and offer exist in database
  • Recalculate total server-side by summing all quote_items subtotals (quantity × unit_price)
  • Reject if client-submitted total doesn't match calculated total (0.01 tolerance for rounding)
  • Generate nanoid(21) token — 21 characters, ~122-bit entropy
  • Create atomic transaction: insert quote header, then insert quote_items
  • Return success/error + public link /quote/[token]

Security per CLAUDE.md:

  • Never trust client-side pricing calculation
  • Server-side recalculation is THE security boundary
  • If attacker modifies total before submit, server action rejects it
  • Tokens are collision-free and unguessable via nanoid

Error handling: Localize error messages to Italian (client not found, offer not found, total mismatch).

Use existing patterns from quote-validators.ts where applicable; reference createQuoteSchema and quoteItemSchema for Zod integration. npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Actions compile" createQuote action handles validation, server-side total recalculation, and atomic quote+items creation. Error messages localized to Italian. Returns token and public link.

Task 2: Create QuoteBuilderForm component (two-column layout) src/components/admin/quotes/QuoteBuilderForm.tsx, src/components/admin/quotes/OfferSelector.tsx, src/components/admin/quotes/PriceOverrideInput.tsx, src/components/admin/quotes/QuotePreview.tsx Create four new component files that make up the two-column form UI:

src/components/admin/quotes/QuoteBuilderForm.tsx — Parent component managing form state:

  • Uses React Hook Form + Zod for validation
  • Manages three sections: client select, offer select, price overrides
  • Left column: form inputs (client dropdown, offer dropdown, price fields)
  • Right column: live preview (selected offer summary + calculated total)
  • On form submit: call createQuote server action
  • On success: display public link in a copiable text box + "Copy Link" button
  • Handles form errors (display via Form/FormMessage from shadcn/ui)

src/components/admin/quotes/OfferSelector.tsx — Dropdown to select offer_micro:

  • Fetches all offer_macros with their offer_micros (via server component query)
  • Displays as grouped dropdown: "Entry Offer" > [Entry A, Entry B, Entry C]
  • On selection change: trigger preview update in parent

src/components/admin/quotes/PriceOverrideInput.tsx — Input for final accepted_total:

  • Single numeric input field
  • On blur: parent calculates preview total and compares to this input
  • Visual feedback: green checkmark if matches, red X if mismatch (but allow save — server validates)

src/components/admin/quotes/QuotePreview.tsx — Right column preview:

  • Displays selected offer name, public name, transformation promise
  • Lists phases (Discovery, Strategy, Execution)
  • For each phase, lists the services that will be included
  • Shows live calculated total as admin enters price overrides
  • Displays estimated revenue and duration

All components use shadcn/ui (Form, Input, Select, Button, Card) for consistency with existing admin UI. Use tailwind grid-cols-2 for two-column layout in QuoteBuilderForm. npm run build 2>&1 | grep -c "error TS" | xargs test 0 -eq && echo "✓ Components compile" Four component files created. Two-column form renders with client/offer selection, price input, and live preview. Form submission calls createQuote action.

Task 3: Create /admin/quotes/new page and wire form src/app/admin/quotes/new/page.tsx, src/app/admin/quotes/new/actions.ts Create the page file and optional actions helper:

src/app/admin/quotes/new/page.tsx — Page component:

  • Server component that imports QuoteBuilderForm
  • Renders page title "Crea Preventivo"
  • Renders QuoteBuilderForm centered in a Card
  • Wraps in AdminLayout (existing pattern from /admin/clients, /admin/projects)
  • Page has Auth.js guard via middleware (existing /admin/* protection)

src/app/admin/quotes/new/actions.ts — Optional re-export of quote actions:

  • Can be empty or re-export createQuote from lib/quote-actions.ts
  • Keeps page-level action imports clean if needed by form

The form will be a client component ("use client") managing form state with React Hook Form. curl -s http://localhost:3000/admin/quotes/new 2>&1 | grep -q "Crea Preventivo" && echo "✓ Page loads" Page and form wired. Admin can navigate to /admin/quotes/new and see the two-column builder form.

<threat_model>

Trust Boundaries

Boundary Description
Admin → Form Input Form data can be manipulated before submit
Client Browser → Server Pricing total can be modified in network inspector before sending
Server → Database Quote must be immutable once accepted

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-09-01 Tampering Price total modified before submit mitigate Server-side recalculation in createQuote action; reject if mismatch between item sum and submitted total
T-09-02 Tampering Quote item quantity/unit_price tampered mitigate Zod schema validates numeric fields; server recalculates before saving
T-09-03 Elevation Non-admin access to /admin/quotes/new mitigate Auth.js middleware guards /admin/* routes (existing infrastructure)
T-09-04 Information Disclosure offer_micro details over-exposed accept Offer details are internal-facing; not visible to public (per Phase 9 Task 5)

</threat_model>

After Phase 9 Plan 2 execution:
  1. Server action compiles: npm run build passes
  2. Components compile: All four components render without errors
  3. Page loads: /admin/quotes/new accessible to authenticated admin
  4. Form submits: Clicking "Salva Preventivo" calls createQuote
  5. Link generated: On success, public link displayed in copyable box
  6. Server validation works: Manually modifying total in browser before submit should be rejected
  7. Database: Quote + quote_items inserted to DB with correct structure

<success_criteria>

  • /admin/quotes/new page accessible and renders form
  • Two-column layout (left: inputs, right: preview) visually distinct
  • Form validates client and offer selection (required fields)
  • Form submit calls createQuote server action
  • Server action validates data and recalculates total server-side
  • On success: public /quote/[token] link displayed and copyable
  • Quote saved to DB with state="draft", token unique, accepted_total immutable
  • On error: user-friendly error message displayed (Italian localization) </success_criteria>
After execution, create `.planning/phases/09-quote-builder-client-acceptance/09-02-SUMMARY.md`