---
phase: 12-offer-composition-drag-drop-csv-import
plan: 03
subsystem: api
tags: [drizzle, postgres, zod, server-actions, next.js]

# Dependency graph
requires:
  - phase: 12-offer-composition-drag-drop-csv-import (Plan 01)
    provides: Additive offer_macros/offer_micros columns (category, ticket, is_archived, transformation-promise fields, tier_letter, public_price) and offer_tier_services junction table, applied to prod via Plan 02
provides:
  - getOfferEditorData(macroId) — full editor data shape (macro fields, A/B/C tiers with assignedServiceIds + computed servicesTotal, category-filtered availableServices, tipoTags/obiettivoTags)
  - getOfferListCards() — list-page cards with category + archive status
  - getOfferFieldOptions() — categoria/ticket/tipo/obiettivo select pools
  - saveOfferEditor(macroId, payload) — single-call persistence of macro scalars, per-tier upsert + offer_tier_services replace, and Tipo/Obiettivo tag replace
  - toggleOfferArchived, addOfferTag, removeOfferTag, renameOfferOption, createOfferMacro server actions
affects: [12-04-offer-list-page, 12-05-offer-editor-page]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Polymorphic tags table reused for offer_macros Tipo/Obiettivo dimensions via entity_type = 'offer_macros.tipo' | 'offer_macros.obiettivo', mirroring the Phase 11 services/leads D-06 pattern"
    - "Computed servicesTotal via coalesce(sum(unit_price::numeric), 0) joined through offer_tier_services -> services, mirroring getCatalogWithMicros's cumulMap pattern"
    - "Delete-then-reinsert for many-to-many replacement (offer_tier_services per tier, Tipo/Obiettivo tags per macro), mirroring updateMicroOfferServices"
    - "Zod enum tier_letter validation (A/B/C) as defense-in-depth alongside the Plan 01 DB CHECK constraint"

key-files:
  created:
    - scripts/verify-12-03-queries.ts
    - scripts/verify-12-03-actions.ts
  modified:
    - src/lib/offer-queries.ts
    - src/app/admin/offers/actions.ts

key-decisions:
  - "No test runner configured (no scripts.test in package.json) — wrote typecheck-only verification scripts documenting the 14 spec'd test cases (5 query + 9 action), per plan's explicit fallback; not executed against prod DB"
  - "saveOfferEditor uses sequential awaits (no transaction wrapper) per macro update -> tier upserts -> tag replace, matching the existing per-statement pattern in this codebase (no interactive tx helper elsewhere)"

patterns-established:
  - "OFFER_TIPO_ENTITY/OFFER_OBIETTIVO_ENTITY constants ('offer_macros.tipo'/'offer_macros.obiettivo') as the canonical entity_type values for offer-level tag dimensions — reuse in Plans 04/05 UI"

requirements-completed: [OFFER-11, OFFER-15, OFFER-16, OFFER-17, OFFER-18]

# Metrics
duration: 12min
completed: 2026-06-15
---

# Phase 12 Plan 03: Offer Editor Query Layer & Server Actions Summary

**Query layer (`getOfferEditorData`, `getOfferListCards`, `getOfferFieldOptions`) and server actions (`saveOfferEditor` + archive/tag/rename/create actions) giving Plans 04/05 a complete, type-safe, admin-only data contract for the offer editor.**

## Performance

- **Duration:** 12 min
- **Started:** 2026-06-15T08:15:00Z (approx)
- **Completed:** 2026-06-15T08:17:07Z
- **Tasks:** 2 completed
- **Files modified:** 4 (2 source, 2 new verification scripts)

## Accomplishments

- Added `getOfferListCards()` to `src/lib/offer-queries.ts`: returns `{ id, internal_name, description, category, is_archived }` per `offer_macros` row, ordered by `sort_order`, including archived offers (client-side filter per UI-SPEC)
- Added `getOfferEditorData(macroId)`: returns `{ macro, tiers, availableServices, tipoTags, obiettivoTags }` — tiers ordered A→B→C via SQL `CASE` expression, each with `assignedServiceIds` and a computed `servicesTotal` (`coalesce(sum(unit_price::numeric), 0)` joined through `offer_tier_services`), available services pre-filtered by `services.category === macro.category` (D-4, best-effort when category unset)
- Added `getOfferFieldOptions()`: `{ categoria, ticket, tipo, obiettivo }` distinct-value pools, mirroring `getCatalogFieldOptions`'s null-filter + alphabetical sort pattern
- Added `saveOfferEditor(macroId, payload)` to `src/app/admin/offers/actions.ts`: Zod-validates the full editor payload (`tier_letter` restricted to `["A","B","C"]`), updates `offer_macros` scalars, upserts each tier (update if `id` present, insert + `.returning({id})` if not), delete-then-reinserts `offer_tier_services` per tier, and delete-then-reinserts Tipo/Obiettivo `tags`
- Added `toggleOfferArchived`, `addOfferTag`/`removeOfferTag` (tipo/obiettivo dimensions), `renameOfferOption` (categoria/ticket/tipo/obiettivo), and `createOfferMacro` (Plan 04 "+ Nuova Offerta" target, defaults `public_name` to `internal_name`)
- All new actions call `requireAdmin()` first; existing exports in both files left untouched

