219 lines
8.4 KiB
Markdown
219 lines
8.4 KiB
Markdown
|
|
---
|
||
|
|
phase: 04-pdf-ingest
|
||
|
|
plan: 01
|
||
|
|
type: execute
|
||
|
|
wave: 1
|
||
|
|
depends_on: []
|
||
|
|
files_modified:
|
||
|
|
- teressa-copeland-homes/src/lib/db/schema.ts
|
||
|
|
- teressa-copeland-homes/src/lib/db/index.ts
|
||
|
|
- teressa-copeland-homes/scripts/seed-forms.ts
|
||
|
|
- teressa-copeland-homes/seeds/forms/.gitkeep
|
||
|
|
- teressa-copeland-homes/package.json
|
||
|
|
autonomous: true
|
||
|
|
requirements:
|
||
|
|
- DOC-01
|
||
|
|
- DOC-02
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "form_templates table exists in the database with id, name, filename, createdAt, updatedAt columns"
|
||
|
|
- "documents table has filePath and formTemplateId columns added"
|
||
|
|
- "Running npm run seed:forms reads seeds/forms/ directory and upserts rows into form_templates"
|
||
|
|
- "seeds/forms/ directory exists and is tracked in git (via .gitkeep)"
|
||
|
|
artifacts:
|
||
|
|
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
|
||
|
|
provides: "formTemplates table definition and updated documents table"
|
||
|
|
contains: "formTemplates"
|
||
|
|
- path: "teressa-copeland-homes/scripts/seed-forms.ts"
|
||
|
|
provides: "CLI seed script for populating form_templates from seeds/forms/"
|
||
|
|
exports: []
|
||
|
|
- path: "teressa-copeland-homes/seeds/forms/.gitkeep"
|
||
|
|
provides: "Tracked placeholder for manually-downloaded PDF seed files"
|
||
|
|
key_links:
|
||
|
|
- from: "scripts/seed-forms.ts"
|
||
|
|
to: "seeds/forms/"
|
||
|
|
via: "node:fs/promises readdir"
|
||
|
|
pattern: "readdir.*seeds/forms"
|
||
|
|
- from: "scripts/seed-forms.ts"
|
||
|
|
to: "src/lib/db/schema.ts formTemplates"
|
||
|
|
via: "drizzle insert onConflictDoUpdate"
|
||
|
|
pattern: "onConflictDoUpdate"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Extend the database schema with a form_templates table and add missing columns to the documents table. Create the seed directory structure and seed script that fulfills the monthly-sync requirement (DOC-02) via manual re-run.
|
||
|
|
|
||
|
|
Purpose: Every subsequent Phase 4 plan depends on this schema and the seed mechanism. Getting the data layer right first prevents downstream rework.
|
||
|
|
Output: Updated schema.ts, new Drizzle migration applied, seed script at npm run seed:forms, seeds/forms/ directory tracked.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||
|
|
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.planning/PROJECT.md
|
||
|
|
@.planning/ROADMAP.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@.planning/phases/04-pdf-ingest/04-CONTEXT.md
|
||
|
|
@.planning/phases/04-pdf-ingest/04-RESEARCH.md
|
||
|
|
|
||
|
|
<interfaces>
|
||
|
|
<!-- Existing schema (teressa-copeland-homes/src/lib/db/schema.ts) -->
|
||
|
|
```typescript
|
||
|
|
import { pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||
|
|
|
||
|
|
export const documentStatusEnum = pgEnum("document_status", ["Draft","Sent","Viewed","Signed"]);
|
||
|
|
|
||
|
|
export const clients = pgTable("clients", {
|
||
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
email: text("email").notNull(),
|
||
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const documents = pgTable("documents", {
|
||
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
|
||
|
|
status: documentStatusEnum("status").notNull().default("Draft"),
|
||
|
|
sentAt: timestamp("sent_at"),
|
||
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
|
|
});
|
||
|
|
```
|
||
|
|
</interfaces>
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 1: Add formTemplates table and extend documents table in schema.ts</name>
|
||
|
|
<files>
|
||
|
|
teressa-copeland-homes/src/lib/db/schema.ts
|
||
|
|
teressa-copeland-homes/seeds/forms/.gitkeep
|
||
|
|
</files>
|
||
|
|
<action>
|
||
|
|
Edit teressa-copeland-homes/src/lib/db/schema.ts:
|
||
|
|
|
||
|
|
1. Add `integer` to the drizzle-orm/pg-core import.
|
||
|
|
2. Add the `formTemplates` table ABOVE the `documents` table (must precede its reference):
|
||
|
|
```typescript
|
||
|
|
export const formTemplates = pgTable("form_templates", {
|
||
|
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
|
|
name: text("name").notNull(),
|
||
|
|
filename: text("filename").notNull().unique(),
|
||
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
|
|
});
|
||
|
|
```
|
||
|
|
3. Add two columns to the `documents` table:
|
||
|
|
```typescript
|
||
|
|
formTemplateId: text("form_template_id").references(() => formTemplates.id), // nullable: custom uploads have no template
|
||
|
|
filePath: text("file_path"), // relative path within uploads/ e.g. "clients/{clientId}/{uuid}.pdf"; nullable for legacy rows
|
||
|
|
```
|
||
|
|
NOTE: Use `text` for IDs (not `integer`) — the existing schema uses `text` PKs with crypto.randomUUID() throughout. The formTemplates.id is also text. Keep consistent.
|
||
|
|
|
||
|
|
Create `teressa-copeland-homes/seeds/forms/.gitkeep` (empty file) so the directory is tracked.
|
||
|
|
|
||
|
|
Then run the migration:
|
||
|
|
```bash
|
||
|
|
cd teressa-copeland-homes && npx drizzle-kit generate && npx drizzle-kit migrate
|
||
|
|
```
|
||
|
|
Confirm migration applied without errors.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
```bash
|
||
|
|
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx drizzle-kit migrate 2>&1 | tail -5
|
||
|
|
```
|
||
|
|
Expected: "No migrations to run" or "Applied N migrations" (no errors).
|
||
|
|
</verify>
|
||
|
|
<done>form_templates table and new documents columns exist in the database. seeds/forms/ directory tracked in git.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 2: Create seed script and wire npm run seed:forms</name>
|
||
|
|
<files>
|
||
|
|
teressa-copeland-homes/scripts/seed-forms.ts
|
||
|
|
teressa-copeland-homes/package.json
|
||
|
|
</files>
|
||
|
|
<action>
|
||
|
|
Create `teressa-copeland-homes/scripts/seed-forms.ts`:
|
||
|
|
```typescript
|
||
|
|
import 'dotenv/config';
|
||
|
|
import { readdir } from 'node:fs/promises';
|
||
|
|
import path from 'node:path';
|
||
|
|
import { eq } from 'drizzle-orm';
|
||
|
|
import { db } from '@/lib/db';
|
||
|
|
import { formTemplates } from '@/lib/db/schema';
|
||
|
|
|
||
|
|
const SEEDS_DIR = path.join(process.cwd(), 'seeds', 'forms');
|
||
|
|
|
||
|
|
async function seedForms() {
|
||
|
|
const files = await readdir(SEEDS_DIR);
|
||
|
|
const pdfs = files.filter(f => f.endsWith('.pdf'));
|
||
|
|
|
||
|
|
if (pdfs.length === 0) {
|
||
|
|
console.log('No PDF files found in seeds/forms/. Add PDFs downloaded from the SkySlope portal and re-run.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let seeded = 0;
|
||
|
|
for (const filename of pdfs) {
|
||
|
|
const name = filename
|
||
|
|
.replace(/\.pdf$/i, '')
|
||
|
|
.replace(/[-_]/g, ' ')
|
||
|
|
.replace(/\b\w/g, c => c.toUpperCase());
|
||
|
|
|
||
|
|
await db.insert(formTemplates)
|
||
|
|
.values({ name, filename })
|
||
|
|
.onConflictDoUpdate({
|
||
|
|
target: formTemplates.filename,
|
||
|
|
set: { name, updatedAt: new Date() },
|
||
|
|
});
|
||
|
|
seeded++;
|
||
|
|
}
|
||
|
|
console.log(`Seeded ${seeded} forms into form_templates.`);
|
||
|
|
process.exit(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
seedForms().catch(err => { console.error(err); process.exit(1); });
|
||
|
|
```
|
||
|
|
|
||
|
|
NOTE: The project uses `.env.local` not `.env`. Check how the existing seed-clients.ts (Phase 3) handles env loading — mirror that pattern exactly (likely `DOTENV_CONFIG_PATH=.env.local` prefix per the STATE.md decision). If the project uses `dotenv/config` with that env var, replace the top import accordingly.
|
||
|
|
|
||
|
|
Add to `teressa-copeland-homes/package.json` scripts:
|
||
|
|
```json
|
||
|
|
"seed:forms": "DOTENV_CONFIG_PATH=.env.local npx tsx scripts/seed-forms.ts"
|
||
|
|
```
|
||
|
|
|
||
|
|
Verify it runs cleanly on an empty seeds/forms/ dir (prints the "No PDF files" message and exits 0).
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
```bash
|
||
|
|
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run seed:forms 2>&1
|
||
|
|
```
|
||
|
|
Expected: "No PDF files found in seeds/forms/" message (or seeds count if PDFs present). No unhandled errors.
|
||
|
|
</verify>
|
||
|
|
<done>npm run seed:forms runs without error. Monthly sync workflow: agent downloads PDFs to seeds/forms/, re-runs npm run seed:forms.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `form_templates` table in schema.ts with id (text PK), name, filename (unique), createdAt, updatedAt
|
||
|
|
- `documents` table has `formTemplateId` (text, nullable) and `filePath` (text, nullable) columns
|
||
|
|
- Migration applied: `npx drizzle-kit migrate` reports no pending migrations
|
||
|
|
- `seeds/forms/.gitkeep` exists
|
||
|
|
- `npm run seed:forms` exits 0 (empty dir case: prints guidance message)
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
Schema changes applied to the local PostgreSQL database. Seed script runnable. Monthly sync mechanism documented and working (re-run after adding PDFs to seeds/forms/).
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
After completion, create `.planning/phases/04-pdf-ingest/04-01-SUMMARY.md`
|
||
|
|
</output>
|