---
phase: 04-progetti-multi-project
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - src/db/schema.ts
  - src/lib/admin-queries.ts
  - src/lib/settings.ts
autonomous: true
requirements:
  - PROJ-01
  - PROJ-03
  - PROJ-05

must_haves:
  truths:
    - "La tabella projects esiste nel DB con colonne id, client_id, name, accepted_total, archived, created_at"
    - "Le tabelle phases, payments, quote_items, time_entries, documents, notes usano project_id (non client_id) come FK"
    - "La tabella clients ha il campo slug opzionale e univoco"
    - "La tabella settings esiste nel DB con colonne key, value, updated_at"
    - "drizzle-kit push completes con exit code 0"
    - "getAllProjectsWithPayments, getProjectFullDetail, getClientWithProjects sono funzioni esportate da admin-queries.ts"
    - "getSetting, updateSetting, getTargetHourlyRate sono funzioni esportate da settings.ts"
  artifacts:
    - path: "src/db/schema.ts"
      provides: "Schema completo con projects table, slug su clients, settings table, FK migrated"
      contains: "export const projects = pgTable"
    - path: "src/lib/admin-queries.ts"
      provides: "Query layer per progetti"
      exports:
        - getAllProjectsWithPayments
        - getProjectFullDetail
        - getClientWithProjects
    - path: "src/lib/settings.ts"
      provides: "Utility per settings key-value"
      exports:
        - getSetting
        - updateSetting
        - getTargetHourlyRate
  key_links:
    - from: "src/lib/admin-queries.ts"
      to: "src/db/schema.ts"
      via: "import projects, settings from @/db/schema"
      pattern: "from.*@/db/schema"
    - from: "src/lib/settings.ts"
      to: "src/db/schema.ts"
      via: "import settings from @/db/schema"
      pattern: "import.*settings.*from"
---

<objective>
Migrazione schema database da modello single-project a multi-project. Aggiunge la tabella `projects` come contenitore principale del lavoro, migra 6 FK da `client_id` a `project_id`, aggiunge `slug` ai clients, crea la tabella `settings`, ed esegue il push al DB Neon. Refactora il query layer admin per supportare le nuove relazioni.

Purpose: Fondamenta bloccanti per tutte le Wave 2 e 3. Nessuna UI può essere costruita finché il DB non ha la struttura corretta e le query non restituiscono dati project-scoped.

Output: Schema applicato al DB live, tre nuove funzioni query esportate, utility settings.
</objective>

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

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

<interfaces>
<!-- Schema corrente — punto di partenza per la migrazione. Estratto da src/db/schema.ts. -->

Tabelle con FK da clients che devono diventare project_id (D-02):
- phases: client_id → project_id (references projects.id)
- payments: client_id → project_id (references projects.id)
- quote_items: client_id → project_id (references projects.id)
- time_entries: client_id → project_id (references projects.id)
- documents: client_id → project_id (references projects.id)
- notes: client_id → project_id (references projects.id)

clients mantiene: id, name, brand_name, brief, token, archived, created_at
clients perde: accepted_total (si sposta su projects — il campo clients.accepted_total rimane ma diventa unused)
clients aggiunge: slug text().unique() (D-04)

comments rimane su entity_id generico — NON toccato (D-02).
tasks rimane su phase_id — NON toccato.
deliverables rimane su task_id — NON toccato.
service_catalog rimane invariato.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: Schema migration — projects table, slug, settings, FK migration</name>
  <files>src/db/schema.ts</files>

  <read_first>
    - src/db/schema.ts — leggere l'intero file prima di modificarlo; capire l'ordine attuale delle tabelle, i pattern di import, la sezione RELATIONS e la sezione TYPESCRIPT TYPES
    - CLAUDE.md — Architecture Constraints: token mai PK, quote_items mai esposti via client API, deliverables.approved_at immutable
  </read_first>

  <action>