## Task Commits

Each task was committed atomically:

1. **Task 1: Query layer — getOfferEditorData, getOfferListCards, getOfferFieldOptions** - `0d742f2` (feat)
2. **Task 2: Server actions — saveOfferEditor, toggleOfferArchived, offer tag CRUD, createOfferMacro** - `a372c61` (feat)

**Plan metadata:** (this commit, follows)

## Files Created/Modified

- `src/lib/offer-queries.ts` - Added `getOfferListCards`, `getOfferEditorData`, `getOfferFieldOptions` + `OfferListCard`/`OfferTierData`/`OfferEditorData`/`OfferFieldOptions` types; imports `offer_tier_services`, `services`, `tags`, `and` from drizzle-orm
- `src/app/admin/offers/actions.ts` - Added `saveOfferEditor`, `toggleOfferArchived`, `addOfferTag`, `removeOfferTag`, `renameOfferOption`, `createOfferMacro` + `SaveOfferEditorPayload`/`tierSchema`/`saveOfferEditorSchema` types; imports `offer_tier_services`, `tags`, `and`, `inArray`
- `scripts/verify-12-03-queries.ts` - Typecheck-only documentation of the 5 query-layer test cases (Tests 1-5 from Task 1's `<behavior>`)
- `scripts/verify-12-03-actions.ts` - Typecheck-only documentation of the 9 server-action test cases (Tests 1-9 from Task 2's `<behavior>`)

## Decisions Made

- No test runner configured in this repo (`package.json` has no `scripts.test`), and `.env.local`'s `DATABASE_URL` points at production per project memory — followed the plan's explicit fallback and wrote `scripts/verify-12-03-queries.ts` / `scripts/verify-12-03-actions.ts` as typecheck-only documentation of all 14 spec'd test cases, not executed against the live DB
- `saveOfferEditor` uses sequential `await`s (macro update → per-tier upsert/replace → tag replace) with no transaction wrapper, matching the existing per-statement pattern used by `updateMicroOfferServices` and the rest of this codebase

## Deviations from Plan

None - plan executed exactly as written. Both tasks implemented the documented function signatures, return shapes, and validation rules verbatim; existing exports in `offer-queries.ts` and `actions.ts` were not modified.

## Issues Encountered

None.

## User Setup Required

None - no external service configuration required. This plan is pure source-code (query/action functions); migration 0008 was already live on production (verified per Plan 02), and no data-mutating scripts were run.

## Next Phase Readiness

- Plan 04 (offer list page) can call `getOfferListCards()` and `createOfferMacro` for the "+ Nuova Offerta" button
- Plan 05 (offer editor page) can call `getOfferEditorData(macroId)` to populate the full editor (macro fields, tier matrix with live totals, Tipo/Obiettivo tags, category-filtered service catalog) and persist all of it via a single `saveOfferEditor(macroId, payload)` call; archive toggling via `toggleOfferArchived`; tag/option CRUD via `addOfferTag`/`removeOfferTag`/`renameOfferOption`
- `npx tsc --noEmit` passes with zero errors across the full project
- No legacy `offer_micro_services`/`offer_services`/existing-export code paths were touched

---

*Phase: 12-offer-composition-drag-drop-csv-import*
*Completed: 2026-06-15*

## Self-Check: PASSED

All created/modified files verified present on disk:

- FOUND: src/lib/offer-queries.ts
- FOUND: src/app/admin/offers/actions.ts
- FOUND: scripts/verify-12-03-queries.ts
- FOUND: scripts/verify-12-03-actions.ts
- FOUND: .planning/phases/12-offer-composition-drag-drop-csv-import/12-03-SUMMARY.md

All task commits verified present in git log:

- FOUND: 0d742f2 (Task 1)
- FOUND: a372c61 (Task 2)
