feat: migrate and seed pricing config + demo catalog

This commit is contained in:
Carlos Narro
2026-05-30 12:31:58 +02:00
parent afef9f2cb0
commit 892c257182
4 changed files with 937 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
CREATE TYPE "public"."calidad" AS ENUM('basica', 'media', 'premium');--> statement-breakpoint
CREATE TYPE "public"."categoria_material" AS ENUM('suelo', 'pared', 'pintura', 'mobiliario');--> statement-breakpoint
CREATE TYPE "public"."unidad_medida" AS ENUM('m2', 'ml', 'ud');--> statement-breakpoint
CREATE TABLE "catalog_items" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"categoria" "categoria_material" NOT NULL,
"nombre" text NOT NULL,
"calidad" "calidad" NOT NULL,
"precio_unit" integer NOT NULL,
"unidad" "unidad_medida" NOT NULL,
"descriptor_render" text DEFAULT '' NOT NULL,
"es_default" boolean DEFAULT false NOT NULL,
"sku" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pricing_config" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"altura_techo_default" double precision DEFAULT 2.5 NOT NULL,
"factor_zona" jsonb DEFAULT '{}'::jsonb NOT NULL,
"mano_obra" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "pricing_config_tenant_id_unique" UNIQUE("tenant_id")
);
--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "m2_suelo" double precision;--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "altura_techo" double precision;--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "calidad_global" "calidad";--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "estructural" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "material_selections" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "leads" ADD COLUMN "desglose_snapshot" jsonb;--> statement-breakpoint
ALTER TABLE "catalog_items" ADD CONSTRAINT "catalog_items_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pricing_config" ADD CONSTRAINT "pricing_config_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "catalog_tenant_idx" ON "catalog_items" USING btree ("tenant_id");--> statement-breakpoint
CREATE UNIQUE INDEX "catalog_tenant_sku_idx" ON "catalog_items" USING btree ("tenant_id","sku");

View File