Modificare src/db/schema.ts con le seguenti operazioni PRECISE, nell'ordine:

**1. Aggiungere slug a clients (D-04)**
Dopo la riga `token: text("token").notNull().unique().$defaultFn(() => nanoid()),` aggiungere:
```
  // slug è opzionale, univoco, URL-safe (es. mario-rossi) — se assente, il link usa il token
  slug: text("slug").unique(),
```
LASCIARE accepted_total su clients — rimane nel schema per compatibilità ma diventa unused (D-03 dice che accepted_total si sposta su projects, non che viene rimosso da clients).

**2. Inserire la tabella projects DOPO clients e PRIMA di phases (D-01)**
```typescript
// ============ PROJECTS ============
export const projects = pgTable("projects", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => nanoid()),
  client_id: text("client_id")
    .notNull()
    .references(() => clients.id, { onDelete: "cascade" }),
  name: text("name").notNull(), // brand/project name
  accepted_total: numeric("accepted_total", { precision: 10, scale: 2 }).default("0"),
  archived: boolean("archived").notNull().default(false),
  created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
```

**3. Migrare FK in phases (D-02)** — cambiare:
```typescript
  client_id: text("client_id")
    .notNull()
    .references(() => clients.id, { onDelete: "cascade" }),
```
con:
```typescript
  project_id: text("project_id")
    .notNull()
    .references(() => projects.id, { onDelete: "cascade" }),
```

**4. Migrare FK in payments (D-02)** — stesso pattern: sostituire `client_id` → `project_id` con references(() => projects.id, { onDelete: "cascade" })

**5. Migrare FK in documents (D-02)** — stesso pattern

**6. Migrare FK in notes (D-02)** — stesso pattern

**7. Migrare FK in time_entries (D-19)** — stesso pattern: `client_id` → `project_id`

**8. Migrare FK in quote_items (D-02)** — stesso pattern. MANTENERE il commento "NEVER exposed via client API" sul campo.

