chore: riorganizzazione e pulizia della cartella di progetto

Rimossi i doppioni e gli artefatti accumulati, senza cancellare nulla di
definitivo: tutto cio' che serviva una revisione e' parcheggiato in cestino/
(gitignored), documentato in cestino/LEGGIMI.md.

- .planning/phases/01-10: 10 cartelle identiche byte-per-byte alle copie in
  .planning/milestones/v1.0-phases e v2.0-phases. HANDOFF.md:33 documentava che
  furono copiate e non spostate, lasciando la pulizia 'facoltativa in futuro'.
  Verificata l'identita' con diff -rq prima di spostare ciascuna.
- scripts/: 13 script one-off gia' eseguiti (push-*, migrate-*, validate-*,
  verify-12-03-*) piu' reset-and-import-services.ts, che cancella dati.
  Restano i 3 riutilizzabili: seed, import-services-notion, import-service-offer-tags.
- CLAUDE-SECURITY-20260727-210226/: cartella di lavoro della run interrotta.

Cancellati subito, senza revisione: 6 .DS_Store, le due cache .impeccable/
(una era dentro src/) e tsconfig.tsbuildinfo.

.gitignore: aggiunti cestino/, .impeccable/ e CLAUDE-SECURITY-*/ per evitare
che si riformino.

