---
phase: 10
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/db/schema.ts
  - src/lib/lead-validators.ts
  - src/lib/lead-service.ts
autonomous: true
requirements:
  - CRM-01
  - CRM-02
  - CRM-03
  - CRM-04
---

<objective>
Expand Phase 10 CRM Foundation: Complete the `leads`, `activities`, and `reminders` tables in the database schema. Establish pipeline stage enums, activity type definitions, and query layer patterns for lead management and activity logging.

Purpose: Create the schema foundation required by Phase 10 UI (Lead CRUD, pipeline view, activity log, follow-up widget). These tables enable full-featured CRM operations including lead tracking, interaction history, and reminder scheduling.

Output:
- Complete `leads` table with CRM-required fields (name, email, phone, company, status/stage, last_contact_date, next_action)
- `activities` table (timestamped records of interactions: calls, emails, meetings, notes)
- `reminders` table (follow-up reminders with due dates and completion state)
- TypeScript types and query layer (Lead, Activity, Reminder; stage constants)
- Database relations wired to leads from quotes (many quotes per lead)
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Expand leads table and add activities, reminders tables to schema</name>
  <files>src/db/schema.ts</files>
  <action>
Update the database schema by expanding the `leads` table and adding two new tables for CRM operations.

**1. Expand `leads` table** (after current definition, line 363):

Replace the placeholder definition with full CRM fields:
```typescript
export const leads = pgTable("leads", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => nanoid()),
  name: text("name").notNull(),
  email: text("email"),
  phone: text("phone"),
  company: text("company"),
  status: text("status")
    .notNull()
    .default("contacted"), // contacted | qualified | proposal_sent | negotiating | won | lost
  last_contact_date: timestamp("last_contact_date", { withTimezone: true }),
  next_action: text("next_action"),
  next_action_date: timestamp("next_action_date", { withTimezone: true }),
  notes: text("notes"),
  created_at: timestamp("created_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
  updated_at: timestamp("updated_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
});
```

**2. Add `activities` table** (after leads table):
```typescript
export const activities = pgTable("activities", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => nanoid()),
  lead_id: text("lead_id")
    .notNull()
    .references(() => leads.id, { onDelete: "cascade" }),
  type: text("type")
    .notNull(), // call | email | meeting | note
  duration_minutes: integer("duration_minutes"), // optional, for calls/meetings
  notes: text("notes").notNull(),
  activity_date: timestamp("activity_date", { withTimezone: true })
    .notNull(),
  created_at: timestamp("created_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
});
```

**3. Add `reminders` table** (after activities table):
```typescript
export const reminders = pgTable("reminders", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => nanoid()),
  lead_id: text("lead_id")
    .notNull()
    .references(() => leads.id, { onDelete: "cascade" }),
  title: text("title").notNull(),
  description: text("description"),
  due_date: timestamp("due_date", { withTimezone: true })
    .notNull(),
  completed_at: timestamp("completed_at", { withTimezone: true }),
  created_at: timestamp("created_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
});
```

**4. Update `quotes` table relation to lead** (already exists, verify at line 341):

Ensure `lead_id` is present and FK to leads.id. The placeholder was already added in Phase 8; just verify it remains.

**5. Add relations** (after existing relations, before closing exports):

```typescript
export const leadsRelations = relations(leads, ({ many }) => ({
  quotes: many(quotes),
  activities: many(activities),
  reminders: many(reminders),
}));

export const activitiesRelations = relations(activities, ({ one }) => ({
  lead: one(leads, { fields: [activities.lead_id], references: [leads.id] }),
}));

export const remindersRelations = relations(reminders, ({ one }) => ({
  lead: one(leads, { fields: [reminders.lead_id], references: [leads.id] }),
}));

export const quotesRelations = relations(quotes, ({ one, many }) => ({
  lead: one(leads, { fields: [quotes.lead_id], references: [leads.id] }),
  client: one(clients, { fields: [quotes.client_id], references: [clients.id] }),
  offerMicro: one(offer_micros, { fields: [quotes.offer_micro_id], references: [offer_micros.id] }),
  items: many(quote_items),
}));
```

**Data Safety:** The placeholder leads table already exists; this task expands it in-place with new columns. No existing data is deleted — backwards compatible.
  </action>
  <verify>
    <automated>npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "TypeScript compile pass"</automated>
  </verify>
  <done>Schema file updated with expanded leads table, activities and reminders tables, and all relations properly configured. npm run build passes. No placeholder data lost — fully backward compatible.</done>
</task>

<task type="auto">
  <name>Task 2: Create Drizzle migration for CRM tables</name>
  <files>src/db/migrations</files>
  <action>
Run drizzle-kit to auto-generate migration files for the new CRM tables and schema changes:

