---
phase: "03"
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - src/db/schema.ts
autonomous: true
requirements:
  - CAT-01
  - CAT-02
  - ADMIN-03

must_haves:
  truths:
    - "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"
  artifacts:
    - path: "src/db/schema.ts"
      provides: "Updated quote_items table definition with nullable service_id and custom_label column"
      contains: "custom_label: text(\"custom_label\")"
  key_links:
    - from: "src/db/schema.ts quote_items.service_id"
      to: "Neon Postgres quote_items table"
      via: "drizzle-kit push"
      pattern: "service_id.*references.*service_catalog"
---

<objective>
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.
</objective>

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

<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/PROJECT.md
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md

<interfaces>
<!-- Current quote_items definition in src/db/schema.ts lines 159-172 -->
```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
});
```

<!-- After changes, the definition must be: -->
```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")
    .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
});
```

<!-- quoteItemsRelations also references service_id — the relation stays but the field is now optional: -->
```typescript
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],
  }),
}));
```
<!-- The relation definition does NOT need to change — Drizzle handles nullable FK relations correctly. -->

<!-- Inferred TypeScript type after change (auto-generated by Drizzle): -->
```typescript
export type QuoteItem = typeof quote_items.$inferSelect;
// QuoteItem.service_id will be: string | null
// QuoteItem.custom_label will be: string | null
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update quote_items schema — make service_id nullable and add custom_label</name>
  <read_first>
    - /Users/simonecavalli/IAMCAVALLI/src/db/schema.ts (full file — understand current definition before editing)
  </read_first>
  <files>src/db/schema.ts</files>
  <action>
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:
```typescript
  service_id: text("service_id")
    .notNull()
    .references(() => service_catalog.id, { onDelete: "restrict" }),
```
After:
```typescript
  service_id: text("service_id")
    .references(() => service_catalog.id, { onDelete: "restrict" }),
```

**Change 2 — Add custom_label field after the `subtotal` line:**
```typescript
  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.
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && grep -v '^//' src/db/schema.ts | grep -c 'custom_label: text("custom_label")'</automated>
    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)
  </verify>
  <done>
    `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.
  </done>
</task>

<task type="auto">
  <name>Task 2: [BLOCKING] Push schema changes to Neon database</name>
  <read_first>
    - /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)
  </read_first>
  <files>— (no source files modified; runs drizzle-kit against live DB)</files>
  <action>
Run drizzle-kit push with the .env.local DATABASE_URL loaded:

```bash
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.
  </action>
  <verify>
    <automated>cd /Users/simonecavalli/IAMCAVALLI && set -a && source .env.local && set +a && npx drizzle-kit push 2>&1 | tail -5</automated>
    Expected: Output contains "No changes" or "Changes applied" — either confirms the schema is in sync.
  </verify>
  <done>
    `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.
  </done>
</task>

</tasks>

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

<verification>
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)
</verification>

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

<output>
After completion, create `.planning/phases/03-service-catalog-quote-builder/03-01-SUMMARY.md`
</output>