diff --git a/src/db/schema.ts b/src/db/schema.ts index bde926b..5079418 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -259,6 +259,10 @@ export const settings = pgTable("settings", { }); // ============ OFFER MACROS ============ +// Phase 12 additive columns (Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico): +// description, category, ticket, is_archived, and the 5 structured +// transformation-promise fields (cliente_ideale/risultato/tempo/pain/metodo). +// transformation_promise (legacy, free-text) stays untouched. export const offer_macros = pgTable("offer_macros", { id: text("id") .primaryKey() @@ -266,6 +270,16 @@ export const offer_macros = pgTable("offer_macros", { internal_name: text("internal_name").notNull(), public_name: text("public_name").notNull(), transformation_promise: text("transformation_promise"), + // Phase 12 additive fields below + description: text("description"), // short description shown on offer cards + category: text("category"), // single-select "Categoria" (Entry/Signature/Retainer Offer) — OFFER-15/18 + ticket: text("ticket"), // single-select "Ticket" (Low/Mid/High Ticket) — OFFER-15 + is_archived: boolean("is_archived").notNull().default(false), // OFFER-18 archive flag + cliente_ideale: text("cliente_ideale"), // "Aiuto: [Cliente Ideale]" + risultato: text("risultato"), // "A ottenere: [Risultato]" + tempo: text("tempo"), // "In: [tempo]" + pain: text("pain"), // "Senza: [Pain]" + metodo: text("metodo"), // "Grazie a: [Metodo]" sort_order: integer("sort_order").notNull().default(0), created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); @@ -283,6 +297,15 @@ export const offer_micros = pgTable("offer_micros", { transformation_promise: text("transformation_promise"), duration_months: integer("duration_months").notNull().default(1), sort_order: integer("sort_order").notNull().default(0), + // Phase 12 additive fields below. + // tier_letter: nullable, values constrained to 'A'|'B'|'C' via a CHECK + // constraint at the SQL level (migration 0008) — not enforced in Drizzle + // (no native CHECK helper in this project's pg-core version); validated in + // Zod at the server-action layer (Plan 03). + tier_letter: text("tier_letter"), + // public_price: manual public price per tier (D-5/OFFER-16), independent of + // the computed services total (computed at query time, never stored). + public_price: numeric("public_price", { precision: 10, scale: 2 }), }); // ============ OFFER SERVICES (distinct from service_catalog — marketing/transformation semantics) ============ @@ -312,6 +335,28 @@ export const offer_micro_services = pgTable( }) ); +// ============ OFFER TIER SERVICES (Phase 12 — junction: offer_micros <-> services) ============ +// Additive replacement for the legacy offer_micro_services (which points to the +// deprecated offer_services table). This junction connects a tier (offer_micros row +// with tier_letter set) to the unified services catalog (Phase 11). Do NOT modify +// offer_micro_services — it remains untouched for legacy data. +export const offer_tier_services = pgTable( + "offer_tier_services", + { + tier_id: text("tier_id") + .notNull() + .references(() => offer_micros.id, { onDelete: "cascade" }), + service_id: text("service_id") + .notNull() + .references(() => services.id, { onDelete: "cascade" }), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (t) => ({ + pk: primaryKey({ columns: [t.tier_id, t.service_id] }), + tierIdx: index("offer_tier_services_tier_idx").on(t.tier_id), + }) +); + // ============ PROJECT OFFERS (assignment of micro-offer to project) ============ export const project_offers = pgTable("project_offers", { id: text("id") @@ -549,6 +594,7 @@ export const offerMacrosRelations = relations(offer_macros, ({ many }) => ({ export const offerMicrosRelations = relations(offer_micros, ({ one, many }) => ({ macro: one(offer_macros, { fields: [offer_micros.macro_id], references: [offer_macros.id] }), services: many(offer_micro_services), + tierServices: many(offer_tier_services), projectOffers: many(project_offers), })); @@ -561,6 +607,11 @@ export const offerMicroServicesRelations = relations(offer_micro_services, ({ on service: one(offer_services, { fields: [offer_micro_services.service_id], references: [offer_services.id] }), })); +export const offerTierServicesRelations = relations(offer_tier_services, ({ one }) => ({ + tier: one(offer_micros, { fields: [offer_tier_services.tier_id], references: [offer_micros.id] }), + service: one(services, { fields: [offer_tier_services.service_id], references: [services.id] }), +})); + export const projectOffersRelations = relations(project_offers, ({ one }) => ({ project: one(projects, { fields: [project_offers.project_id], references: [projects.id] }), micro: one(offer_micros, { fields: [project_offers.micro_id], references: [offer_micros.id] }), @@ -638,6 +689,8 @@ export type OfferService = typeof offer_services.$inferSelect; export type NewOfferService = typeof offer_services.$inferInsert; export type OfferMicroService = typeof offer_micro_services.$inferSelect; export type NewOfferMicroService = typeof offer_micro_services.$inferInsert; +export type OfferTierService = typeof offer_tier_services.$inferSelect; +export type NewOfferTierService = typeof offer_tier_services.$inferInsert; export type ProjectOffer = typeof project_offers.$inferSelect; export type NewProjectOffer = typeof project_offers.$inferInsert; export type OfferPhase = typeof offer_phases.$inferSelect;