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>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user