@@ -0,0 +1,834 @@
{
"id": "57e8d006-18f6-4aba-a61a-02d155a80bbc",
"prevId": "66acce06-f292-49db-adc1-fa9cfcc7d2a9",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.catalog_items": {
"name": "catalog_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"categoria": {
"name": "categoria",
"type": "categoria_material",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"nombre": {
"name": "nombre",
"type": "text",
"primaryKey": false,
"notNull": true
},
"calidad": {
"name": "calidad",
"type": "calidad",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"precio_unit": {
"name": "precio_unit",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"unidad": {
"name": "unidad",
"type": "unidad_medida",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"descriptor_render": {
"name": "descriptor_render",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"es_default": {
"name": "es_default",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"sku": {
"name": "sku",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"catalog_tenant_idx": {
"name": "catalog_tenant_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"catalog_tenant_sku_idx": {
"name": "catalog_tenant_sku_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "sku",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"catalog_items_tenant_id_tenants_id_fk": {
"name": "catalog_items_tenant_id_tenants_id_fk",
"tableFrom": "catalog_items",
"tableTo": "tenants",
"columnsFrom": [
"tenant_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.lead_estado_history": {
"name": "lead_estado_history",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"estado": {
"name": "estado",
"type": "lead_estado",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"changed_at": {
"name": "changed_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"changed_by": {
"name": "changed_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"lead_estado_history_lead_id_leads_id_fk": {
"name": "lead_estado_history_lead_id_leads_id_fk",
"tableFrom": "lead_estado_history",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.lead_fotos": {
"name": "lead_fotos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"orden": {
"name": "orden",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"lead_fotos_lead_id_leads_id_fk": {
"name": "lead_fotos_lead_id_leads_id_fk",
"tableFrom": "lead_fotos",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.lead_pipeline_eventos": {
"name": "lead_pipeline_eventos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stage": {
"name": "stage",
"type": "pipeline_stage",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"occurred_at": {
"name": "occurred_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"lead_pipeline_eventos_lead_id_leads_id_fk": {
"name": "lead_pipeline_eventos_lead_id_leads_id_fk",
"tableFrom": "lead_pipeline_eventos",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.leads": {
"name": "leads",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"nombre": {
"name": "nombre",
"type": "text",
"primaryKey": false,
"notNull": true
},
"telefono": {
"name": "telefono",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provincia": {
"name": "provincia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"tipo_reforma": {
"name": "tipo_reforma",
"type": "tipo_reforma",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"consent_privacidad": {
"name": "consent_privacidad",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"consent_contratacion": {
"name": "consent_contratacion",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"pipeline_stage": {
"name": "pipeline_stage",
"type": "pipeline_stage",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'form_completado'"
},
"estado": {
"name": "estado",
"type": "lead_estado",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'nuevo'"
},
"presupuesto_estimado": {
"name": "presupuesto_estimado",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"transcripcion": {
"name": "transcripcion",
"type": "text",
"primaryKey": false,
"notNull": false
},
"entidades": {
"name": "entidades",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"render_url": {
"name": "render_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"pdf_url": {
"name": "pdf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"audio_url": {
"name": "audio_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"notas": {
"name": "notas",
"type": "text",
"primaryKey": false,
"notNull": false
},
"m2_suelo": {
"name": "m2_suelo",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"altura_techo": {
"name": "altura_techo",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"calidad_global": {
"name": "calidad_global",
"type": "calidad",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"estructural": {
"name": "estructural",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"material_selections": {
"name": "material_selections",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"desglose_snapshot": {
"name": "desglose_snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"leads_tenant_created_idx": {
"name": "leads_tenant_created_idx",
"columns": [
{
"expression": "tenant_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"leads_estado_idx": {
"name": "leads_estado_idx",
"columns": [
{
"expression": "estado",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"leads_tenant_id_tenants_id_fk": {
"name": "leads_tenant_id_tenants_id_fk",
"tableFrom": "leads",
"tableTo": "tenants",
"columnsFrom": [
"tenant_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.precision_history": {
"name": "precision_history",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"lead_id": {
"name": "lead_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"estimated": {
"name": "estimated",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"final": {
"name": "final",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"delta_pct": {
"name": "delta_pct",
"type": "numeric(6, 2)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"precision_history_lead_id_leads_id_fk": {
"name": "precision_history_lead_id_leads_id_fk",
"tableFrom": "precision_history",
"tableTo": "leads",
"columnsFrom": [
"lead_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.pricing_config": {
"name": "pricing_config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tenant_id": {
"name": "tenant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"altura_techo_default": {
"name": "altura_techo_default",
"type": "double precision",
"primaryKey": false,
"notNull": true,
"default": 2.5
},
"factor_zona": {
"name": "factor_zona",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"mano_obra": {
"name": "mano_obra",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"pricing_config_tenant_id_tenants_id_fk": {
"name": "pricing_config_tenant_id_tenants_id_fk",
"tableFrom": "pricing_config",
"tableTo": "tenants",
"columnsFrom": [
"tenant_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"pricing_config_tenant_id_unique": {
"name": "pricing_config_tenant_id_unique",
"nullsNotDistinct": false,
"columns": [
"tenant_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.tenants": {
"name": "tenants",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true
},
"nombre_empresa": {
"name": "nombre_empresa",
"type": "text",
"primaryKey": false,
"notNull": true
},
"logo_url": {
"name": "logo_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"provincia": {
"name": "provincia",
"type": "text",
"primaryKey": false,
"notNull": false
},
"whatsapp_business": {
"name": "whatsapp_business",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"tenants_slug_unique": {
"name": "tenants_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.calidad": {
"name": "calidad",
"schema": "public",
"values": [
"basica",
"media",
"premium"
]
},
"public.categoria_material": {
"name": "categoria_material",
"schema": "public",
"values": [
"suelo",
"pared",
"pintura",
"mobiliario"
]
},
"public.lead_estado": {
"name": "lead_estado",
"schema": "public",
"values": [
"nuevo",
"contactado",
"visita_agendada",
"presupuesto_enviado",
"ganado",
"perdido"
]
},
"public.pipeline_stage": {
"name": "pipeline_stage",
"schema": "public",
"values": [
"form_completado",
"fotos_subidas",
"prellamada_enviada",
"llamada_completada",
"render_generado",
"presupuesto_generado",
"whatsapp_entregado"
]
},
"public.tipo_reforma": {
"name": "tipo_reforma",
"schema": "public",
"values": [
"cocina",
"bano",
"salon",
"comedor",
"integral",
"otro"
]
},
"public.unidad_medida": {
"name": "unidad_medida",
"schema": "public",
"values": [
"m2",
"ml",
"ud"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1780056789929, "when": 1780056789929,
"tag": "0000_motionless_jackpot", "tag": "0000_motionless_jackpot",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1780137082579,
"tag": "0001_bored_preak",
"breakpoints": true
} }
] ]
} }

View File

@@ -363,6 +363,66 @@ async function main() {
} }
} }
// --- Precios + catálogo demo (motor de presupuesto) ---
const [tenantRow] = await db
.select()
.from(schema.tenants)
.where(eq(schema.tenants.slug, 'reformas-ejemplo'))
.limit(1);
if (tenantRow) {
await db.delete(schema.catalogItems).where(eq(schema.catalogItems.tenantId, tenantRow.id));
await db.delete(schema.pricingConfig).where(eq(schema.pricingConfig.tenantId, tenantRow.id));
await db.insert(schema.pricingConfig).values({
tenantId: tenantRow.id,
alturaTechoDefault: 2.5,
factorZona: { Madrid: 1.1, Barcelona: 1.15, Valencia: 1.0, Sevilla: 0.95 },
manoObra: { demolicion: 1800, fontaneria: 2200, electricidad: 1600, mano_de_obra: 3500 },
});
const cat = (
categoria: 'suelo' | 'pared' | 'pintura' | 'mobiliario',
nombre: string,
calidad: 'basica' | 'media' | 'premium',
precioEuros: number,
unidad: 'm2' | 'ml' | 'ud',
descriptorRender: string,
sku: string,
) => ({
tenantId: tenantRow.id,
categoria,
nombre,
calidad,
precioUnit: Math.round(precioEuros * 100),
unidad,
descriptorRender,
esDefault: true,
sku,
});
await db.insert(schema.catalogItems).values([
cat('suelo', 'Gres cerámico básico', 'basica', 16, 'm2', 'suelo gres beige liso', 'SUE-B'),
cat('suelo', 'Porcelánico símil madera', 'media', 28, 'm2', 'porcelánico símil roble claro', 'SUE-M'),
cat('suelo', 'Porcelánico gran formato', 'premium', 48, 'm2', 'porcelánico gran formato gris piedra', 'SUE-P'),
cat('pared', 'Azulejo blanco brillo', 'basica', 14, 'm2', 'azulejo blanco brillo 20x20', 'PAR-B'),
cat('pared', 'Azulejo rectificado', 'media', 24, 'm2', 'azulejo rectificado blanco mate 30x60', 'PAR-M'),
cat('pared', 'Porcelánico decorativo', 'premium', 42, 'm2', 'porcelánico decorativo símil mármol', 'PAR-P'),
cat('pintura', 'Plástica mate', 'basica', 6, 'm2', 'pintura plástica blanca mate', 'PIN-B'),
cat('pintura', 'Plástica lavable', 'media', 9, 'm2', 'pintura lavable blanco roto', 'PIN-M'),
cat('pintura', 'Esmalte premium', 'premium', 14, 'm2', 'esmalte al agua acabado seda gris perla', 'PIN-P'),
cat('mobiliario', 'Muebles melamina', 'basica', 180, 'ml', 'muebles cocina melamina blanca', 'MOB-B'),
cat('mobiliario', 'Muebles laminado', 'media', 320, 'ml', 'muebles cocina laminado roble con tirador integrado', 'MOB-M'),
cat('mobiliario', 'Muebles lacado', 'premium', 550, 'ml', 'muebles cocina lacado mate antracita y encimera porcelánica', 'MOB-P'),
]);
// Inputs demo en un lead ya avanzado para poder recalcular su presupuesto.
await db
.update(schema.leads)
.set({ m2Suelo: 12, calidadGlobal: 'media', estructural: false })
.where(eq(schema.leads.email, 'roberto.salas@example.com'));
}
console.log('Seed completado.'); console.log('Seed completado.');
await client.end(); await client.end();
} }