// 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 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, };