```bash
npx drizzle-kit generate --name=phase-10-crm-leads-activities-reminders
```

This creates a new migration file in `src/db/migrations/` with SQL for the expanded leads table and new activities/reminders tables. The migration is NOT applied yet (Phase 10 execution will push to DB).

Validate the generated migration:
- Ensure leads table ALTER statement (adding phone, company, status, last_contact_date, etc.) is correct
- Ensure activities table has FK to leads with CASCADE delete
- Ensure reminders table has FK to leads with CASCADE delete
- Ensure quotes table lead_id FK exists (may already be present from Phase 8)
- Ensure all NOT NULL constraints match schema

If migration looks incorrect, manually edit the SQL in `src/db/migrations/{migration_file}.sql` to fix.

Check for syntax errors:
```bash
head -50 src/db/migrations/phase-10-crm-leads-activities-reminders.sql
```
  </action>
  <verify>
    <automated>ls src/db/migrations/ | grep phase-10 | head -1</automated>
  </verify>
  <done>Migration file generated and located in src/db/migrations/. SQL is syntactically valid and includes all table and relation changes.</done>
</task>

<task type="auto">
  <name>Task 3: Add lead validators (Zod schemas) and activity/reminder query layer</name>
  <files>src/lib/lead-validators.ts, src/lib/lead-service.ts</files>
  <action>
Create two new library files for lead operations:

**src/lib/lead-validators.ts** — Zod schemas for CRM operations:
```typescript
import { z } from "zod";

// Pipeline stages — enum match database default values
export const LEAD_STAGES = ["contacted", "qualified", "proposal_sent", "negotiating", "won", "lost"] as const;
export const ACTIVITY_TYPES = ["call", "email", "meeting", "note"] as const;

// Lead creation (admin)
export const createLeadSchema = z.object({
  name: z.string().min(1, "Nome richiesto").max(100),
  email: z.string().email("Email valida").optional().or(z.literal("")),
  phone: z.string().optional().or(z.literal("")),
  company: z.string().optional().or(z.literal("")),
  status: z.enum(LEAD_STAGES).default("contacted"),
  notes: z.string().optional().or(z.literal("")),
});

// Lead update
export const updateLeadSchema = createLeadSchema.partial().required({ status: false });

// Activity logging
export const createActivitySchema = z.object({
  lead_id: z.string().min(1, "Lead richiesto"),
  type: z.enum(ACTIVITY_TYPES),
  activity_date: z.date().or(z.string()),
  duration_minutes: z.number().min(0).optional(),
  notes: z.string().min(1, "Note richieste").max(1000),
});

// Reminder creation
export const createReminderSchema = z.object({
  lead_id: z.string().min(1, "Lead richiesto"),
  title: z.string().min(1, "Titolo richiesto").max(200),
  description: z.string().optional(),
  due_date: z.date().or(z.string()),
});

// Change lead stage
export const updateLeadStageSchema = z.object({
  lead_id: z.string().min(1),
  stage: z.enum(LEAD_STAGES),
});
```

**src/lib/lead-service.ts** — Query layer for leads, activities, and reminders:
```typescript
import { eq, and, isNull, desc, gte, lte, ilike } from "drizzle-orm";
import { db } from "@/db";
import { leads, activities, reminders, quotes } from "@/db/schema";

// Get all leads with counts of quotes and upcoming reminders
export async function getAllLeads() {
  return await db
    .select()
    .from(leads)
    .orderBy(desc(leads.updated_at));
}

// Get lead by ID with related quotes and activity count
export async function getLeadById(id: string) {
  const [lead] = await db
    .select()
    .from(leads)
    .where(eq(leads.id, id));

  return lead;
}

// Get leads by stage (for pipeline view)
export async function getLeadsByStage(stage: string) {
  return await db
    .select()
    .from(leads)
    .where(eq(leads.status, stage))
    .orderBy(desc(leads.last_contact_date));
}

// Get leads that need follow-up (last contact > 7 days ago)
export async function getLeadsNeedingFollowUp(daysAgo: number = 7) {
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - daysAgo);

  return await db
    .select()
    .from(leads)
    .where(
      and(
        isNull(leads.last_contact_date),
        gte(leads.created_at, cutoffDate)
      )
    )
    .orderBy(leads.created_at);
}

// Get activity log for a lead (reverse chronological)
export async function getActivityLog(leadId: string) {
  return await db
    .select()
    .from(activities)
    .where(eq(activities.lead_id, leadId))
    .orderBy(desc(activities.activity_date));
}

// Get upcoming reminders for a lead
export async function getUpcomingReminders(leadId: string) {
  return await db
    .select()
    .from(reminders)
    .where(
      and(
        eq(reminders.lead_id, leadId),
        isNull(reminders.completed_at)
      )
    )
    .orderBy(reminders.due_date);
}

// Get all overdue reminders (admin widget)
export async function getOverdueReminders() {
  const now = new Date();
  return await db
    .select()
    .from(reminders)
    .innerJoin(leads, eq(reminders.lead_id, leads.id))
    .where(
      and(
        lte(reminders.due_date, now),
        isNull(reminders.completed_at)
      )
    )
    .orderBy(reminders.due_date);
}

// Create activity and auto-update lead.last_contact_date
export async function createActivity(data: {
  lead_id: string;
  type: string;
  activity_date: Date;
  duration_minutes?: number;
  notes: string;
}) {
  const [activity] = await db
    .insert(activities)
    .values(data)
    .returning();

  // Auto-update lead.last_contact_date
  await db
    .update(leads)
    .set({ last_contact_date: data.activity_date, updated_at: new Date() })
    .where(eq(leads.id, data.lead_id));

  return activity;
}

// Update lead stage
export async function updateLeadStage(leadId: string, stage: string) {
  const [updated] = await db
    .update(leads)
    .set({ status: stage, updated_at: new Date() })
    .where(eq(leads.id, leadId))
    .returning();

  return updated;
}

// Search leads by name, email, or company
export async function searchLeads(query: string) {
  return await db
    .select()
    .from(leads)
    .where(
      or(
        ilike(leads.name, `%${query}%`),
        ilike(leads.email, `%${query}%`),
        ilike(leads.company, `%${query}%`)
      )
    )
    .orderBy(desc(leads.updated_at));
}
```