src/ non e' stato toccato: la struttura e' dettata dall'App Router di Next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 14:13:32 +02:00
parent fb6ab92fd0
commit 94b3b2f766
98 changed files with 432 additions and 30516 deletions
-86
View File
@@ -1,86 +0,0 @@
import { db } from "@/db";
import { service_catalog, offer_services, services } from "@/db/schema";
import { eq, and } from "drizzle-orm";
async function migrate() {
console.log("Starting services unification migration...\n");
// 1. Backfill from service_catalog (operational pricing, used by quote_items)
const catalogRows = await db.select().from(service_catalog);
let catalogInserted = 0;
let catalogSkipped = 0;
for (const row of catalogRows) {
const existing = await db
.select()
.from(services)
.where(and(eq(services.migrated_from, "service_catalog"), eq(services.migrated_id, row.id)))
.limit(1);
if (existing.length > 0) {
catalogSkipped++;
continue;
}
await db.insert(services).values({
name: row.name,
description: row.description,
unit_price: row.unit_price,
category: "catalog",
active: row.active,
migrated_from: "service_catalog",
migrated_id: row.id,
});
catalogInserted++;
}
console.log(`service_catalog: ${catalogInserted} inserted, ${catalogSkipped} skipped (already migrated)`);
// 2. Backfill from offer_services (marketing pricing, used by offer_micro_services)
const offerRows = await db.select().from(offer_services);
let offerInserted = 0;
let offerSkipped = 0;
let renamed = 0;
for (const row of offerRows) {
const existing = await db
.select()
.from(services)
.where(and(eq(services.migrated_from, "offer_services"), eq(services.migrated_id, row.id)))
.limit(1);
if (existing.length > 0) {
offerSkipped++;
continue;
}
// Name collision check against ALL services rows inserted so far (both sources)
const collision = await db
.select()
.from(services)
.where(eq(services.name, row.name))
.limit(1);
const finalName = collision.length > 0 ? `${row.name} (Offer)` : row.name;
if (collision.length > 0) renamed++;
await db.insert(services).values({
name: finalName,
description: row.transformation_description,
unit_price: row.price,
category: "offer",
active: row.active,
migrated_from: "offer_services",
migrated_id: row.id,
});
offerInserted++;
}
console.log(`offer_services: ${offerInserted} inserted, ${offerSkipped} skipped (already migrated), ${renamed} renamed for collision`);
console.log("\nMigration complete. Run scripts/validate-services-migration.ts next.");
process.exit(0);
}
migrate().catch((err) => {
console.error("Migration failed:", err);
process.exit(1);
});
-50
View File
@@ -1,50 +0,0 @@
import { db } from "@/db";
import { services, tags } from "@/db/schema";
import { eq, and } from "drizzle-orm";
async function migrate() {
console.log("Starting tags migration: assigning 'Offerta' tag to services migrated from offer_services...\n");
const offerServices = await db
.select({ id: services.id })
.from(services)
.where(eq(services.migrated_from, "offer_services"));
let taggedCount = 0;
let skippedCount = 0;
for (const service of offerServices) {
const existing = await db
.select()
.from(tags)
.where(
and(
eq(tags.entity_type, "services"),
eq(tags.entity_id, service.id),
eq(tags.name, "Offerta")
)
)
.limit(1);
if (existing.length > 0) {
skippedCount++;
continue;
}
await db.insert(tags).values({
entity_type: "services",
entity_id: service.id,
name: "Offerta",
});
taggedCount++;
}
console.log(`Assigned 'Offerta' tag: ${taggedCount} services, ${skippedCount} already tagged`);
console.log("\nMigration complete. Run scripts/validate-tags-migration.ts next.");
process.exit(0);
}
migrate().catch((err) => {
console.error("Migration failed:", err);
process.exit(1);
});
-55
View File
@@ -1,55 +0,0 @@
// PRODUCTION DEPLOY NOTE: This migration is additive-only (CREATE TABLE IF NOT EXISTS +
// 2 indexes, no drops/truncates). Per CLAUDE.md Data Safety, apply to production via
// SSH+docker exec BEFORE pushing Phase 11 schema-dependent code (Plans 02-04).
// Run: npx tsx scripts/push-11-tags-migration.ts (with prod DATABASE_URL)
import postgres from "postgres";
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
console.log("Pushing tags table migration...");
await client`
CREATE TABLE IF NOT EXISTS tags (
id text PRIMARY KEY,
entity_type text NOT NULL,
entity_id text NOT NULL,
name text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL
)
`;
await client`
CREATE UNIQUE INDEX IF NOT EXISTS tags_entity_name_unique
ON tags (entity_type, entity_id, name)
`;
await client`
CREATE INDEX IF NOT EXISTS tags_entity_idx
ON tags (entity_type, entity_id)
`;
console.log("✓ tags table created successfully");
process.exit(0);
} catch (err: unknown) {
if (err instanceof Error) {
if (err.message.includes("already exists")) {
console.log("✓ tags table already exists (skipped)");
process.exit(0);
}
console.error("Error pushing migration:", err.message);
} else {
console.error("Unknown error:", err);
}
process.exit(1);
}
}
push();
-27
View File
@@ -1,27 +0,0 @@
// PRODUCTION DEPLOY NOTE: additive-only (ADD COLUMN IF NOT EXISTS, no drops/truncates).
// Per CLAUDE.md Data Safety, apply to the live DB via SSH tunnel / docker exec BEFORE
// deploying the schema-dependent catalog code. Idempotent — safe to re-run.
// Run: npx tsx scripts/push-11b-fase-column.ts (with DATABASE_URL pointing at the DB)
import postgres from "postgres";
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
console.log("Adding services.fase column...");
await client`ALTER TABLE services ADD COLUMN IF NOT EXISTS fase text`;
console.log("✓ services.fase column ready");
process.exit(0);
} catch (err: unknown) {
console.error("Error pushing migration:", err instanceof Error ? err.message : err);
process.exit(1);
}
}
push();
-142
View File
@@ -1,142 +0,0 @@
// PRODUCTION DEPLOY NOTE: This migration (0008_offer_tier_schema.sql) is additive-only
// (ADD COLUMN IF NOT EXISTS, a guarded DO-block CHECK constraint, CREATE TABLE IF NOT
// EXISTS, and CREATE INDEX IF NOT EXISTS — no drops/truncates). Per CLAUDE.md Data
// Safety, this script MUST be run against PRODUCTION via SSH+docker exec BEFORE Phase
// 12 Wave 3 code (query layer reading offer_tier_services/tier_letter/public_price/
// category/ticket) is exercised against production data. This is the BLOCKING step
// for Plan 02. Idempotent — safe to re-run.
// Run: npx tsx scripts/push-12-offer-tier-schema.ts (with prod DATABASE_URL)
import postgres from "postgres";
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
console.log("Pushing offer tier schema migration (0008)...");
// offer_macros: archive flag + short description + category/ticket dimensions +
// structured transformation promise
const offerMacrosColumns: Array<[string, string]> = [
["description", "text"],
["category", "text"],
["ticket", "text"],
["is_archived", "boolean NOT NULL DEFAULT false"],
["cliente_ideale", "text"],
["risultato", "text"],
["tempo", "text"],
["pain", "text"],
["metodo", "text"],
];
for (const [column, type] of offerMacrosColumns) {
try {
await client.unsafe(
`ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS ${column} ${type}`
);
console.log(` ✓ offer_macros.${column} ready`);
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(` ✓ offer_macros.${column} already exists (skipped)`);
} else {
throw err;
}
}
}
// offer_micros: tier designation (A/B/C) + manual public price
const offerMicrosColumns: Array<[string, string]> = [
["tier_letter", "text"],
["public_price", "numeric(10, 2)"],
];
for (const [column, type] of offerMicrosColumns) {
try {
await client.unsafe(
`ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS ${column} ${type}`
);
console.log(` ✓ offer_micros.${column} ready`);
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(` ✓ offer_micros.${column} already exists (skipped)`);
} else {
throw err;
}
}
}
// CHECK constraint for tier_letter, guarded so it's safe to re-run.
try {
await client.unsafe(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'offer_micros_tier_letter_check'
) THEN
ALTER TABLE offer_micros
ADD CONSTRAINT offer_micros_tier_letter_check
CHECK (tier_letter IS NULL OR tier_letter IN ('A', 'B', 'C'));
END IF;
END $$;
`);
console.log(" ✓ offer_micros_tier_letter_check constraint ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_micros_tier_letter_check already exists (skipped)");
} else {
throw err;
}
}
// New junction table: tier (offer_micros) <-> unified services catalog.
try {
await client.unsafe(`
CREATE TABLE IF NOT EXISTS offer_tier_services (
tier_id text NOT NULL REFERENCES offer_micros(id) ON DELETE CASCADE,
service_id text NOT NULL REFERENCES services(id) ON DELETE CASCADE,
created_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (tier_id, service_id)
)
`);
console.log(" ✓ offer_tier_services table ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_tier_services table already exists (skipped)");
} else {
throw err;
}
}
try {
await client.unsafe(`
CREATE INDEX IF NOT EXISTS offer_tier_services_tier_idx ON offer_tier_services USING btree (tier_id)
`);
console.log(" ✓ offer_tier_services_tier_idx index ready");
} catch (err: unknown) {
if (err instanceof Error && err.message.includes("already exists")) {
console.log(" ✓ offer_tier_services_tier_idx already exists (skipped)");
} else {
throw err;
}
}
console.log("✓ Migration 0008 (offer tier schema) applied successfully");
process.exit(0);
} catch (err: unknown) {
if (err instanceof Error) {
console.error("Error pushing migration:", err.message);
} else {
console.error("Unknown error:", err);
}
process.exit(1);
} finally {
await client.end();
}
}
push();
-79
View File
@@ -1,79 +0,0 @@
import postgres from "postgres";
import { readFileSync } from "fs";
import { join } from "path";
// Applies 0005_phase_10_crm_leads_activities_reminders.sql (additive-only:
// ALTER TABLE leads ADD COLUMN x8, CREATE TABLE activities/reminders, indexes).
// Idempotent: skips if already applied. Never drops or truncates anything.
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const sql = postgres(databaseUrl, { max: 1 });
try {
const cols = await sql`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'leads' ORDER BY ordinal_position
`;
const tables = await sql`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('activities', 'reminders')
`;
const colNames = cols.map((c) => c.column_name);
console.log("Pre-state — leads columns:", colNames.join(", "));
console.log(
"Pre-state — CRM tables:",
tables.map((t) => t.table_name).join(", ") || "none"
);
if (colNames.includes("status") && tables.length === 2) {
console.log("✓ Migration 0005 already applied (skipped)");
process.exit(0);
}
const migrationSql = readFileSync(
join(
__dirname,
"../src/db/migrations/0005_phase_10_crm_leads_activities_reminders.sql"
),
"utf-8"
);
console.log("Applying migration 0005 in a transaction...");
await sql.begin(async (tx) => {
await tx.unsafe(migrationSql);
});
const postCols = await sql`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'leads' ORDER BY ordinal_position
`;
const postTables = await sql`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_name IN ('activities', 'reminders')
`;
console.log(
"Post-state — leads columns:",
postCols.map((c) => c.column_name).join(", ")
);
console.log(
"Post-state — CRM tables:",
postTables.map((t) => t.table_name).join(", ")
);
console.log("✓ Migration 0005 applied successfully");
process.exit(0);
} catch (err: unknown) {
console.error(
"Error applying migration:",
err instanceof Error ? err.message : err
);
process.exit(1);
}
}
push();
-48
View File
@@ -1,48 +0,0 @@
import postgres from "postgres";
import { readFileSync } from "fs";
import { join } from "path";
async function push() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
console.log("Pushing services table migration...");
// Create the services table
await client`
CREATE TABLE IF NOT EXISTS services (
id text PRIMARY KEY,
name text NOT NULL,
description text,
unit_price numeric(10, 2) NOT NULL,
category text,
active boolean DEFAULT true NOT NULL,
migrated_from text,
migrated_id text,
created_at timestamp with time zone DEFAULT now() NOT NULL
)
`;
console.log("✓ services table created successfully");
process.exit(0);
} catch (err: unknown) {
if (err instanceof Error) {
if (err.message.includes("already exists")) {
console.log("✓ services table already exists (skipped)");
process.exit(0);
}
console.error("Error pushing migration:", err.message);
} else {
console.error("Unknown error:", err);
}
process.exit(1);
}
}
push();
-134
View File
@@ -1,134 +0,0 @@
// One-shot: deletes 3 legacy test services, then imports 55 from Notion CSV.
// offer_tier_services rows cascade-delete automatically (ON DELETE CASCADE).
// Run via SSH tunnel:
// DATABASE_URL=$(node --env-file=.env.local -e 'const u=new URL(process.env.DATABASE_URL); u.host="127.0.0.1:54321"; process.stdout.write(u.toString())') \
// npx tsx scripts/reset-and-import-services.ts
import postgres from "postgres";
import { customAlphabet } from "nanoid";
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 12);
type ServiceRow = {
name: string;
unit_price: number;
category: string;
fase: string | null;
};
const SERVICES: ServiceRow[] = [
{ name: "Raccolta e Mappatura Materiali", unit_price: 100, category: "Signature Offer", fase: "Fase 1 → Onboarding / Setup" },
{ name: "Audit iniziale (UX/UI, struttura, conversione)", unit_price: 500, category: "Signature Offer", fase: "Fase 1 → Onboarding / Setup" },
{ name: "Workshop (1° fase)", unit_price: 900, category: "Signature Offer", fase: "Fase 1 → Onboarding / Setup" },
{ name: "Analisi Competitor", unit_price: 400, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Documento di restituzione (problemi + lista ottimizzazioni)", unit_price: 500, category: "Entry Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Redesign visivo dell'above-the-fold / hero (il 'prima → dopo')", unit_price: 700, category: "Entry Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Call di restituzione", unit_price: 200, category: "Entry Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Call di presentazione Prima/Dopo", unit_price: 200, category: "Entry Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Roadmap / istruzioni operative + mini kit", unit_price: 600, category: "Entry Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "UX Research", unit_price: 1200, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Customer Journey", unit_price: 900, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Architettura (sitemap)", unit_price: 500, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Brand Identity - Visual identity", unit_price: 2000, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Brand Identity - Voice identity", unit_price: 800, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Direzione Creative (moodboard)", unit_price: 600, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Workshop (2° fase)", unit_price: 900, category: "Signature Offer", fase: "Fase 2 → Analisi / Strategia" },
{ name: "Settaggio CMS (Wordpress Webflow ecc)", unit_price: 500, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "UX - UI", unit_price: 1000, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Wireframe (Low-Mid Fidelity)", unit_price: 600, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Homepage (Figma + Dev)", unit_price: 1500, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Revisione #1", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- Chi siamo", unit_price: 600, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- Contatti", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- Blog (Archivio)", unit_price: 300, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- - Blog post (Template Singolo)", unit_price: 600, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- Case Study (Archivio)", unit_price: 300, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- - Case Study (Template Singolo)", unit_price: 800, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- Landing Page (Metodo o Differenziante)", unit_price: 1000, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- - - Thank You Page", unit_price: 300, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "- - - 404", unit_price: 150, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Responsive (inclusa nelle pagine?)", unit_price: 800, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "UX writing (Revisione testi)", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Seo Setup (basic on-page)", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Seo Avanzato (+ keyword + ricerca + blog post dentro retainer)", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Revisione #2 finale", unit_price: 400, category: "Signature Offer", fase: "Fase 3 → Esecuzione / Core" },
{ name: "Web Core Vitals (Optimization)", unit_price: 500, category: "Signature Offer", fase: "Fase 4 → Raffinamento / Extra" },
{ name: "QA - Test Cross Browser e responsive", unit_price: 400, category: "Signature Offer", fase: "Fase 4 → Raffinamento / Extra" },
{ name: "Setting GA4 - Tag Manager - Hotjar/Clarify", unit_price: 500, category: "Signature Offer", fase: "Fase 4 → Raffinamento / Extra" },
{ name: "Raccolta Feedback Post Lancio", unit_price: 200, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Go-live / messa online & accessi", unit_price: 300, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Audit uscita", unit_price: 500, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Real User Testing", unit_price: 1200, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Follow Up Handover", unit_price: 200, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Video Tutorial per micro modifiche in autonomia", unit_price: 400, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Revisioni Extra illimitate (sicuri illimitati???)", unit_price: 1200, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Brand Kit (youtube - linkedin - insta)", unit_price: 600, category: "Signature Offer", fase: "Fase 5 → Offboarding / Consegna" },
{ name: "Mantenimento tecnico (sito sempre online, bello, funzionante)", unit_price: 200, category: "Retainer Offer", fase: null },
{ name: "Monitoraggio dati (GA4 / Hotjar-Clarity)", unit_price: 100, category: "Retainer Offer", fase: null },
{ name: "Report mensile", unit_price: 100, category: "Retainer Offer", fase: null },
{ name: "CRO - analisi UX (hotmap, punti di drop)", unit_price: 200, category: "Retainer Offer", fase: null },
{ name: "CRO - implementazione miglioramenti", unit_price: 300, category: "Retainer Offer", fase: null },
{ name: "Call di Mentorship/Consulenza/allineamento/revisione", unit_price: 800, category: "Retainer Offer", fase: null },
{ name: "Art direction su direzione da prendere (consulente strategico interno disponibile 24/7)", unit_price: 2000, category: "Retainer Offer", fase: null },
{ name: "Extra landing (prezzo singolo per pompare prezzo)", unit_price: 3000, category: "Retainer Offer", fase: null },
{ name: "SEO avanzato (Blog post)", unit_price: 400, category: "Retainer Offer", fase: null },
];
const LEGACY_IDS = [
"LLi-DynaQ_Y13vgREFtSB", // Audit Iniziale (test)
"MlhY34V7M_ylSPekeuIsz", // Raccolta e Mappatura Materiali (test, wrong category)
"lDZkctWhcXqsYVDy2nac6", // Analisi Competitor (test, wrong category)
];
async function run() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const client = postgres(databaseUrl);
try {
// Step 1: delete legacy test services (offer_tier_services cascade-deletes)
console.log("Deleting 3 legacy test services...");
const deleted = await client`
DELETE FROM services WHERE id = ANY(${LEGACY_IDS}) RETURNING name
`;
for (const row of deleted) console.log(` ✗ deleted: ${row.name}`);
console.log();
// Step 2: import 55 Notion services
console.log(`Importing ${SERVICES.length} services...`);
let inserted = 0;
let skipped = 0;
for (const svc of SERVICES) {
const existing = await client`
SELECT id FROM services WHERE name = ${svc.name} LIMIT 1
`;
if (existing.length > 0) {
console.log(` ↷ exists: ${svc.name}`);
skipped++;
continue;
}
const id = nanoid();
await client`
INSERT INTO services (id, name, unit_price, category, fase, active, migrated_from)
VALUES (${id}, ${svc.name}, ${svc.unit_price}, ${svc.category}, ${svc.fase}, true, 'notion-csv-2026-06-18')
`;
console.log(`${svc.name}`);
inserted++;
}
const [{ count }] = await client`SELECT count(*) FROM services`;
console.log(`\n✓ Done — ${inserted} inserted, ${skipped} skipped. Total in DB: ${count}`);
process.exit(0);
} catch (err) {
console.error("Error:", err instanceof Error ? err.message : err);
process.exit(1);
} finally {
await client.end();
}
}
run();
-262
View File
@@ -1,262 +0,0 @@
import { db } from "@/db";
import {
offer_phases,
offer_phase_services,
quotes,
quote_items,
projects,
phases,
leads,
} from "@/db/schema";
import { sql } from "drizzle-orm";
/**
* Validation script for Phase 8 migration
*
* Checks that all new tables/columns exist and FKs are intact
* Exit code 0 if all checks pass, 1 if any fail
*/
type ValidationResult = {
name: string;
passed: boolean;
error?: string;
};
const results: ValidationResult[] = [];
async function runChecks() {
console.log("=== Phase 8 Migration Validation ===\n");
// Check 1: offer_phases table exists
try {
const phaseCount = await db
.select({ count: sql<number>`COUNT(*)` })
.from(offer_phases);
results.push({
name: "offer_phases table exists",
passed: true,
error: `(${phaseCount[0].count} rows)`,
});
} catch (e) {
results.push({
name: "offer_phases table exists",
passed: false,
error: (e as Error).message,
});
}
// Check 2: offer_phase_services table exists
try {
const serviceCount = await db
.select({ count: sql<number>`COUNT(*)` })
.from(offer_phase_services);
results.push({
name: "offer_phase_services table exists",
passed: true,
error: `(${serviceCount[0].count} rows)`,
});
} catch (e) {
results.push({
name: "offer_phase_services table exists",
passed: false,
error: (e as Error).message,
});
}
// Check 3: quotes table exists
try {
const quoteCount = await db
.select({ count: sql<number>`COUNT(*)` })
.from(quotes);
results.push({
name: "quotes table exists",
passed: true,
error: `(${quoteCount[0].count} rows)`,
});
} catch (e) {
results.push({
name: "quotes table exists",
passed: false,
error: (e as Error).message,
});
}
// Check 4: leads table exists
try {
const leadCount = await db
.select({ count: sql<number>`COUNT(*)` })
.from(leads);
results.push({
name: "leads table exists",
passed: true,
error: `(${leadCount[0].count} rows)`,
});
} catch (e) {
results.push({
name: "leads table exists",
passed: false,
error: (e as Error).message,
});
}
// Check 5: quote_items.quote_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'quote_items' AND column_name = 'quote_id'`
);
results.push({
name: "quote_items.quote_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "quote_items.quote_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Check 6: quote_items.offer_micro_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'quote_items' AND column_name = 'offer_micro_id'`
);
results.push({
name: "quote_items.offer_micro_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "quote_items.offer_micro_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Check 7: quote_items.offer_phase_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'quote_items' AND column_name = 'offer_phase_id'`
);
results.push({
name: "quote_items.offer_phase_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "quote_items.offer_phase_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Check 8: projects.offer_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'projects' AND column_name = 'offer_id'`
);
results.push({
name: "projects.offer_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "projects.offer_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Check 9: projects.created_from_lead_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'projects' AND column_name = 'created_from_lead_id'`
);
results.push({
name: "projects.created_from_lead_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "projects.created_from_lead_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Check 10: phases.offer_phase_id column exists
try {
const col = await db
.select({ column_name: sql<string>`column_name` })
.from(
sql`information_schema.columns`
)
.where(
sql`table_name = 'phases' AND column_name = 'offer_phase_id'`
);
results.push({
name: "phases.offer_phase_id column exists",
passed: col.length > 0,
error: col.length > 0 ? undefined : "Column not found",
});
} catch (e) {
results.push({
name: "phases.offer_phase_id column exists",
passed: false,
error: (e as Error).message,
});
}
// Print results
let allPassed = true;
console.log("Checks:\n");
for (const result of results) {
const status = result.passed ? "✓" : "✗";
const msg = result.error
? ` ${result.error}`
: " passed";
console.log(`${status} ${result.name}${msg}`);
if (!result.passed) allPassed = false;
}
console.log(
`\n=== Result: ${allPassed ? "ALL CHECKS PASSED" : "SOME CHECKS FAILED"} ===`
);
process.exit(allPassed ? 0 : 1);
}
runChecks().catch((e) => {
console.error("Validation script error:", e);
process.exit(1);
});
-116
View File
@@ -1,116 +0,0 @@
import { db } from "@/db";
import { service_catalog, offer_services, services, quote_items, offer_micro_services } from "@/db/schema";
import { sql, eq } from "drizzle-orm";
async function validate() {
let failures = 0;
// Check 1: row counts match
const [catalogCount] = await db.select({ n: sql<number>`count(*)::int` }).from(service_catalog);
const [offerCount] = await db.select({ n: sql<number>`count(*)::int` }).from(offer_services);
const [migratedCatalog] = await db
.select({ n: sql<number>`count(*)::int` })
.from(services)
.where(eq(services.migrated_from, "service_catalog"));
const [migratedOffer] = await db
.select({ n: sql<number>`count(*)::int` })
.from(services)
.where(eq(services.migrated_from, "offer_services"));
console.log(`service_catalog rows: ${catalogCount.n} | migrated: ${migratedCatalog.n}`);
console.log(`offer_services rows: ${offerCount.n} | migrated: ${migratedOffer.n}`);
if (catalogCount.n !== migratedCatalog.n) {
console.log("FAIL: service_catalog row count does not match migrated count");
failures++;
} else {
console.log("PASS: service_catalog fully migrated");
}
if (offerCount.n !== migratedOffer.n) {
console.log("FAIL: offer_services row count does not match migrated count");
failures++;
} else {
console.log("PASS: offer_services fully migrated");
}
// Check 2: no orphaned migrated_id (every migrated_id from service_catalog still exists in service_catalog)
const orphanedCatalog = await db.execute(sql`
SELECT COUNT(*)::int AS n FROM services s
WHERE s.migrated_from = 'service_catalog'
AND NOT EXISTS (SELECT 1 FROM service_catalog sc WHERE sc.id = s.migrated_id)
`);
const orphanedCatalogCount = (orphanedCatalog as unknown as Array<{ n: number }>)[0]?.n ?? 0;
if (orphanedCatalogCount > 0) {
console.log(`FAIL: ${orphanedCatalogCount} services rows reference missing service_catalog ids`);
failures++;
} else {
console.log("PASS: no orphaned service_catalog references");
}
const orphanedOffer = await db.execute(sql`
SELECT COUNT(*)::int AS n FROM services s
WHERE s.migrated_from = 'offer_services'
AND NOT EXISTS (SELECT 1 FROM offer_services os WHERE os.id = s.migrated_id)
`);
const orphanedOfferCount = (orphanedOffer as unknown as Array<{ n: number }>)[0]?.n ?? 0;
if (orphanedOfferCount > 0) {
console.log(`FAIL: ${orphanedOfferCount} services rows reference missing offer_services ids`);
failures++;
} else {
console.log("PASS: no orphaned offer_services references");
}
// Check 3: existing quote_items.service_id still resolve in service_catalog (untouched FK — must remain valid)
const orphanedQuoteItems = await db.execute(sql`
SELECT COUNT(*)::int AS n FROM quote_items qi
WHERE qi.service_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM service_catalog sc WHERE sc.id = qi.service_id)
`);
const orphanedQuoteItemsCount = (orphanedQuoteItems as unknown as Array<{ n: number }>)[0]?.n ?? 0;
if (orphanedQuoteItemsCount > 0) {
console.log(`FAIL: ${orphanedQuoteItemsCount} quote_items reference missing service_catalog ids (pre-existing FK broken!)`);
failures++;
} else {
console.log("PASS: quote_items.service_id -> service_catalog FK intact (unchanged by this migration)");
}
// Check 4: existing offer_micro_services.service_id still resolve in offer_services (untouched FK — must remain valid)
const orphanedMicroServices = await db.execute(sql`
SELECT COUNT(*)::int AS n FROM offer_micro_services oms
WHERE NOT EXISTS (SELECT 1 FROM offer_services os WHERE os.id = oms.service_id)
`);
const orphanedMicroServicesCount = (orphanedMicroServices as unknown as Array<{ n: number }>)[0]?.n ?? 0;
if (orphanedMicroServicesCount > 0) {
console.log(`FAIL: ${orphanedMicroServicesCount} offer_micro_services reference missing offer_services ids (pre-existing FK broken!)`);
failures++;
} else {
console.log("PASS: offer_micro_services.service_id -> offer_services FK intact (unchanged by this migration)");
}
// Check 5: name collision report (informational)
const collisions = await db.execute(sql`
SELECT name, COUNT(*)::int AS n FROM services GROUP BY name HAVING COUNT(*) > 1
`);
const collisionRows = collisions as unknown as Array<{ name: string; n: number }>;
if (collisionRows.length > 0) {
console.log(`WARNING: ${collisionRows.length} duplicate names remain in services (review):`, collisionRows);
} else {
console.log("PASS: no duplicate names in services");
}
// Check 6: informational — count of net-new (non-migrated) services
const [netNew] = await db
.select({ n: sql<number>`count(*)::int` })
.from(services)
.where(sql`migrated_from IS NULL`);
console.log(`INFO: ${netNew.n} net-new services created post-migration (migrated_from IS NULL)`);
console.log(`\n${failures === 0 ? "ALL CHECKS PASSED" : `${failures} CHECK(S) FAILED`}`);
process.exit(failures === 0 ? 0 : 1);
}
validate().catch((err) => {
console.error("Validation failed:", err);
process.exit(1);
});
-55
View File
@@ -1,55 +0,0 @@
import { db } from "@/db";
import { services, tags } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm";
async function validate() {
let failures = 0;
const [offerServicesCount] = await db
.select({ n: sql<number>`count(*)::int` })
.from(services)
.where(eq(services.migrated_from, "offer_services"));
const [offerTagCount] = await db
.select({ n: sql<number>`count(*)::int` })
.from(tags)
.where(and(eq(tags.entity_type, "services"), eq(tags.name, "Offerta")));
console.log(`offer_services-derived services: ${offerServicesCount.n} | Offerta tags: ${offerTagCount.n}`);
if (offerServicesCount.n !== offerTagCount.n) {
console.log("FAIL: Offerta tag count does not match offer_services-derived services count");
failures++;
} else {
console.log("PASS: all offer_services-derived services have the Offerta tag");
}
const orphanedTags = await db.execute(sql`
SELECT COUNT(*)::int AS n FROM tags t
WHERE t.entity_type = 'services'
AND NOT EXISTS (SELECT 1 FROM services s WHERE s.id = t.entity_id)
`);
const orphanedTagsCount = (orphanedTags as unknown as Array<{ n: number }>)[0]?.n ?? 0;
if (orphanedTagsCount > 0) {
console.log(`FAIL: ${orphanedTagsCount} tags reference non-existent services`);
failures++;
} else {
console.log("PASS: no orphaned tag references");
}
const tagCounts = await db.execute(sql`
SELECT entity_type, COUNT(*)::int AS n FROM tags GROUP BY entity_type
`);
const counts = tagCounts as unknown as Array<{ entity_type: string; n: number }>;
for (const row of counts) {
console.log(`INFO: ${row.n} tags for entity_type=${row.entity_type}`);
}
console.log(`\n${failures === 0 ? "ALL CHECKS PASSED" : `${failures} CHECK(S) FAILED`}`);
process.exit(failures === 0 ? 0 : 1);
}
validate().catch((err) => {
console.error("Validation failed:", err);
process.exit(1);
});
-242
View File
@@ -1,242 +0,0 @@
// Phase 12 Plan 03 — Task 2 verification script for actions.ts additions.
//
// This project has no test runner configured (no `scripts.test` in package.json),
// and `.env.local`'s DATABASE_URL points at PRODUCTION (per project memory) — so
// this script does NOT execute any DB calls or server actions (server actions
// also require an Auth.js session, unavailable outside a request context). It
// typechecks against the real exports/types from
// src/app/admin/offers/actions.ts and documents the 9 expected behaviors from
// the plan's <behavior> block, for manual run against a dev DB later if desired.
//
// Run (manual, dev DB only, inside a request context with admin session):
// npx tsx scripts/verify-12-03-actions.ts
import type { SaveOfferEditorPayload } from "@/app/admin/offers/actions";
import {
saveOfferEditor,
toggleOfferArchived,
addOfferTag,
removeOfferTag,
renameOfferOption,
createOfferMacro,
} from "@/app/admin/offers/actions";
// ── Test 1: tier_letter Zod validation ───────────────────────────────────────
// saveOfferEditor(macroId, payload) rejects (throws) when payload.tiers contains
// a tier_letter not in ["A","B","C"] — Zod enum validation, defense-in-depth
// alongside the DB CHECK constraint from Plan 01.
async function test1_invalidTierLetter(macroId: string) {
const badPayload = {
internal_name: "Test",
public_name: "Test",
tiers: [
{
tier_letter: "D", // invalid — not in ["A","B","C"]
internal_name: "Tier X",
public_name: "Tier X",
duration_months: 3,
assignedServiceIds: [],
},
],
tipoTags: [],
obiettivoTags: [],
} as unknown as SaveOfferEditorPayload;
// Expected: saveOfferEditor(macroId, badPayload) throws (Zod enum mismatch
// surfaces as parsed.error.issues[0].message before any DB write).
return () => saveOfferEditor(macroId, badPayload);
}
// ── Test 2: macro scalar field update ────────────────────────────────────────
// saveOfferEditor updates offer_macros scalars (internal_name, description,
// category, ticket, cliente_ideale, risultato, tempo, pain, metodo) in one
// db.update(offer_macros)...where(eq(id, macroId)).
async function test2_macroScalars(macroId: string) {
const payload: SaveOfferEditorPayload = {
internal_name: "Offerta Test",
public_name: "Offerta Pubblica",
description: "Una descrizione",
category: "Signature Offer",
ticket: "Mid Ticket",
cliente_ideale: "Coach",
risultato: "Lead qualificati",
tempo: "90 giorni",
pain: "Mancanza di processo",
metodo: "Sistema X",
tiers: [],
tipoTags: [],
obiettivoTags: [],
};
// Expected: after call, offer_macros row for macroId has all 9 scalar
// fields set to the payload values.
return () => saveOfferEditor(macroId, payload);
}
// ── Test 3: tier upsert (update existing id, insert when no id) ──────────────
// If tier.id is provided -> db.update(offer_micros).set({...}); if no id ->
// db.insert(offer_micros).values({..., macro_id: macroId}).
async function test3_tierUpsert(macroId: string, existingTierId: string) {
const payload: SaveOfferEditorPayload = {
internal_name: "Offerta Test",
public_name: "Offerta Pubblica",
tiers: [
{
id: existingTierId, // -> update path
tier_letter: "A",
internal_name: "Tier A",
public_name: "Tier A Pubblico",
duration_months: 3,
public_price: 1500,
assignedServiceIds: [],
},
{
// no id -> insert path (supports macros with < 3 tiers)
tier_letter: "B",
internal_name: "Tier B",
public_name: "Tier B Pubblico",
duration_months: 6,
public_price: 2500,
assignedServiceIds: [],
},
],
tipoTags: [],
obiettivoTags: [],
};
// Expected: existingTierId row updated in place; a new offer_micros row
// created for tier B with macro_id = macroId.
return () => saveOfferEditor(macroId, payload);
}
// ── Test 4: offer_tier_services delete-then-reinsert ─────────────────────────
// A tier with assignedServiceIds: ["svc1","svc2"] -> after call,
// offer_tier_services has exactly 2 rows for that tier_id, both svc1/svc2.
async function test4_tierServicesReplace(macroId: string, tierId: string) {
const payload: SaveOfferEditorPayload = {
internal_name: "Offerta Test",
public_name: "Offerta Pubblica",
tiers: [
{
id: tierId,
tier_letter: "A",
internal_name: "Tier A",
public_name: "Tier A Pubblico",
duration_months: 3,
assignedServiceIds: ["svc1", "svc2"],
},
],
tipoTags: [],
obiettivoTags: [],
};
// Expected: offer_tier_services has exactly 2 rows where tier_id === tierId,
// service_id IN ("svc1", "svc2").
return () => saveOfferEditor(macroId, payload);
}
// ── Test 5: Tipo/Obiettivo tags delete-then-reinsert ─────────────────────────
// saveOfferEditor replaces tags where entity_type IN
// ("offer_macros.tipo","offer_macros.obiettivo") and entity_id = macroId.
async function test5_tagsReplace(macroId: string) {
const payload: SaveOfferEditorPayload = {
internal_name: "Offerta Test",
public_name: "Offerta Pubblica",
tiers: [],
tipoTags: ["Audit", "Coaching"],
obiettivoTags: ["Lead Generation"],
};
// Expected: tags table has 2 rows entity_type="offer_macros.tipo" (Audit,
// Coaching) and 1 row entity_type="offer_macros.obiettivo" (Lead
// Generation), all entity_id = macroId; any prior rows for this macroId +
// these two entity_types are removed first.
return () => saveOfferEditor(macroId, payload);
}
// ── Test 6: toggleOfferArchived ──────────────────────────────────────────────
// toggleOfferArchived(macroId, archived: boolean) sets offer_macros.is_archived = archived.
async function test6_toggleArchived(macroId: string) {
// Expected: offer_macros.is_archived === true after toggleOfferArchived(macroId, true);
// === false after toggleOfferArchived(macroId, false).
return [
() => toggleOfferArchived(macroId, true),
() => toggleOfferArchived(macroId, false),
];
}
// ── Test 7: addOfferTag / removeOfferTag ─────────────────────────────────────
// addOfferTag(dimension, macroId, value) / removeOfferTag(dimension, macroId,
// value) work for dimension in ("tipo","obiettivo"). Reject any other
// dimension value.
async function test7_tagCrud(macroId: string) {
// Expected: addOfferTag("tipo", macroId, "Audit") inserts a tags row
// (entity_type="offer_macros.tipo", entity_id=macroId, name="Audit");
// removeOfferTag("tipo", macroId, "Audit") deletes it.
// addOfferTag("invalid" as any, macroId, "x") throws.
return {
addTipo: () => addOfferTag("tipo", macroId, "Audit"),
removeTipo: () => removeOfferTag("tipo", macroId, "Audit"),
addObiettivo: () => addOfferTag("obiettivo", macroId, "Lead Generation"),
removeObiettivo: () => removeOfferTag("obiettivo", macroId, "Lead Generation"),
// @ts-expect-error -- intentional invalid dimension to verify the throw path
invalid: () => addOfferTag("invalid", macroId, "x"),
};
}
// ── Test 8: renameOfferOption ────────────────────────────────────────────────
// renameOfferOption(field, oldValue, newValue) for field in ("categoria",
// "ticket") updates offer_macros.category/ticket for all matching rows; for
// field in ("tipo","obiettivo") updates tags.name.
async function test8_renameOption() {
// Expected:
// renameOfferOption("categoria", "Entry Offer", "Offerta Base") updates
// all offer_macros rows where category = "Entry Offer" -> "Offerta Base"
// renameOfferOption("ticket", "Low Ticket", "Ticket Basso") updates
// all offer_macros rows where ticket = "Low Ticket" -> "Ticket Basso"
// renameOfferOption("tipo", "Audit", "Diagnosi") updates all tags rows
// where entity_type="offer_macros.tipo" and name="Audit" -> "Diagnosi"
// renameOfferOption("obiettivo", "Lead Generation", "Acquisizione Lead")
// updates tags rows where entity_type="offer_macros.obiettivo"
return {
renameCategoria: () => renameOfferOption("categoria", "Entry Offer", "Offerta Base"),
renameTicket: () => renameOfferOption("ticket", "Low Ticket", "Ticket Basso"),
renameTipo: () => renameOfferOption("tipo", "Audit", "Diagnosi"),
renameObiettivo: () => renameOfferOption("obiettivo", "Lead Generation", "Acquisizione Lead"),
};
}
// ── Test 9: createOfferMacro ─────────────────────────────────────────────────
// createOfferMacro(formData) creates a new offer_macros row from internal_name
// (required) + optional public_name/description/category. If public_name is
// omitted, defaults to internal_name.
async function test9_createOfferMacro() {
const fd1 = new FormData();
fd1.append("internal_name", "Nuova Offerta Interna");
fd1.append("description", "Descrizione breve");
fd1.append("category", "Entry Offer");
// Expected: new offer_macros row with internal_name="Nuova Offerta Interna",
// public_name="Nuova Offerta Interna" (defaulted), description set,
// category="Entry Offer".
const fd2 = new FormData();
fd2.append("internal_name", "Altra Offerta");
fd2.append("public_name", "Nome Pubblico Esplicito");
// Expected: new offer_macros row with public_name="Nome Pubblico Esplicito"
// (not defaulted, since explicitly provided).
return {
withDefaultPublicName: () => createOfferMacro(fd1),
withExplicitPublicName: () => createOfferMacro(fd2),
};
}
// Not executed automatically (production DB + no request context for
// requireAdmin) — typecheck-only verification.
export {
test1_invalidTierLetter,
test2_macroScalars,
test3_tierUpsert,
test4_tierServicesReplace,
test5_tagsReplace,
test6_toggleArchived,
test7_tagCrud,
test8_renameOption,
test9_createOfferMacro,
};
-112
View File
@@ -1,112 +0,0 @@
// Phase 12 Plan 03 — Task 1 verification script for offer-queries.ts additions.
//
// This project has no test runner configured (no `scripts.test` in package.json),
// and `.env.local`'s DATABASE_URL points at PRODUCTION (per project memory) — so
// this script does NOT execute any DB calls. It typechecks against the real
// exports/types from src/lib/offer-queries.ts and documents the 5 expected
// behaviors from the plan's <behavior> block, for manual run against a dev DB
// later if desired.
//
// Run (manual, dev DB only): npx tsx scripts/verify-12-03-queries.ts
import {
getOfferEditorData,
getOfferListCards,
getOfferFieldOptions,
type OfferEditorData,
type OfferListCard,
type OfferFieldOptions,
type OfferTierData,
} from "@/lib/offer-queries";
// ── Test 1: getOfferListCards() ──────────────────────────────────────────────
// Input: 2 offer_macros rows (1 with is_archived=true, 1 with is_archived=false).
// Expected: array of length 2, both present, ordered by sort_order, each row
// shaped { id, internal_name, description, category, is_archived }. Archived
// rows ARE returned — filtering is client-side (UI-SPEC "Mostra offerte archiviate").
async function test1_getOfferListCards() {
const cards: OfferListCard[] = await getOfferListCards();
// Expected assertions (manual):
// cards.length === 2
// cards.some(c => c.is_archived === true)
// cards.some(c => c.is_archived === false)
return cards;
}
// ── Test 2: getOfferEditorData(macroId) — macro with 0 tiers ─────────────────
// Input: macro with category = "Signature Offer", 0 offer_micros rows, 3
// services with category="Signature Offer" + 2 with a different category.
// Expected: { macro: {...}, tiers: [], availableServices: [...3 matching...],
// tipoTags: [], obiettivoTags: [] }
async function test2_emptyTiers(macroId: string) {
const data: OfferEditorData | null = await getOfferEditorData(macroId);
// Expected assertions (manual):
// data !== null
// data.tiers.length === 0
// data.availableServices.length === 3 (only services.category === macro.category)
// data.tipoTags.length === 0 && data.obiettivoTags.length === 0
return data;
}
// ── Test 3: getOfferEditorData(macroId) — 3 tiers, tier A has 2 services ─────
// Input: macro with 3 offer_micros rows (tier_letter A/B/C) and 2
// offer_tier_services rows assigned to tier A.
// Expected: tiers sorted A->B->C; each tier has
// { id, tier_letter, public_price, assignedServiceIds, servicesTotal };
// tier A's servicesTotal === sum(unit_price) of its 2 assigned services;
// tiers B/C have assignedServiceIds === [] and servicesTotal === "0".
async function test3_tiersWithServices(macroId: string) {
const data: OfferEditorData | null = await getOfferEditorData(macroId);
// Expected assertions (manual):
// data.tiers.map(t => t.tier_letter) === ["A", "B", "C"]
// data.tiers[0].assignedServiceIds.length === 2
// Number(data.tiers[0].servicesTotal) === sum of the 2 services' unit_price
// data.tiers[1].assignedServiceIds === [] && data.tiers[1].servicesTotal === "0"
// data.tiers[2].assignedServiceIds === [] && data.tiers[2].servicesTotal === "0"
const tierA: OfferTierData | undefined = data?.tiers[0];
return { data, tierA };
}
// ── Test 4: getOfferEditorData(macroId) — tipoTags/obiettivoTags ─────────────
// Input: 2 "tipo" tags + 1 "obiettivo" tag exist for the macro (entity_type =
// "offer_macros.tipo" / "offer_macros.obiettivo", entity_id = macroId).
// Expected: tipoTags.length === 2, obiettivoTags.length === 1.
async function test4_tags(macroId: string) {
const data: OfferEditorData | null = await getOfferEditorData(macroId);
// Expected assertions (manual):
// data?.tipoTags.length === 2
// data?.obiettivoTags.length === 1
return data;
}
// ── Test 5: getOfferFieldOptions() ───────────────────────────────────────────
// Input: 2 macros with category "Entry Offer"/"Signature Offer", 1 "tipo" tag
// "Audit", 1 "obiettivo" tag "Lead Generation".
// Expected: categoria contains both values; tipo === ["Audit"];
// obiettivo === ["Lead Generation"].
async function test5_fieldOptions() {
const options: OfferFieldOptions = await getOfferFieldOptions();
// Expected assertions (manual):
// options.categoria includes "Entry Offer" and "Signature Offer"
// options.tipo includes "Audit"
// options.obiettivo includes "Lead Generation"
return options;
}
// Not executed automatically (production DB) — typecheck-only verification.
// To run manually against a dev DB: uncomment the call below.
// void (async () => {
// await test1_getOfferListCards();
// await test2_emptyTiers("macro-id");
// await test3_tiersWithServices("macro-id");
// await test4_tags("macro-id");
// await test5_fieldOptions();
// })();
export {
test1_getOfferListCards,
test2_emptyTiers,
test3_tiersWithServices,
test4_tags,
test5_fieldOptions,
};