**9. Aggiungere tabella settings ALLA FINE prima della sezione RELATIONS (D-21 — Claude's Discretion: key-value)**
```typescript
// ============ SETTINGS (global admin settings — key-value store) ============
export const settings = pgTable("settings", {
  key: text("key").primaryKey(),
  value: text("value").notNull(),
  updated_at: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
```

**10. Aggiornare la sezione RELATIONS**

Aggiungere projectsRelations:
```typescript
export const projectsRelations = relations(projects, ({ one, many }) => ({
  client: one(clients, { fields: [projects.client_id], references: [clients.id] }),
  phases: many(phases),
  payments: many(payments),
  documents: many(documents),
  notes: many(notes),
  quote_items: many(quote_items),
  time_entries: many(time_entries),
}));
```

Aggiornare clientsRelations — aggiungere `projects: many(projects)`, rimuovere le relazioni che si spostano (phases, payments, documents, notes, quote_items rimangono su clients? NO — seguire il nuovo schema: phases/payments/etc. puntano ora a projects, non a clients). clientsRelations diventa:
```typescript
export const clientsRelations = relations(clients, ({ many }) => ({
  projects: many(projects),
}));
```

Aggiornare phasesRelations — cambiare `client` in `project`:
```typescript
export const phasesRelations = relations(phases, ({ one, many }) => ({
  project: one(projects, { fields: [phases.project_id], references: [projects.id] }),
  tasks: many(tasks),
}));
```

Aggiornare paymentsRelations:
```typescript
export const paymentsRelations = relations(payments, ({ one }) => ({
  project: one(projects, { fields: [payments.project_id], references: [projects.id] }),
}));
```

Aggiornare documentsRelations:
```typescript
export const documentsRelations = relations(documents, ({ one }) => ({
  project: one(projects, { fields: [documents.project_id], references: [projects.id] }),
}));
```

Aggiornare notesRelations:
```typescript
export const notesRelations = relations(notes, ({ one }) => ({
  project: one(projects, { fields: [notes.project_id], references: [projects.id] }),
}));
```

Aggiornare quoteItemsRelations:
```typescript
export const quoteItemsRelations = relations(quote_items, ({ one }) => ({
  project: one(projects, { fields: [quote_items.project_id], references: [projects.id] }),
  service: one(service_catalog, {
    fields: [quote_items.service_id],
    references: [service_catalog.id],
  }),
}));
```

Aggiungere timeEntriesRelations:
```typescript
export const timeEntriesRelations = relations(time_entries, ({ one }) => ({
  project: one(projects, { fields: [time_entries.project_id], references: [projects.id] }),
}));
```

**11. Aggiornare la sezione TYPESCRIPT TYPES**
Aggiungere dopo le type esistenti:
```typescript
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
export type Setting = typeof settings.$inferSelect;
export type NewSetting = typeof settings.$inferInsert;
```
  </action>

  <verify>
    <automated>npx tsc --noEmit 2>&1 | head -30</automated>
  </verify>

  <acceptance_criteria>
    - src/db/schema.ts contains `export const projects = pgTable("projects"`
    - src/db/schema.ts contains `slug: text("slug").unique()`
    - src/db/schema.ts contains `export const settings = pgTable("settings"`
    - src/db/schema.ts contains `project_id: text("project_id")` (at least 6 occurrences for the 6 migrated tables)
    - src/db/schema.ts does NOT contain `client_id` in phases, payments, documents, notes, time_entries, quote_items tables (grep: `grep -n "client_id" src/db/schema.ts` — solo clients table e projects table devono avere client_id)
    - src/db/schema.ts contains `export type Project = typeof projects.$inferSelect`
    - TypeScript compila senza errori su src/db/schema.ts
  </acceptance_criteria>

  <done>schema.ts aggiornato con tutte le nuove tabelle e FK migrate, TypeScript pulito</done>
</task>

<task type="auto">
  <name>Task 2: [BLOCKING] drizzle-kit push — apply schema to Neon DB</name>
  <files>src/db/schema.ts</files>

  <read_first>
    - drizzle.config.ts — verificare la configurazione del push (URL, schema path, out dir)
  </read_first>

  <action>
Eseguire il push del nuovo schema al database Neon live.

IMPORTANTE: All data is test data — hard migration (drop/recreate tables) is acceptable per A2 dell'RESEARCH.md Assumptions Log. Confermare eventuali prompt interattivi di drizzle-kit con "yes" se richiesto.

```bash
npx drizzle-kit push
```

Se drizzle-kit chiede conferma per operazioni distruttive (drop/recreate di colonne), rispondere "yes" — tutti i dati sono di test.

Dopo il push, verificare che le tabelle esistano nel DB con la struttura corretta.
  </action>

  <verify>
    <automated>npx drizzle-kit push 2>&1; echo "Exit code: $?"</automated>
  </verify>

  <acceptance_criteria>
    - Il comando `npx drizzle-kit push` completa con exit code 0
    - L'output non contiene "error" o "failed" (case-insensitive)
    - L'output conferma che le tabelle sono state create/aggiornate
  </acceptance_criteria>

  <done>Schema applicato al DB Neon live, tutte le nuove tabelle presenti</done>
</task>

<task type="auto">
  <name>Task 3: Query layer — getAllProjectsWithPayments, getProjectFullDetail, getClientWithProjects + settings.ts</name>
  <files>
    src/lib/admin-queries.ts
    src/lib/settings.ts
  </files>

  <read_first>
    - src/lib/admin-queries.ts — leggere l'intero file per capire i pattern di query, i tipi esistenti (ClientWithPayments, QuoteItemWithLabel), getAllClientsWithPayments e getClientFullDetail che sono i template esatti per le nuove funzioni
    - src/db/schema.ts — verificare i nuovi tipi disponibili: Project, Setting, e le FK corrette (project_id)
  </read_first>

  <action>
**A. Aggiornare src/lib/admin-queries.ts**

Aggiungere in cima agli import `projects` e `settings` dalla schema:
```typescript
import {
  clients,
  projects,
  payments,
  phases,
  tasks,
  deliverables,
  comments,
  documents,
  notes,
  time_entries,
  quote_items,
  service_catalog,
  settings,
} from "@/db/schema";
```

Aggiungere import dei nuovi tipi:
```typescript
import type {
  Client,
  Project,
  Phase,
  Task,
  Deliverable,
  Payment,
  Document,
  Note,
  Comment,
  ServiceCatalog,
} from "@/db/schema";
```

**1. Aggiungere tipo ProjectWithPayments e funzione getAllProjectsWithPayments**

Seguire ESATTAMENTE il pattern di ClientWithPayments e getAllClientsWithPayments (linee 28-105 del file corrente), sostituendo clients con projects e client_id con project_id:

```typescript
export type ProjectWithPayments = {
  id: string;
  name: string;
  client: { id: string; name: string; slug: string | null };
  accepted_total: string;
  archived: boolean;
  created_at: Date;
  payments: Array<{ id: string; label: string; status: string; amount: string }>;
  activeTimerEntryId: string | null;
  activeTimerStartedAt: Date | null;
  totalTrackedSeconds: number;
};

export async function getAllProjectsWithPayments(
  includeArchived = false
): Promise<ProjectWithPayments[]> {
  // 1. Fetch all projects with their parent client
  const allProjects = await db
    .select({
      id: projects.id,
      name: projects.name,
      client_id: projects.client_id,
      accepted_total: projects.accepted_total,
      archived: projects.archived,
      created_at: projects.created_at,
    })
    .from(projects)
    .orderBy(projects.created_at);

  const visible = includeArchived
    ? allProjects
    : allProjects.filter((p) => !p.archived);

  if (visible.length === 0) return [];

  const projectIds = visible.map((p) => p.id);
  const clientIds = [...new Set(visible.map((p) => p.client_id))];

  // 2. Parallel: payments, active timer, totals, parent clients
  const [allPayments, activeEntries, totals, parentClients] = await Promise.all([
    db
      .select()
      .from(payments)
      .where(inArray(payments.project_id, projectIds)),

    db
      .select({
        id: time_entries.id,
        project_id: time_entries.project_id,
        started_at: time_entries.started_at,
      })
      .from(time_entries)
      .where(isNull(time_entries.ended_at)),

    db
      .select({
        project_id: time_entries.project_id,
        total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)`,
      })
      .from(time_entries)
      .where(inArray(time_entries.project_id, projectIds))
      .groupBy(time_entries.project_id),

    db
      .select({ id: clients.id, name: clients.name, slug: clients.slug })
      .from(clients)
      .where(inArray(clients.id, clientIds)),
  ]);

  // 3. Build result map
  return visible.map((project) => {
    const projectPayments = allPayments.filter((p) => p.project_id === project.id);
    const activeEntry = activeEntries.find((e) => e.project_id === project.id);
    const totalRow = totals.find((t) => t.project_id === project.id);
    const parentClient = parentClients.find((c) => c.id === project.client_id);

    return {
      id: project.id,
      name: project.name,
      client: parentClient ?? { id: project.client_id, name: "—", slug: null },
      accepted_total: project.accepted_total ?? "0",
      archived: project.archived,
      created_at: project.created_at,
      payments: projectPayments.map((p) => ({
        id: p.id,
        label: p.label,
        status: p.status,
        amount: String(p.amount),
      })),
      activeTimerEntryId: activeEntry?.id ?? null,
      activeTimerStartedAt: activeEntry?.started_at ?? null,
      totalTrackedSeconds: totalRow ? parseInt(totalRow.total) : 0,
    };
  });
}
```

**2. Aggiungere tipo ProjectFullDetail e funzione getProjectFullDetail**

Seguire il pattern di getClientFullDetail (template completo nel RESEARCH.md alle linee 455-586). Ogni query DEVE avere `.where(eq(table.project_id, id))` — verificare uno per uno:

```typescript
export type ProjectFullDetail = {
  project: Project & { client: { id: string; name: string; brand_name: string; slug: string | null } };
  phases: Array<Phase & { tasks: Array<Task & { deliverables: Deliverable[] }> }>;
  payments: Payment[];
  documents: Document[];
  notes: Note[];
  comments: Comment[];
  quoteItems: QuoteItemWithLabel[];
  activeServices: ServiceCatalog[];
  activeTimerEntryId: string | null;
  activeTimerStartedAt: Date | null;
  totalTrackedSeconds: number;
};

