chore: merge executor worktree (worktree-agent-ab7d5b1ee10efadff)
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
phase: 12-offer-composition-drag-drop-csv-import
|
||||||
|
plan: 01
|
||||||
|
subsystem: database
|
||||||
|
tags: [drizzle, postgres, schema, migration]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 11-catalog-database-view-ux
|
||||||
|
provides: unified `services` catalog table (category/fase single-select pattern, migrated_from audit trail)
|
||||||
|
provides:
|
||||||
|
- Additive Drizzle schema columns on offer_macros (description, category, ticket, is_archived, cliente_ideale, risultato, tempo, pain, metodo)
|
||||||
|
- Additive Drizzle schema columns on offer_micros (tier_letter, public_price)
|
||||||
|
- New offer_tier_services junction table + relations + types (offer_micros <-> services)
|
||||||
|
- Hand-written migration 0008_offer_tier_schema.sql (additive/idempotent, CHECK constraint on tier_letter)
|
||||||
|
- Idempotent push script scripts/push-12-offer-tier-schema.ts for Plan 02's BLOCKING prod-apply step
|
||||||
|
affects: [12-02-prod-migration-apply, 12-03-offer-list-editor, 12-04-tier-matrix, 12-05-offer-detail-actions]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Additive-only schema extension via hand-written SQL migration (drizzle-kit generate non-functional in this repo)"
|
||||||
|
- "Single-select dimension columns (category/ticket) on offer_macros, mirroring services.category/fase pattern from Phase 11"
|
||||||
|
- "New junction table (offer_tier_services) as additive replacement for legacy junction (offer_micro_services), pointing at unified services catalog instead of deprecated offer_services"
|
||||||
|
- "DB-level CHECK constraint via guarded DO block for idempotent re-runs, with Zod validation deferred to server-action layer (Plan 03)"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/db/migrations/0008_offer_tier_schema.sql
|
||||||
|
- scripts/push-12-offer-tier-schema.ts
|
||||||
|
modified:
|
||||||
|
- src/db/schema.ts
|
||||||
|
- src/db/migrations/meta/_journal.json
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "tier_letter constrained via SQL CHECK (guarded DO block) rather than Drizzle enum, since this pg-core version has no native CHECK helper — Zod enum validation is deferred to Plan 03's server-action layer as defense-in-depth"
|
||||||
|
- "offer_tier_services created as a brand-new junction (not a modification of offer_micro_services) to avoid touching legacy offer_services-linked data, per D-6"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Phase 12 additive columns documented inline with comment blocks above offer_macros/offer_micros, following the services.migrated_from comment style from Phase 11"
|
||||||
|
|
||||||
|
requirements-completed: [OFFER-11, OFFER-15, OFFER-16, OFFER-17, OFFER-18]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 6min
|
||||||
|
completed: 2026-06-14
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 12 Plan 01: Offer Tier Schema Foundation Summary
|
||||||
|
|
||||||
|
**Additive Drizzle schema + hand-written migration 0008 adding tier designations (A/B/C), manual public pricing, archive flag, category/ticket dimensions, structured transformation-promise fields, and a new offer_tier_services junction table pointing at the unified services catalog.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 6 min
|
||||||
|
- **Started:** 2026-06-14T19:13:00Z (approx)
|
||||||
|
- **Completed:** 2026-06-14T19:19:38Z
|
||||||
|
- **Tasks:** 2 completed
|
||||||
|
- **Files modified:** 4
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Extended `offer_macros` with 9 additive columns: `description`, `category`, `ticket`, `is_archived`, and 5 structured transformation-promise fields (`cliente_ideale`, `risultato`, `tempo`, `pain`, `metodo`)
|
||||||
|
- Extended `offer_micros` with `tier_letter` (A/B/C, CHECK-constrained at SQL level) and `public_price` (numeric)
|
||||||
|
- Added new `offer_tier_services` junction table (tier <-> unified `services` catalog), its relations, and `OfferTierService`/`NewOfferTierService` types — without touching the legacy `offer_micro_services`/`offer_services` tables
|
||||||
|
- Hand-wrote idempotent, additive-only migration `0008_offer_tier_schema.sql` (11 `ADD COLUMN IF NOT EXISTS` statements, guarded DO-block CHECK constraint, `CREATE TABLE IF NOT EXISTS offer_tier_services` + index), appended journal entry
|
||||||
|
- Created `scripts/push-12-offer-tier-schema.ts` — idempotent push script with PRODUCTION DEPLOY NOTE documenting the BLOCKING manual prod-apply step required before Plan 02/Wave 3
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Task 1: Extend offer_macros and offer_micros, add offer_tier_services junction in schema.ts** - `11d6c11` (feat)
|
||||||
|
2. **Task 2: Hand-write migration 0008 SQL + journal entry + idempotent push script** - `89d15ee` (feat)
|
||||||
|
|
||||||
|
**Plan metadata:** (this commit, follows)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `src/db/schema.ts` - Additive columns on offer_macros/offer_micros, new offer_tier_services table + relations + types
|
||||||
|
- `src/db/migrations/0008_offer_tier_schema.sql` - Hand-written additive/idempotent migration (ALTER TABLE ADD COLUMN IF NOT EXISTS, guarded CHECK constraint, CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS)
|
||||||
|
- `src/db/migrations/meta/_journal.json` - Appended entry for `0008_offer_tier_schema` (idx 8, following 0006 entry shape)
|
||||||
|
- `scripts/push-12-offer-tier-schema.ts` - Idempotent push script for Plan 02's BLOCKING production-apply step
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- `tier_letter` validated via DB-level CHECK constraint (guarded DO block, idempotent) rather than a Drizzle-native enum/CHECK helper (unavailable in this pg-core version); Zod enum validation will be added at the server-action layer in Plan 03 as defense-in-depth (per plan's T-12-02 mitigation)
|
||||||
|
- `offer_tier_services` is a brand-new additive junction table (not a modification of `offer_micro_services`), pointing at the unified `services` catalog (Phase 11) rather than the deprecated `offer_services` table, per D-6/D-3
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written. The acceptance-criteria grep counts for `tier_letter`/`public_price`, the 6 transformation/archive columns, and the DROP/TRUNCATE/RENAME check show slightly higher raw match counts than the plan's exact numbers because explanatory comment blocks (explicitly requested by the plan, e.g. "Add a comment block directly above offer_macros documenting...") also contain those words (e.g. a comment mentions "tier_letter" when describing the CHECK constraint, and the migration's header comment says "No drops/truncates"). All actual code definitions are present exactly once, the diff to `src/db/schema.ts` is purely additive (`git diff HEAD~2 -- src/db/schema.ts | grep '^-'` returns empty), and a line-anchored grep (`grep -inE "^\s*(DROP|TRUNCATE|RENAME)\b"`) confirms zero actual destructive SQL statements in the migration.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
A `cd /Users/simonecavalli/Vault/IAMCAVALLI` in one intermediate Bash call caused a transient cwd drift into the main repo's worktree (sibling path with the same relative structure), which produced false "0 matches" results for one verification pass. Re-ran all verification from the correct worktree root (`/Users/simonecavalli/Vault/IAMCAVALLI/.claude/worktrees/agent-ab7d5b1ee10efadff`) with confirmed correct results before committing. No files were affected — all Edit/Write operations used relative paths inside the worktree.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required. Note: migration 0008 and the push script are created here only; per the plan and project memory, the actual production apply (SSH+docker exec) is Plan 02's BLOCKING step and is NOT performed in this plan.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- `src/db/schema.ts` compiles cleanly (`npx tsc --noEmit` passes, zero errors) and exposes all Phase 12 additive columns/table/types needed by Plan 03's query layer and server actions
|
||||||
|
- Migration 0008 + push script are ready for Plan 02 to apply to production via SSH+docker exec before Wave 3 code reads `offer_tier_services`/`tier_letter`/`public_price`/`category`/`ticket`
|
||||||
|
- No existing table/column was dropped, renamed, or altered destructively — `clients`/`projects`/`payments`/`phases` and legacy offer tables (`offer_micro_services`, `offer_services`) remain byte-identical
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 12-offer-composition-drag-drop-csv-import*
|
||||||
|
*Completed: 2026-06-14*
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
All created/modified files verified present on disk:
|
||||||
|
|
||||||
|
- FOUND: src/db/schema.ts
|
||||||
|
- FOUND: src/db/migrations/0008_offer_tier_schema.sql
|
||||||
|
- FOUND: src/db/migrations/meta/_journal.json
|
||||||
|
- FOUND: scripts/push-12-offer-tier-schema.ts
|
||||||
|
- FOUND: .planning/phases/12-offer-composition-drag-drop-csv-import/12-01-SUMMARY.md
|
||||||
|
|
||||||
|
All task commits verified present in git log:
|
||||||
|
|
||||||
|
- FOUND: 11d6c11 (Task 1)
|
||||||
|
- FOUND: 89d15ee (Task 2)
|
||||||
|
- FOUND: bfc9932 (docs: complete plan)
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// 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();
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
-- Phase 12: Offer Editor — additive schema for tier designations, public pricing,
|
||||||
|
-- archive flag, category/ticket dimensions, structured transformation promise, and
|
||||||
|
-- tier<->services junction. All statements additive/idempotent. No drops/truncates
|
||||||
|
-- (Data Safety LOCKED).
|
||||||
|
|
||||||
|
-- offer_macros: archive flag + short description + category/ticket dimensions +
|
||||||
|
-- structured transformation promise
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS description text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS category text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS ticket text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS is_archived boolean NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS cliente_ideale text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS risultato text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS tempo text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS pain text;
|
||||||
|
ALTER TABLE offer_macros ADD COLUMN IF NOT EXISTS metodo text;
|
||||||
|
|
||||||
|
-- offer_micros: tier designation (A/B/C) + manual public price
|
||||||
|
ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS tier_letter text;
|
||||||
|
ALTER TABLE offer_micros ADD COLUMN IF NOT EXISTS public_price numeric(10, 2);
|
||||||
|
|
||||||
|
-- CHECK constraint for tier_letter, added separately so the ADD COLUMN above stays
|
||||||
|
-- idempotent even if the constraint already exists (guarded via DO block).
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- New junction table: tier (offer_micros) <-> unified services catalog.
|
||||||
|
-- Additive replacement for legacy offer_micro_services (untouched, points to offer_services).
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS offer_tier_services_tier_idx ON offer_tier_services USING btree (tier_id);
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1781442000000,
|
"when": 1781442000000,
|
||||||
"tag": "0006_add_tags_table",
|
"tag": "0006_add_tags_table",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781700000000,
|
||||||
|
"tag": "0008_offer_tier_schema",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -259,6 +259,10 @@ export const settings = pgTable("settings", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============ OFFER MACROS ============
|
// ============ 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", {
|
export const offer_macros = pgTable("offer_macros", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
@@ -266,6 +270,16 @@ export const offer_macros = pgTable("offer_macros", {
|
|||||||
internal_name: text("internal_name").notNull(),
|
internal_name: text("internal_name").notNull(),
|
||||||
public_name: text("public_name").notNull(),
|
public_name: text("public_name").notNull(),
|
||||||
transformation_promise: text("transformation_promise"),
|
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),
|
sort_order: integer("sort_order").notNull().default(0),
|
||||||
created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
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"),
|
transformation_promise: text("transformation_promise"),
|
||||||
duration_months: integer("duration_months").notNull().default(1),
|
duration_months: integer("duration_months").notNull().default(1),
|
||||||
sort_order: integer("sort_order").notNull().default(0),
|
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) ============
|
// ============ 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) ============
|
// ============ PROJECT OFFERS (assignment of micro-offer to project) ============
|
||||||
export const project_offers = pgTable("project_offers", {
|
export const project_offers = pgTable("project_offers", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
@@ -549,6 +594,7 @@ export const offerMacrosRelations = relations(offer_macros, ({ many }) => ({
|
|||||||
export const offerMicrosRelations = relations(offer_micros, ({ one, many }) => ({
|
export const offerMicrosRelations = relations(offer_micros, ({ one, many }) => ({
|
||||||
macro: one(offer_macros, { fields: [offer_micros.macro_id], references: [offer_macros.id] }),
|
macro: one(offer_macros, { fields: [offer_micros.macro_id], references: [offer_macros.id] }),
|
||||||
services: many(offer_micro_services),
|
services: many(offer_micro_services),
|
||||||
|
tierServices: many(offer_tier_services),
|
||||||
projectOffers: many(project_offers),
|
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] }),
|
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 }) => ({
|
export const projectOffersRelations = relations(project_offers, ({ one }) => ({
|
||||||
project: one(projects, { fields: [project_offers.project_id], references: [projects.id] }),
|
project: one(projects, { fields: [project_offers.project_id], references: [projects.id] }),
|
||||||
micro: one(offer_micros, { fields: [project_offers.micro_id], references: [offer_micros.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 NewOfferService = typeof offer_services.$inferInsert;
|
||||||
export type OfferMicroService = typeof offer_micro_services.$inferSelect;
|
export type OfferMicroService = typeof offer_micro_services.$inferSelect;
|
||||||
export type NewOfferMicroService = typeof offer_micro_services.$inferInsert;
|
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 ProjectOffer = typeof project_offers.$inferSelect;
|
||||||
export type NewProjectOffer = typeof project_offers.$inferInsert;
|
export type NewProjectOffer = typeof project_offers.$inferInsert;
|
||||||
export type OfferPhase = typeof offer_phases.$inferSelect;
|
export type OfferPhase = typeof offer_phases.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user