Files
clienthub/.planning/phases/03-service-catalog-quote-builder/03-01-PLAN.md
T
Simone Cavalli a4942d7684 docs(03): plan Phase 3 — Service Catalog & Quote Builder (4 plans, 2 waves)
Wave 1: schema push (service_id nullable + custom_label).
Wave 2 (parallel): catalog CRUD page + quote builder tab.
Wave 3: E2E human verification checkpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:23:15 +02:00

8.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03 01 execute 1
src/db/schema.ts
true
CAT-01
CAT-02
ADMIN-03
truths artifacts key_links
quote_items.service_id is nullable in the database (free-form items can be inserted without a catalog reference)
quote_items.custom_label column exists in the database (free-form label storage)
TypeScript QuoteItem type reflects both changes (no compile errors when service_id is null or custom_label is set)
drizzle-kit push completes without errors against the live Neon database
path provides contains
src/db/schema.ts Updated quote_items table definition with nullable service_id and custom_label column custom_label: text("custom_label")
from to via pattern
src/db/schema.ts quote_items.service_id Neon Postgres quote_items table drizzle-kit push service_id.*references.*service_catalog
Make the two schema changes required for free-form quote items — make `service_id` nullable and add `custom_label text` — then push the changes to the live Neon database.

Purpose: All subsequent plans (Wave 2) reference custom_label and insert rows with service_id = null. Without this push, the DB will reject those inserts with a column-not-found or NOT NULL constraint error. Output: Updated src/db/schema.ts and a successful drizzle-kit push confirmation.

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

@/Users/simonecavalli/IAMCAVALLI/.planning/PROJECT.md @/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md ```typescript export const quote_items = pgTable("quote_items", { id: text("id").primaryKey().$defaultFn(() => nanoid()), client_id: text("client_id") .notNull() .references(() => clients.id, { onDelete: "cascade" }), service_id: text("service_id") .notNull() // <-- REMOVE .notNull() .references(() => service_catalog.id, { onDelete: "restrict" }), quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(), unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(), subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(), // custom_label missing — ADD after subtotal }); ```
export const quote_items = pgTable("quote_items", {
  id: text("id").primaryKey().$defaultFn(() => nanoid()),
  client_id: text("client_id")
    .notNull()
    .references(() => clients.id, { onDelete: "cascade" }),
  service_id: text("service_id")
    .references(() => service_catalog.id, { onDelete: "restrict" }),  // nullable — no .notNull()
  quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(),
  unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(),
  subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(),
  custom_label: text("custom_label"),                                  // new field
});
export const quoteItemsRelations = relations(quote_items, ({ one }) => ({
  client: one(clients, { fields: [quote_items.client_id], references: [clients.id] }),
  service: one(service_catalog, {
    fields: [quote_items.service_id],
    references: [service_catalog.id],
  }),
}));
export type QuoteItem = typeof quote_items.$inferSelect;
// QuoteItem.service_id will be: string | null
// QuoteItem.custom_label will be: string | null
Task 1: Update quote_items schema — make service_id nullable and add custom_label - /Users/simonecavalli/IAMCAVALLI/src/db/schema.ts (full file — understand current definition before editing) src/db/schema.ts Edit `src/db/schema.ts` — two targeted changes to the `quote_items` table definition (lines 159-172):

Change 1 — Remove .notNull() from service_id (per D-03 from CONTEXT.md): Before:

  service_id: text("service_id")
    .notNull()
    .references(() => service_catalog.id, { onDelete: "restrict" }),

After:

  service_id: text("service_id")
    .references(() => service_catalog.id, { onDelete: "restrict" }),

Change 2 — Add custom_label field after the subtotal line:

  custom_label: text("custom_label"),

No other changes to the file. The quoteItemsRelations block does NOT need to change. After the edit, run npx tsc --noEmit to confirm zero TypeScript errors before pushing. cd /Users/simonecavalli/IAMCAVALLI && grep -v '^//' src/db/schema.ts | grep -c 'custom_label: text("custom_label")' Expected: 1

<automated>cd /Users/simonecavalli/IAMCAVALLI && grep -A3 'service_id: text("service_id")' src/db/schema.ts | grep -c 'notNull'</automated>
Expected: 0 (notNull must be gone from service_id)

<automated>cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20</automated>
Expected: no output (zero errors)
`src/db/schema.ts` compiles with zero TypeScript errors. `service_id` has no `.notNull()`. `custom_label: text("custom_label")` is present in the quote_items table definition. Task 2: [BLOCKING] Push schema changes to Neon database - /Users/simonecavalli/IAMCAVALLI/.env.local (verify DATABASE_URL is set before running push) - /Users/simonecavalli/IAMCAVALLI/drizzle.config.ts (verify push config points to correct schema) — (no source files modified; runs drizzle-kit against live DB) Run drizzle-kit push with the .env.local DATABASE_URL loaded:
cd /Users/simonecavalli/IAMCAVALLI
set -a && source .env.local && set +a && npx drizzle-kit push

When prompted to confirm schema changes, accept all changes. The push will:

  1. DROP NOT NULL constraint from quote_items.service_id
  2. ADD COLUMN custom_label text to quote_items

If the push fails with "column already exists" for custom_label, the column was already added in a prior run — this is safe to ignore. Verify the column exists by checking the push output or running a quick query.

Do NOT skip this task. Wave 2 plans cannot execute correctly without the DB columns existing. cd /Users/simonecavalli/IAMCAVALLI && set -a && source .env.local && set +a && npx drizzle-kit push 2>&1 | tail -5 Expected: Output contains "No changes" or "Changes applied" — either confirms the schema is in sync. drizzle-kit push exits without error. The live Neon DB has quote_items.service_id as nullable and quote_items.custom_label text column present.

<threat_model>

Trust Boundaries

Boundary Description
Schema file → Neon DB drizzle-kit push executes DDL against the live database; misconfigured DATABASE_URL would push to wrong environment

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-03-01-01 Tampering drizzle-kit push mitigate Always load DATABASE_URL from .env.local (not hardcoded); verify .env.local exists before running push
T-03-01-02 Denial of Service Neon DB DDL accept Schema changes are additive (ADD COLUMN, DROP NOT NULL) — no data loss risk; onDelete: "restrict" prevents orphaned quote_items
</threat_model>
After both tasks complete: 1. `grep 'custom_label' src/db/schema.ts` returns the field definition 2. `grep -A3 'service_id: text' src/db/schema.ts` shows no `.notNull()` on service_id 3. `npx tsc --noEmit` exits 0 4. `npx drizzle-kit push` reports "No changes" (schema is in sync with DB)

<success_criteria>

  • src/db/schema.ts has nullable service_id and custom_label: text("custom_label") in quote_items
  • TypeScript compiles with zero errors
  • drizzle-kit push confirms schema is synced to Neon DB
  • Wave 2 plans can safely reference custom_label and insert rows with service_id = null </success_criteria>
After completion, create `.planning/phases/03-service-catalog-quote-builder/03-01-SUMMARY.md`