These skeleton implementations provide the foundation. Phase 10 Tasks 2-3 will flesh out UI integration and form handling.
  </action>
  <verify>
    <automated>npm run build 2>&1 | grep -c "error" | grep -q 0 && echo "Validators compile"</automated>
  </verify>
  <done>Lead validators and service layer created. Schemas handle stage/type enums, activity logging, and reminder queries. Query layer is paginated for scalability. npm run build passes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin → Lead Data | Session-authenticated via Auth.js; admin actions return full lead details and activity history |
| Lead ID references | All lead operations (activities, reminders) checked via lead_id FK; cascade delete prevents orphaned records |
| Activity date validation | Activity dates must be <= current date (no future backdating); server-side timestamp immutability |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-10-01 | Tampering | Activity history modification | mitigate | Activity records immutable after creation; no update endpoint; deletion requires admin auth + audit log |
| T-10-02 | Spoofing | Lead creation with fake company/contact info | mitigate | Email/phone not validated until win (Phase 11); accept unverified input at lead stage; verify on quote accept |
| T-10-03 | Information Disclosure | Leaking lead contact details (email, phone) via API | mitigate | Lead queries require Auth.js session; no public API for leads; admin-only access |
| T-10-04 | Denial of Service | Bulk activity creation spam | mitigate | No rate limit in Phase 10 MVP; add rate limiting per admin session if needed (Phase 12) |
| T-10-05 | Elevation | Non-admin user updating lead stage | mitigate | All lead mutations require Auth.js session; middleware guards /admin/leads routes |
| T-10-06 | Tampering | Backdating activities (future activity_date) | accept | No validation in Phase 10; assume admin is honest; add validation if abuse occurs |

</threat_model>

<verification>
After Phase 10 Plan 1 execution:

1. Schema compiles: `npm run build` passes with no TS errors
2. Drizzle migration file generated: `src/db/migrations/` contains new migration
3. Lead validators import cleanly: `import { createLeadSchema, LEAD_STAGES } from '@/lib/lead-validators'` works
4. Query layer exports all functions: `getLeadById`, `getActivityLog`, `createActivity`, `updateLeadStage`, etc.
5. Relations updated: leads → activities (cascade), leads → reminders (cascade), quotes → leads (optional FK)

Data safety check:
- Placeholder leads table expanded: NOT deleted, just new columns added
- New activities/reminders tables created from scratch: no existing data at risk
- quotes table lead_id FK already present from Phase 8: no schema conflict
</verification>

<success_criteria>
- `leads`, `activities`, `reminders` tables properly defined with all CRM fields
- TypeScript compiles cleanly; Lead/Activity/Reminder types exported
- Drizzle migration file generated (not yet applied to DB)
- Lead validators (Zod) handle stage enums, activity types, date validation
- Query layer provides CRUD + search operations for leads, activities, reminders
- Relations properly configured: leads ← activities, leads ← reminders, leads ← quotes
- Database schema is ready for Phase 10 UI (pipeline view, activity log, follow-up widget)
</success_criteria>

<output>
After execution, create `.planning/phases/10-crm-pipeline-activity-logging/10-01-SUMMARY.md`
</output>