export async function getProjectFullDetail(id: string): Promise<ProjectFullDetail | null> {
  // 1. Fetch project
  const projectRows = await db
    .select()
    .from(projects)
    .where(eq(projects.id, id))
    .limit(1);

  if (projectRows.length === 0) return null;
  const project = projectRows[0];

  // 2. Fetch parent client
  const clientRows = await db
    .select({ id: clients.id, name: clients.name, brand_name: clients.brand_name, slug: clients.slug })
    .from(clients)
    .where(eq(clients.id, project.client_id))
    .limit(1);
  const client = clientRows[0] ?? { id: project.client_id, name: "—", brand_name: "—", slug: null };

  // 3. Phases scoped to THIS project (not client)
  const phasesRows = await db
    .select()
    .from(phases)
    .where(eq(phases.project_id, id))
    .orderBy(asc(phases.sort_order));

  const phaseIds = phasesRows.map((p) => p.id);

  // 4. Tasks scoped to this project's phases
  const tasksRows = phaseIds.length === 0
    ? []
    : await db
        .select()
        .from(tasks)
        .where(inArray(tasks.phase_id, phaseIds))
        .orderBy(asc(tasks.sort_order));

  const taskIds = tasksRows.map((t) => t.id);

  // 5. Deliverables scoped to this project's tasks
  const deliverablesRows = taskIds.length === 0
    ? []
    : await db
        .select()
        .from(deliverables)
        .where(inArray(deliverables.task_id, taskIds));

  // 6. Parallel: payments, documents, notes, comments, quote items, active services, timer
  const [paymentsRows, documentsRows, notesRows, quoteItemRows, activeServiceRows, activeEntry, totalRes] =
    await Promise.all([
      db.select().from(payments).where(eq(payments.project_id, id)),
      db.select().from(documents).where(eq(documents.project_id, id)).orderBy(asc(documents.created_at)),
      db.select().from(notes).where(eq(notes.project_id, id)).orderBy(asc(notes.created_at)),
      db
        .select({
          id: quote_items.id,
          label: sql<string>`COALESCE(${service_catalog.name}, ${quote_items.custom_label})`,
          custom_label: quote_items.custom_label,
          service_id: quote_items.service_id,
          quantity: quote_items.quantity,
          unit_price: quote_items.unit_price,
          subtotal: quote_items.subtotal,
          project_id: quote_items.project_id,
        })
        .from(quote_items)
        .leftJoin(service_catalog, eq(quote_items.service_id, service_catalog.id))
        .where(eq(quote_items.project_id, id))
        .orderBy(asc(quote_items.id)),
      db.select().from(service_catalog).where(eq(service_catalog.active, true)).orderBy(asc(service_catalog.name)),
      db
        .select({ id: time_entries.id, started_at: time_entries.started_at })
        .from(time_entries)
        .where(eq(time_entries.project_id, id))
        .where(isNull(time_entries.ended_at))
        .limit(1),
      db
        .select({ total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)` })
        .from(time_entries)
        .where(eq(time_entries.project_id, id)),
    ]);

  // 7. Comments (polymorphic) — collect entity IDs from this project
  const allEntityIds = [id, ...taskIds, ...deliverablesRows.map((d) => d.id)];
  const commentsRows = allEntityIds.length === 0
    ? []
    : await db
        .select()
        .from(comments)
        .where(inArray(comments.entity_id, allEntityIds))
        .orderBy(asc(comments.created_at));

  // 8. Rebuild hierarchy
  const phasesWithTasks = phasesRows.map((phase) => ({
    ...phase,
    tasks: tasksRows
      .filter((t) => t.phase_id === phase.id)
      .map((task) => ({
        ...task,
        deliverables: deliverablesRows.filter((d) => d.task_id === task.id),
      })),
  }));

  return {
    project: { ...project, client } as any,
    phases: phasesWithTasks,
    payments: paymentsRows,
    documents: documentsRows,
    notes: notesRows,
    comments: commentsRows,
    quoteItems: quoteItemRows as QuoteItemWithLabel[],
    activeServices: activeServiceRows,
    activeTimerEntryId: activeEntry[0]?.id ?? null,
    activeTimerStartedAt: activeEntry[0]?.started_at ?? null,
    totalTrackedSeconds: totalRes[0] ? parseInt(totalRes[0].total) : 0,
  };
}
```

NOTA: La chiamata `.where()` doppia nella query activeEntry non è valida in Drizzle — combinare con `and()`:
```typescript
import { eq, inArray, asc, isNull, sql, and } from "drizzle-orm";
// ...
.where(and(eq(time_entries.project_id, id), isNull(time_entries.ended_at)))
```

**3. Aggiungere tipo ClientWithProjects e funzione getClientWithProjects**

```typescript
export type ClientWithProjects = Client & {
  projects: Array<{
    id: string;
    name: string;
    accepted_total: string;
    archived: boolean;
    created_at: Date;
  }>;
};

export async function getClientWithProjects(clientId: string): Promise<ClientWithProjects | null> {
  const clientRows = await db
    .select()
    .from(clients)
    .where(eq(clients.id, clientId))
    .limit(1);

  if (clientRows.length === 0) return null;
  const client = clientRows[0];

  const projectRows = await db
    .select()
    .from(projects)
    .where(eq(projects.client_id, clientId))
    .orderBy(asc(projects.created_at));

  return {
    ...client,
    projects: projectRows.map((p) => ({
      id: p.id,
      name: p.name,
      accepted_total: p.accepted_total ?? "0",
      archived: p.archived,
      created_at: p.created_at,
    })),
  };
}
```

**B. Creare src/lib/settings.ts (nuovo file)**

```typescript
import { db } from "@/db";
import { settings } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

// Constant for all known settings keys — prevents typos at call sites
export const SETTINGS_KEYS = {
  TARGET_HOURLY_RATE: "target_hourly_rate",
} as const;

export async function getSetting(key: string): Promise<string | null> {
  const rows = await db
    .select({ value: settings.value })
    .from(settings)
    .where(eq(settings.key, key))
    .limit(1);
  return rows[0]?.value ?? null;
}

export async function updateSetting(key: string, value: string): Promise<void> {
  const existing = await getSetting(key);
  if (existing !== null) {
    await db
      .update(settings)
      .set({ value, updated_at: new Date() })
      .where(eq(settings.key, key));
  } else {
    await db.insert(settings).values({ key, value });
  }
  revalidatePath("/admin/impostazioni");
}

export async function getTargetHourlyRate(): Promise<number> {
  const value = await getSetting(SETTINGS_KEYS.TARGET_HOURLY_RATE);
  // Default 50€/h if never set by admin
  return value ? parseFloat(value) : 50;
}
```
  </action>

  <verify>
    <automated>npx tsc --noEmit 2>&1 | head -40</automated>
  </verify>

  <acceptance_criteria>
    - src/lib/admin-queries.ts exports `getAllProjectsWithPayments` (grep: `grep "export async function getAllProjectsWithPayments" src/lib/admin-queries.ts`)
    - src/lib/admin-queries.ts exports `getProjectFullDetail` (grep: `grep "export async function getProjectFullDetail" src/lib/admin-queries.ts`)
    - src/lib/admin-queries.ts exports `getClientWithProjects` (grep: `grep "export async function getClientWithProjects" src/lib/admin-queries.ts`)
    - src/lib/settings.ts exists (grep: `ls src/lib/settings.ts`)
    - src/lib/settings.ts exports `getSetting`, `updateSetting`, `getTargetHourlyRate` (grep: `grep "export async function" src/lib/settings.ts`)
    - src/lib/settings.ts contains `SETTINGS_KEYS` constant (grep: `grep "SETTINGS_KEYS" src/lib/settings.ts`)
    - `npx tsc --noEmit` completa senza errori
    - `npm run build` completa senza errori TypeScript
  </acceptance_criteria>

  <done>Query layer aggiornato con tutte le funzioni project-scoped; settings.ts creato con SETTINGS_KEYS constant</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Admin → DB | Tutte le operazioni di schema e query avvengono lato server; nessun dato raw esposto al browser in questo piano |
| quote_items isolation | quote_items ora scoped a project_id — getProjectFullDetail li include solo nelle query admin, mai nel client-view path |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-01 | Information Disclosure | getProjectFullDetail | mitigate | Ogni sub-query ha WHERE eq(table.project_id, id) — verificare che nessuna query usi client_id al posto di project_id come scope; commento esplicito su ogni query |
| T-04-02 | Information Disclosure | quote_items | mitigate | quote_items inclusi SOLO in getProjectFullDetail (admin path); la funzione client-view (Wave 3) NON deve mai includere quote_items — invariante CLAUDE.md preserved |
| T-04-03 | Tampering | drizzle-kit push | accept | Hard migration su dati di test; nessun dato production a rischio (A2 assunto e verificato) |
| T-04-04 | Elevation of Privilege | settings table | accept | Settings contengono solo target_hourly_rate (non dati sensibili); letta in admin context; nessun dato cliente esposto |
</threat_model>

<verification>
Dopo il completamento di tutti e 3 i task:

```bash
# 1. Schema check
grep -c "project_id" src/db/schema.ts

# 2. Verify no client_id FK in migrated tables (solo clients e projects devono averlo)
grep -n "client_id" src/db/schema.ts

# 3. New tables present
grep "export const projects\|export const settings" src/db/schema.ts

# 4. Query functions exported
grep "export async function" src/lib/admin-queries.ts

# 5. Settings utility complete
grep "export" src/lib/settings.ts

# 6. TypeScript clean
npx tsc --noEmit

# 7. Build clean
npm run build
```
</verification>

<success_criteria>
- `grep "export const projects = pgTable" src/db/schema.ts` → output presente
- `grep "export const settings = pgTable" src/db/schema.ts` → output presente
- `grep -n "client_id" src/db/schema.ts` → solo righe in clients table e projects table (client_id come FK verso clients)
- `grep "export async function getAllProjectsWithPayments\|export async function getProjectFullDetail\|export async function getClientWithProjects" src/lib/admin-queries.ts` → 3 match
- `npx drizzle-kit push` ha completato con exit code 0
- `npx tsc --noEmit` → no errori
- `npm run build` → no errori
</success_criteria>

<output>
After completion, create `.planning/phases/04-progetti-multi-project/04-01-SUMMARY.md` following the template at `@/Users/simonecavalli/.claude/get-shit-done/templates/summary.md`.

Key items to document:
- Exact schema changes made (nuovi campi, nuove tabelle, FK migrate)
- drizzle-kit push output (conferma che le tabelle sono state create)
- Eventuali TypeScript errors risolti e come
- Nuove funzioni query esportate con le loro signature
</output>