a4942d7684
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>
216 lines
8.8 KiB
Markdown
216 lines
8.8 KiB
Markdown
---
|
|
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> |