-- ESTRUCTURA DE DATOS · VIAJES MUNDOMANÍA -- Versión de la web: 1.3.6 -- Fecha del contrato: 2026-10-01 -- Dialecto: SQLite 3.31 o posterior. Codificación: UTF-8. -- SOLO ESTRUCTURA: no contiene registros, reservas, clientes, tarifas, -- fotografías, credenciales, claves ni datos de ejemplo. -- -- ALCANCE: contrato documental del modelo actual y su representación SQL. -- Las tablas marcadas ACTUAL reflejan la persistencia ya usada; las marcadas -- DOCUMENTAL formalizan objetos hoy mantenidos en JS, API o navegador. -- Este archivo NO es una migración ni activa nuevas funciones. Se valida -- exclusivamente en una base vacía y no se ejecuta sobre la web en servicio. -- Las reglas y funciones JavaScript/HTTP se reflejan como contratos en -- comentarios junto a sus entidades. SQLite no dispone de CREATE FUNCTION: -- esas funciones siguen ejecutándose en la aplicación, no en este DDL. -- Los mapas de propiedades documentan objetos anidados y campos JSON para -- evitar perder propiedades sin fingir que existe almacenamiento SQL activo. -- Los tipos/nullables describen las fuentes, no garantizan disponibilidad. -- Ninguna solicitud confirma plazas ni cobra por ejecutar este esquema. PRAGMA foreign_keys = ON; BEGIN TRANSACTION; -- ============================================================================ -- 10. SOLICITUDES DE RESERVA, SNAPSHOT, API Y GESTION -- Dialecto: SQLite. Solo estructura; no contiene filas ni datos de clientes. -- ACTIVE = estructura que utiliza el servidor de prueba. -- DOCUMENTAL = contrato de objetos/codigo; no es otra tabla instalada. -- PENDIENTE = codigo existente o capacidad prevista que el servidor no expone. -- Las funciones de negocio son JavaScript/PHP/Python, no funciones SQL. -- Se describen aqui; este fichero no las convierte en procedimientos SQL. -- ============================================================================ -- ACTIVE. Reproduccion exacta de la migracion vigente de booking_requests. -- No se anaden CHECK, FK, triggers ni indices inexistentes en la migracion. -- Las validaciones y los estados admitidos se aplican en el servicio. CREATE TABLE `booking_requests` ( `id` text PRIMARY KEY NOT NULL, `idempotency_key` text NOT NULL, `full_name` text NOT NULL, `email` text NOT NULL, `mobile` text NOT NULL, `snapshot` text NOT NULL, `status` text DEFAULT 'pending' NOT NULL, `email_status` text DEFAULT 'prepared' NOT NULL, `email_html` text NOT NULL, `created_at` text NOT NULL, `updated_at` text NOT NULL, `notes` text DEFAULT '' NOT NULL ); CREATE UNIQUE INDEX `booking_requests_idempotency_key_unique` ON `booking_requests` (`idempotency_key`); CREATE INDEX `idx_booking_requests_created` ON `booking_requests` (`created_at`); -- ACTIVE. Tabla tecnica creada por el adaptador SQLite, fuera de la migracion. -- name: nombre de migracion; digest: SHA-256 del fichero; applied_at: UTC ISO. -- Evita reaplicar migraciones y detecta cambios de una migracion ya aplicada. CREATE TABLE gateway_migrations ( name TEXT PRIMARY KEY, digest TEXT NOT NULL, applied_at TEXT NOT NULL ); -- DICCIONARIO DE booking_requests (todas las columnas): -- id TEXT: referencia generada por el servidor, prefijo MM- y -- 16 caracteres hexadecimales mayusculos de UUID aleatorio. -- idempotency_key TEXT: UUID del intento, validado con patron UUID de 36 -- caracteres; reutilizado al reintentar el mismo formulario. -- full_name TEXT: nombre titular, trim, entre 3 y 120 caracteres; -- rechaza saltos de linea y caracteres de marcado < >. -- email TEXT: trim y minusculas, hasta 254 caracteres, formato -- de contacto validado; no se verifica la entrega del email. -- mobile TEXT: entre 8 y 25 caracteres admitidos, minimo 8 digitos; -- permite prefijo +, parentesis, espacios y guiones. -- snapshot TEXT: JSON de req_snapshot, congelado al solicitar. -- status TEXT: pending | reviewing | confirmed | unavailable | -- cancelled. Alta siempre pending. Sin maquina de transiciones -- ni historial de estados: la gestion sobrescribe el estado. -- email_status TEXT: prepared. Unico estado que escribe la implementacion; -- NO significa enviado. No hay cola de envios instalada. -- email_html TEXT: documento HTML del resumen pendiente de confirmacion; -- generado al alta, conservado en almacenamiento privado. -- created_at TEXT: instante UTC ISO-8601 de creacion. -- updated_at TEXT: instante UTC ISO-8601; coincide al crear y cambia en -- la gestion del estado/notas. El snapshot no se modifica. -- notes TEXT: travelNotes hasta 1500 caracteres al alta; la gestion -- admite hasta 4000. No hay tabla separada de notas. -- DOCUMENTAL req_snapshot: objeto serializado en booking_requests.snapshot. -- No hay FK a ofertas porque estas proceden del catalogo y de cotizadores. -- Mantiene la propuesta completa aunque despues cambie el catalogo. -- Los campos opcionales undefined se omiten por JSON.stringify; no se inventan. -- Campo Tipo JSON Semantica -- quoteId string Identificador de tarifa, own: o travel:. -- offerId string Identificador de oferta o viaje. -- title string Titulo de la oferta solicitada. -- hotel string Hotel o lista de hoteles de etapas. -- photos req_snapshot_photo[] Copia de las fotos, src absoluto. -- checkIn string (YYYY-MM-DD) Fecha de entrada/salida del circuito. -- checkOut string (YYYY-MM-DD) Fecha final segun la propuesta. -- nights integer Numero de noches. -- adults integer Adultos para esta tarifa concreta. -- childAges integer[] Edades seleccionadas, vacio sin menores. -- room string Descripcion de habitacion. -- rooms integer, opcional Numero de habitaciones si la tarifa lo da. -- board string Regimen de alojamiento. -- price number Precio final TOTAL solicitado en EUR. -- sourcePrice number PVP de origen conservado solo en privado; -- supplierPvp, sourcePrice o price, por orden. -- currency string Codigo de moneda (EUR en este servicio). -- includes string[] Inclusiones de la tarifa. -- conditions string[] Condiciones de la tarifa. -- checkedAt string, opcional Momento de comprobacion de la tarifa. -- isCircuit boolean theme de la oferta es circuitos. -- programDays integer, opcional Dias de programa de tarifa u oferta. -- departureCity string, opcional Ciudad de salida del circuito. -- departureAirport string, opcional Codigo de aeropuerto de salida. -- pricePerPerson number, opcional Precio por persona del circuito. -- returnDateDiffers boolean Retorno distinto de la fecha del programa. -- DOCUMENTAL req_snapshot_photo: conserva las propiedades de offer.photos[] -- mediante copia de objeto. Campos observados en catalogo/tarifas propias: -- src, alt, caption, sourceUrl, originalSrc, originalUrl, author, license, -- licenseUrl, kind, credit: string (salvo src/alt, metadatos opcionales). -- src se convierte en URL absoluta usando el origen de la peticion. -- En travelQuote las fotos se simplifican a src y alt. -- No se almacena un binario de imagen dentro de la solicitud. -- DOCUMENTAL req_create_input: POST /api/requests, application/json. -- quoteId string: tarifa existente, o seleccion codificada own:/travel:. -- requestKey string: clave idempotente del intento. -- fullName string: titular; ver reglas de full_name. -- email string: contacto; ver reglas de email. -- mobile string: contacto; ver reglas de mobile. -- website string, opcional: campo trampa; si contiene valor se rechaza. -- acceptedPending boolean: debe aceptarse que no hay reserva/plazas confirmadas. -- acceptedAge boolean, opcional: requerido si quote.minAdultAge existe. -- travelNotes string, opcional: se convierte a string, se limita a 1500. -- No se acepta del navegador un precio arbitrario ni un snapshot como autoridad. -- El formulario habitual envia todos salvo travelNotes; crea requestKey al abrir. -- Evita doble pulsacion; tras error conserva datos y habilita reintento. -- DOCUMENTAL req_create_output: -- reference string: booking_requests.id. -- message string: solicitud pendiente, reconfirmacion por un agente, sin cobro. -- emailStatus string: prepared; documento preparado, no email enviado. -- HTTP 201: alta; HTTP 200: reintento identico previamente guardado. -- Errores JSON: error:string; el puente puede incluir tambien message:string. -- HTTP 400: contacto/aceptaciones/UUID/JSON invalidos; 403: origen prohibido; -- 409: tarifa caducada, no disponible, precio distinto o clave con otros datos; -- 413: cuerpo grande; 415: tipo de contenido; 429: limite; 503: servicio ocupado. -- DOCUMENTAL req_own_selection: POST /api/own-price. -- offerId:string, occupantId:integer, checkIn:YYYY-MM-DD, nights:integer, -- childAges:integer[]. expectedPrice:number se anade en la seleccion codificada -- de quote.id; solo se usa al reconfirmar /api/requests, no como PVP de entrada. -- occupantId mapea: 1=(2 adultos,0 menores), 2=(2,1), 3=(2,2), 4=(3,0), 5=(3,1). -- No hay tarifa inferida para otras ocupaciones ni multiplicacion de precio de -- adulto para inventar precio infantil. -- Respuesta: {quote:req_own_quote, offer:req_own_offer}; error:error:string. -- req_own_quote: id:string, offerId:string, checkIn:string, checkOut:string, -- nights:integer, adults:integer, childAges:integer[], room:string, rooms:integer, -- board:string, price:number, sourcePrice:number PRIVADO, priceUnit:string(total), -- includes:string[], conditions:string[], checkedAt:string UTC. -- req_own_offer: id:string, title:string, hotel:string, photos:photo[]. -- Publicacion filtra sourcePrice y otros metadatos internos de las respuestas. -- DOCUMENTAL req_travel_selection: {trip:objeto del cotizador, -- expectedTotalCents:integer}. quote.id codifica este objeto con prefijo travel:. -- Contratos anidados de trip y de travelBudget: fragmento de cotizadores. -- travelQuote convierte el presupuesto en req_snapshot compatible: 2 adultos, -- sin menores, precio total en euros=totalPriceCents/100, fechas de propuesta, -- hoteles unidos desde etapas y fotos src/alt. Incluye excursiones seleccionadas, -- traslados seleccionados y experiencias pendientes de cotizar en condiciones. -- POST /api/webs/estimar: input del cotizador -> travelBudget; ACTIVA. -- POST /api/webs/reservas: {trip,expectedTotalCents,requestId,accepted, -- contact:{name,email,phone,notes}} -> wrapper /api/requests; PENDIENTE/BLOQUEADA -- en el puente publico actual, aunque la funcion existe en el motor. -- Su error usa message:string; alta teorica devuelve reference/message/emailStatus. -- PENDIENTE/BLOQUEADO EN ESTE SERVIDOR: gestion administrativa que existe en codigo. -- GET /api/admin/requests: {requests: req_admin_record[]} con las 200 mas recientes. -- req_admin_record: id,full_name,email,mobile,status,email_status,created_at, -- updated_at,notes:string; snapshot:req_snapshot (JSON ya convertido a objeto). -- Nunca devuelve idempotency_key ni email_html dentro del listado. -- Filtros de interfaz: texto por id/titular/email/titulo y estado; se aplican a la -- lista cargada. Sin paginacion mas alla de esas 200 solicitudes en el codigo. -- PATCH /api/admin/requests/{id}: {status:string,notes:string} -> {ok:boolean}. -- Solo admite estados del diccionario y notes<=4000; no envia correo ni cobra. -- GET /api/admin/requests/{id}/email: HTML almacenado, consulta/descarga del resumen. -- La plantilla lleva titulo, titular, referencia, hasta 3 fotos HTTPS, hotel o -- circuito, fechas/duracion, salida si aplica, viajeros/edades, habitacion/regimen, -- precio total, inclusiones, condiciones y contacto. Todo queda PENDIENTE. -- La plantilla escapa texto HTML. La vista administrativa define CSP limitada. -- No existen entidades operativas payment, ticket, voucher, confirmed_booking, -- email_queue, email_delivery ni historial de cambios de estado. -- FUNCIONES DE NEGOCIO Y PERSISTENCIA (aplicacion, no stored procedures): -- createRequest: valida contacto, honeypot, aceptaciones, clave y tarifa; comprueba -- oferta visible, fecha no pasada y caducidad solo si saleEndsAtConfirmed=true. -- Recalcula own: usando API vigente; compara precio esperado con el del servidor. -- Recalcula travel: y verifica expectedTotalCents; tarifas normales salen de QUOTES. -- Idempotencia compara email, nombre, movil y quoteId; igual -> misma referencia, -- diferente -> 409. El indice UNIQUE aporta garantia de clave unica en SQLite. -- Cuenta por email: maximo 5 solicitudes en la ultima hora antes de guardar. -- snapshot: copia la tarifa autorizada; emailSummary genera documento preparado; -- guarda una fila pending/prepared con timestamps. No confirma al proveedor. -- calculateOwn: vuelve a consultar oferta publicada y sin proveedor externo; -- valida calendario, min/max noches, dias bloqueados, edades y ocupacion exacta; -- obtiene PVP numerico positivo. Redondea HACIA ABAJO a multiplos de 5 EUR -- (Math.floor((pvp+1e-8)/5)*5), rechaza total resultante no positivo. -- ownPriceRoute: valida origen, limita texto de entrada a 2000 caracteres, -- calcula y devuelve quote/offer; control de espera de API limitado. -- travelQuote/travelRoute: ver wrapper y contrato de cotizadores anteriores. -- emailSummary: devuelve HTML, no transporte SMTP, no API de envio de correo. -- authorized: en el worker original requiere identidad autenticada y email de -- lista autorizada configurada. No es login implementado para el SFTP actual. -- sameOrigin: Origin debe coincidir y Sec-Fetch-Site no debe ser cross-site. -- escapeHtml/displayDate/money/json: escape, fechas es-ES UTC, EUR y respuesta -- sin cache; no alteran ni persisten registros. -- fetch: enrutador de API/admin/recursos; GET/HEAD para recursos y metodos -- explicitos para API. db: obtiene adaptador o falla sin almacenamiento. -- reservas.js: abrir/cerrar dialogo, seleccionar tarifa, validar/enviar/reintentar, -- mostrar referencia pendiente y volver a oferta; no confirma plazas. -- admin.js: load/render, filtros, guardar status/notes, visualizar/descargar email. -- La aplicacion no aplica las reglas de negocio con triggers SQLite. -- CONTROLES DEL PUENTE PUBLICO (sin secretos ni configuracion privada): -- api.php/mmFail: solo POST JSON a /api/own-price, /api/requests y /api/webs/estimar; -- origen propio, tipo de contenido, parseo JSON y cuerpo <=8000/16000 bytes. -- Rechaza otras rutas, incluida administracion y reserva directa de cotizadores. -- Exclusividad de solicitudes con bloqueo acotado; evita carreras entre altas. -- Limite adicional por huella de IP: 15 intentos/minuto y 60/hora; registro de -- intentos en JSON privado, no en booking_requests ni publicado con este esquema. -- req_rate_attempts (DOCUMENTAL): diccionario {huella:string -> integer[] Unix}. -- Se depuran intentos anteriores a una hora; maximo 10000 huellas simultaneas. -- No se guardan direcciones IP en texto dentro de esa estructura. -- La ejecucion hija tiene limites temporales y de salida; errores no muestran -- SQL, contacto del cliente ni rutas privadas. Sin cache ni indexacion de API. -- runner: comprueba de nuevo ruta/origen/JSON/tamano; ejecuta worker con DB; -- req_runner_input (DOCUMENTAL): path:string, origin:string, -- headers:diccionario, body:string|object. Cabeceras reenviadas: -- content-type, accept, origin, sec-fetch-site. req_runner_output (DOCUMENTAL): -- status:integer HTTP, body:string(JSON), headers:diccionario. -- reply: serializa respuesta interna; checkDeadline/expire limitan el proceso; -- query: invoca adaptador con tiempo/tamano acotados y rechaza errores internos. -- publicData elimina propiedades internas source*/supplier*/commission*/markup* -- y otras de origen/tarifas antes de la respuesta publica; photos.sourceUrl es -- excepcion de atribucion. Los filtros NO borran el snapshot almacenado privado. -- database/prepare/bind/first/all/run: parametros enlazados, una operacion por -- llamada. first -> objeto|null; all -> {results:object[]}; run -> -- {meta:{changes:integer}}. No concatenacion de valores de contacto en consultas. -- req_sqlite_operation (DOCUMENTAL): operation:string(first|all|run), -- query:string, parameters:JSON scalar[]. Entrada exclusivamente interna. -- req_sqlite_result (DOCUMENTAL): ok:boolean,result:objeto|null. -- sqlite-adapter.main: conexion SQLite privada; foreign_keys=ON, journal_mode=WAL, -- busy_timeout=15000; migra comprobando hashes; aplica cambios y devuelve JSON. -- Solo se inspecciono codigo/migraciones. No se leyeron registros de una BD real. -- ACTUAL en produccion desde 1.3.1; no activa avisos en la prueba anterior. -- Aviso a la agencia separado del email al cliente (email_status=prepared). CREATE TABLE agency_notifications ( request_id TEXT PRIMARY KEY NOT NULL REFERENCES booking_requests(id), state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','sending','accepted','failed','unknown')), attempts INTEGER NOT NULL DEFAULT 0, message_id TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, accepted_at TEXT, next_attempt_at TEXT, last_error TEXT ); CREATE INDEX idx_agency_notifications_retry ON agency_notifications(state,next_attempt_at); -- request_id: una notificacion por solicitud, FK a la solicitud congelada. -- state: pending, sending, accepted por MTA (no prueba de entrega), failed -- con rechazo conocido, unknown si no se conoce el resultado SMTP. -- attempts: numero de intentos; message_id: identificador determinista de correo. -- created_at/updated_at/accepted_at/next_attempt_at: fechas UTC ISO-8601. -- last_error: nombre de clase de error, nunca credenciales ni respuesta con PII. -- notify: encola la referencia que acaba de guardarse, no importa historicos; -- envia por MTA local al destinatario fijo configurado fuera del directorio publico. -- Hasta dos avisos por llamada; fallos conocidos esperan 300 s y se reintentan -- con nuevas solicitudes; no hay tarea periodica. sending interrumpido pasa a -- unknown a los 180 s y exige revision, sin reintento automatico. -- build_message: asunto/referencia, contacto, notas escapadas, fechas, viajeros, -- fotos y precio final congelado. Reply-To al solicitante validado. -- agencyNotificationStatus de respuesta: accepted | pending, opcional cuando -- el aviso esta activado. No sustituye emailStatus del mensaje al cliente. -- No hay confirmacion automatica de plazas, ni cobro, ni envio al cliente. -- ============================================================================ -- CATALOGO, TARIFAS Y NAVEGACION: MODELO DOCUMENTAL, NO TABLAS ACTIVAS -- Dialecto: SQLite 3.31 o posterior. DDL de estructura, sin datos ni migracion. -- La web actual lee catalogos JavaScript y consulta la API para ofertas propias. -- Crear estas tablas no conecta el runtime, no copia datos y no reserva plazas. -- Los nombres de propiedades se conservan en el mapa exhaustivo al final. -- Campos de origen/precios internos son definiciones de estructura, nunca valores. -- Su eventual contenido debe permanecer privado; no exportarlo a la web publica. -- Los campos NULL sin tipo observable no permiten inferir otros formatos. -- ============================================================================ -- Identidad compartida de ofertas: una oferta puede existir en varias representaciones. CREATE TABLE IF NOT EXISTS cat_identidad_oferta ( id TEXT PRIMARY KEY ); -- Entidades geograficas derivadas del catalogo y de las reglas de explorar.js. CREATE TABLE IF NOT EXISTS cat_comunidad ( nombre TEXT PRIMARY KEY, etiqueta TEXT, icono TEXT, nota TEXT, orden INTEGER ); CREATE TABLE IF NOT EXISTS cat_zona ( nombre TEXT NOT NULL, comunidad_nombre TEXT NOT NULL REFERENCES cat_comunidad(nombre), etiqueta TEXT, icono TEXT, orden INTEGER, PRIMARY KEY (nombre, comunidad_nombre) ); CREATE TABLE IF NOT EXISTS cat_ocupacion ( id TEXT PRIMARY KEY, api_occupant_id INTEGER UNIQUE, adults INTEGER NOT NULL CHECK (adults > 0), children INTEGER NOT NULL CHECK (children >= 0), etiqueta TEXT ); -- Destinos visuales, planes, temporadas y estado temporal de filtros se documentan -- una sola vez en ui_destination_definition, ui_plan_definition y ui_navigation_state -- (fragmento 40). History API mantiene esos filtros en el navegador; no en SQLite. -- Entidad de MM_CATALOG CREATE TABLE IF NOT EXISTS cat_catalogo ( row_id INTEGER PRIMARY KEY, "updated_at" TEXT, "review_mode" TEXT ); -- Circuitos: circuit_area, circuit_zone, circuit_zone_label, circuit_icon, program_days, -- occupancy_label y los hijos de salidas/aeropuertos son datos propios del circuito. -- El alojamiento puede ser un circuito de varios hoteles; hotel es la etiqueta disponible. -- Entidad de MM_CATALOG.offers[] CREATE TABLE IF NOT EXISTS cat_oferta ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "community" TEXT REFERENCES cat_comunidad(nombre), "theme" TEXT, "event" TEXT, "supplier" TEXT, "source_type" TEXT, "booking_mode" TEXT, "status" TEXT, "sale_ends_at_confirmed" INTEGER CHECK ("sale_ends_at_confirmed" IN (0, 1)), "price_unit" TEXT, "price_note" TEXT, "photo_note" TEXT, "id" TEXT UNIQUE REFERENCES cat_identidad_oferta(id), "title" TEXT, "city" TEXT, "zone" TEXT, "province" TEXT, "hotel" TEXT, "price" NUMERIC, "nights" NUMERIC, "board" TEXT, "date_label" TEXT, "quote_intro" TEXT, "image" TEXT, "source_url" TEXT, "source_price" NUMERIC, "min_adult_age" NUMERIC, "stars" NUMERIC, "gallery_type" TEXT, "supplier_offer_id" TEXT, "public_url" TEXT, "sale_ends_at" TEXT, "sale_ends_at_origin" TEXT, "sale_ends_at_scope" TEXT, "nights_is_minimum" INTEGER CHECK ("nights_is_minimum" IN (0, 1)), "source_price_unit" TEXT, "departure_date" TEXT, "hotel_key" TEXT, "image_alt" TEXT, "visible" INTEGER CHECK ("visible" IN (0, 1)), "getaway_group" TEXT, "from" INTEGER CHECK ("from" IN (0, 1)), "reviewed_at" TEXT, "source_sheet" TEXT, "source_file" TEXT, "circuit_area" TEXT, "circuit_zone" TEXT, "circuit_zone_label" TEXT, "circuit_icon" TEXT, "destination" TEXT, "program_days" NUMERIC, "summary" TEXT, "occupancy_label" TEXT, FOREIGN KEY (zone, community) REFERENCES cat_zona(nombre, comunidad_nombre) ); -- Entidad de MM_CATALOG.offers[].photos[] CREATE TABLE IF NOT EXISTS cat_oferta_foto ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "src" TEXT, "alt" TEXT, "caption" TEXT, "source_url" TEXT, "original_src" TEXT, "author" TEXT, "license" TEXT, "license_url" TEXT, "kind" TEXT, "credit" TEXT ); -- Entidad de MM_CATALOG.offers[].sourceMetadata CREATE TABLE IF NOT EXISTS cat_oferta_procedencia ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, "supplier" TEXT, "leaflet_id" TEXT, "year_confirmed" INTEGER CHECK ("year_confirmed" IN (0, 1)), "full_leaflet_verified" INTEGER CHECK ("full_leaflet_verified" IN (0, 1)), "offer_id" TEXT REFERENCES cat_identidad_oferta(id), "category" TEXT, "source_checked_at" TEXT ); -- Entidad de MM_CATALOG.pricingRule CREATE TABLE IF NOT EXISTS cat_regla_precio ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, "step_euros" NUMERIC, "direction" TEXT, "applies_to" TEXT ); -- Entidad de MM_CATALOG.photoPolicy CREATE TABLE IF NOT EXISTS cat_politica_foto ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, "hotel_only" TEXT, "experience_and_hotel" TEXT, "cover" TEXT, "assignment" TEXT ); -- Entidad de MM_CATALOG.suppliers[] CREATE TABLE IF NOT EXISTS cat_proveedor ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "name" TEXT, "url" TEXT, "access_verified_at" TEXT ); -- Entidad de MM_CATALOG.childAgePolicy CREATE TABLE IF NOT EXISTS cat_politica_edad ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, "min" NUMERIC, "max" NUMERIC, "require_exact_age" INTEGER CHECK ("require_exact_age" IN (0, 1)), "pricing" TEXT ); -- Entidad de MM_CATALOG.hotelSelectionPolicy CREATE TABLE IF NOT EXISTS cat_politica_seleccion_hotel ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, "maximum_hotels_per_getaway" NUMERIC, "comparison" TEXT ); -- Entidad de MM_CATALOG.hotelSelectionPolicy.groups{groupId} CREATE TABLE IF NOT EXISTS cat_comparacion_hoteles ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_politica_seleccion_hotel(row_id) ON DELETE CASCADE, group_id TEXT NOT NULL, "check_in" TEXT, "check_out" TEXT, "adults" NUMERIC, "board" TEXT, "park_days" NUMERIC ); -- Entidad de MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId} CREATE TABLE IF NOT EXISTS cat_seleccion_hoteles ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_politica_seleccion_hotel(row_id) ON DELETE CASCADE, group_id TEXT NOT NULL, "date" TEXT, "basis" TEXT ); -- Entidad de MM_CATALOG.roundingPolicy CREATE TABLE IF NOT EXISTS cat_politica_redondeo ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo(row_id) ON DELETE CASCADE, "increment_euros" NUMERIC, "direction" TEXT, "scope" TEXT, "decimals" NUMERIC ); -- Entidad de MM_QUOTES CREATE TABLE IF NOT EXISTS cat_tarifario ( row_id INTEGER PRIMARY KEY, "month" TEXT, "updated_at" TEXT, "review_mode" TEXT, "pending_child_ages" INTEGER CHECK ("pending_child_ages" IN (0, 1)) ); -- Entidad de MM_QUOTES.quotes[] CREATE TABLE IF NOT EXISTS cat_cotizacion ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "id" TEXT UNIQUE, "offer_id" TEXT REFERENCES cat_identidad_oferta(id), "supplier" TEXT, "check_in" TEXT, "check_out" TEXT, "nights" NUMERIC, "adults" NUMERIC, "rooms" NUMERIC, "room" TEXT, "board" TEXT, "park_days" NUMERIC, "supplier_pvp" NUMERIC, "price" NUMERIC, "price_unit" TEXT, "currency" TEXT, "availability" TEXT, "checked_at" TEXT, "source_url" TEXT, "package_name" TEXT, "pvp_verified" INTEGER CHECK ("pvp_verified" IN (0, 1)), "supplier_board" TEXT, "source_offer_url" TEXT, "park_name" TEXT, "variant_label" TEXT, "source_type" TEXT, "source_row" NUMERIC, "min_adult_age" NUMERIC, "source_sheet" TEXT, "source_cell" TEXT, "source_file" TEXT, "source_price" NUMERIC, "program_days" NUMERIC, "departure_city" TEXT, "departure_airport" TEXT, "price_per_person" NUMERIC, "return_date_differs" INTEGER CHECK ("return_date_differs" IN (0, 1)) ); -- Entidad de MM_QUOTES.checks[] CREATE TABLE IF NOT EXISTS cat_comprobacion ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "offer_id" TEXT REFERENCES cat_identidad_oferta(id), "check_in" TEXT, "check_out" TEXT, "adults" NUMERIC, "status" TEXT, "note" TEXT, "nights" NUMERIC, "checked_at" TEXT, "result" TEXT ); -- Entidad de MM_QUOTES.childAgePolicy CREATE TABLE IF NOT EXISTS cat_tarifario_politica_edad ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario(row_id) ON DELETE CASCADE, "min" NUMERIC, "max" NUMERIC, "require_exact_age" INTEGER CHECK ("require_exact_age" IN (0, 1)), "pricing" TEXT ); -- Entidad de MM_QUOTES.roundingPolicy CREATE TABLE IF NOT EXISTS cat_tarifario_redondeo ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario(row_id) ON DELETE CASCADE, "increment_euros" NUMERIC, "direction" TEXT, "scope" TEXT, "decimals" NUMERIC ); -- Entidad de MM_OWN_OFFERS CREATE TABLE IF NOT EXISTS cat_catalogo_propio ( row_id INTEGER PRIMARY KEY, "source" TEXT, "checked_at" TEXT, "stage" TEXT ); -- Entidad de MM_OWN_OFFERS.offers[] CREATE TABLE IF NOT EXISTS cat_oferta_propia ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_catalogo_propio(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "id" TEXT UNIQUE REFERENCES cat_identidad_oferta(id), "title" TEXT, "hotel" TEXT, "stars" NUMERIC, "date_label" TEXT, "nights" TEXT, "board" TEXT, "category" TEXT, "destination" TEXT, "source_url" TEXT, "price_status" TEXT, "migration_phase" TEXT, "poster" TEXT, "max_child_age" NUMERIC, "poster_source" TEXT, "photo_note" TEXT, "poster_clean" INTEGER CHECK ("poster_clean" IN (0, 1)), "poster_design_url" TEXT, "poster_visible_fraction" NUMERIC, "poster_ratio" NUMERIC ); -- Entidad de MM_OWN_OFFERS.offers[].photos[] CREATE TABLE IF NOT EXISTS cat_oferta_propia_foto ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_oferta_propia(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "src" TEXT, "alt" TEXT, "caption" TEXT, "source_url" TEXT, "original_src" TEXT, "kind" TEXT, "credit" TEXT, "license" TEXT, "license_url" TEXT, "original_url" TEXT, "author" TEXT ); -- Entidad de MM_OWN_RATES{offerId} CREATE TABLE IF NOT EXISTS cat_tarifa_propia ( row_id INTEGER PRIMARY KEY, source_offer_key TEXT NOT NULL UNIQUE REFERENCES cat_identidad_oferta(id), "api_id" NUMERIC, "id" TEXT UNIQUE REFERENCES cat_identidad_oferta(id), "title" TEXT, "hotel" TEXT, "board" TEXT, "service" INTEGER CHECK ("service" IN (0, 1)) ); -- Entidad de MM_OWN_RATES{offerId}.photos[] CREATE TABLE IF NOT EXISTS cat_tarifa_propia_foto ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "src" TEXT, "alt" TEXT, "caption" TEXT, "source_url" TEXT, "original_src" TEXT, "kind" TEXT, "credit" TEXT, "license" TEXT, "license_url" TEXT, "original_url" TEXT, "author" TEXT ); -- Reflejo literal del detalle de API. Las columnas precio_* conservan cada tarifa mensual -- y el ordinal del menor; no deben interpretarse como importes por estancia sin consultar -- la semantica del proveedor. disponibilidad_*_json conserva NULL frente a listas vacias -- o listas de cadenas fecha. SQLite 3.31 no exige JSON1 para crear este esquema. -- Campos observados solo como NULL (proveedor_id, hotel_id, proveedor_regimen, etc.) -- se tipan TEXT anulable; su estructura no se infiere ni se inventa. -- Entidad de MM_OWN_RATES{offerId}.detail CREATE TABLE IF NOT EXISTS cat_tarifa_propia_detalle ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia(row_id) ON DELETE CASCADE, "id" NUMERIC, "titulo" TEXT, "subtitulo" TEXT, "descripcion" TEXT, "fecha_inicio" TEXT, "fecha_fin" TEXT, "url_web" TEXT, "precio_enero" NUMERIC, "precio_febrero" NUMERIC, "precio_marzo" NUMERIC, "precio_abril" NUMERIC, "precio_mayo" NUMERIC, "precio_junio" NUMERIC, "precio_julio" NUMERIC, "precio_agosto" NUMERIC, "precio_septiembre" NUMERIC, "precio_octubre" NUMERIC, "precio_noviembre" NUMERIC, "precio_diciembre" NUMERIC, "precio_cualquier_mes" NUMERIC, "precio_anticipo" NUMERIC, "orden" NUMERIC, "publicada" INTEGER CHECK ("publicada" IN (0, 1)), "precio_primer_peque_enero" NUMERIC, "precio_segundo_peque_enero" NUMERIC, "precio_segundo_peque_febrero" NUMERIC, "precio_primer_peque_febrero" NUMERIC, "precio_primer_peque_marzo" NUMERIC, "precio_segundo_peque_marzo" NUMERIC, "precio_primer_peque_abril" NUMERIC, "precio_segundo_peque_abril" NUMERIC, "precio_primer_peque_mayo" NUMERIC, "precio_segundo_peque_mayo" NUMERIC, "precio_primer_peque_junio" NUMERIC, "precio_segundo_peque_junio" NUMERIC, "precio_primer_peque_julio" NUMERIC, "precio_segundo_peque_julio" NUMERIC, "precio_primer_peque_agosto" NUMERIC, "precio_segundo_peque_agosto" NUMERIC, "precio_primer_peque_septiembre" NUMERIC, "precio_segundo_peque_septiembre" NUMERIC, "precio_primer_peque_octubre" NUMERIC, "precio_segundo_peque_octubre" NUMERIC, "precio_primer_peque_noviembre" NUMERIC, "precio_segundo_peque_noviembre" NUMERIC, "precio_primer_peque_diciembre" NUMERIC, "precio_segundo_peque_diciembre" NUMERIC, "disponibilidad_enero_json" TEXT, "disponibilidad_febrero_json" TEXT, "disponibilidad_marzo_json" TEXT, "disponibilidad_abril_json" TEXT, "disponibilidad_mayo" TEXT, "disponibilidad_junio_json" TEXT, "disponibilidad_julio_json" TEXT, "disponibilidad_agosto_json" TEXT, "disponibilidad_septiembre_json" TEXT, "disponibilidad_octubre_json" TEXT, "disponibilidad_noviembre_json" TEXT, "disponibilidad_diciembre_json" TEXT, "min_noches" NUMERIC, "max_noches" NUMERIC, "nombre_hotel" TEXT, "numero_estrellas_hotel" NUMERIC, "regimen" TEXT, "es_oferta" INTEGER CHECK ("es_oferta" IN (0, 1)), "precio_tercer_peque_julio" NUMERIC, "precio_tercer_peque_agosto" NUMERIC, "tiene_tercer_peque" INTEGER CHECK ("tiene_tercer_peque" IN (0, 1)), "edad_maxima_peque" NUMERIC, "sin_pago_previo" INTEGER CHECK ("sin_pago_previo" IN (0, 1)), "proveedor_id" TEXT, "hotel_id" TEXT, "proveedor_regimen" TEXT ); -- Entidad de MM_OWN_RATES{offerId}.detail.imagen CREATE TABLE IF NOT EXISTS cat_tarifa_propia_imagen_principal ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia_detalle(row_id) ON DELETE CASCADE, "imagen" TEXT, "mime" TEXT, "url" TEXT, "id" TEXT ); -- Entidad de MM_OWN_RATES{offerId}.detail.imagenes[] CREATE TABLE IF NOT EXISTS cat_tarifa_propia_imagen ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia_detalle(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "imagen" TEXT, "mime" TEXT, "url" TEXT, "id" TEXT ); -- Entidad de MM_OWN_RATES{offerId}.detail.categorias[] CREATE TABLE IF NOT EXISTS cat_tarifa_propia_categoria ( row_id INTEGER PRIMARY KEY, parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia_detalle(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), "id" NUMERIC, "nombre" TEXT, "es_externo" INTEGER CHECK ("es_externo" IN (0, 1)), "url" TEXT, "activo" INTEGER CHECK ("activo" IN (0, 1)), "sub_categories" TEXT, "orden" NUMERIC, "imagen" TEXT, "imagen_nombre" TEXT ); -- Lista ordenada de MM_CATALOG.offers[].sections; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_seccion ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.offers[].conditions; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_condicion ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.offers[].includes; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_inclusion ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.offers[].sourceRows; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_fila_origen ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor NUMERIC, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.offers[].departureDates; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_salida ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.offers[].departureAirports; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_aeropuerto ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.childAgePolicy.infantAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_politica_edad_bebe ( parent_row_id INTEGER NOT NULL REFERENCES cat_politica_edad(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.childAgePolicy.childAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_politica_edad_menor ( parent_row_id INTEGER NOT NULL REFERENCES cat_politica_edad(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.hotelSelectionPolicy.groups{groupId}.selectedOfferIds; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_comparacion_oferta ( parent_row_id INTEGER NOT NULL REFERENCES cat_comparacion_hoteles(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), oferta_id TEXT REFERENCES cat_identidad_oferta(id), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.hotelSelectionPolicy.groups{groupId}.childAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_comparacion_edad ( parent_row_id INTEGER NOT NULL REFERENCES cat_comparacion_hoteles(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.hotelSelectionPolicy.groups{groupId}.supplierPvps; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_comparacion_pvp ( parent_row_id INTEGER NOT NULL REFERENCES cat_comparacion_hoteles(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), pvp NUMERIC, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId}.selected; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_seleccion_oferta ( parent_row_id INTEGER NOT NULL REFERENCES cat_seleccion_hoteles(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), oferta_id TEXT REFERENCES cat_identidad_oferta(id), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.quotes[].childAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_cotizacion_menor ( parent_row_id INTEGER NOT NULL REFERENCES cat_cotizacion(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.quotes[].includes; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_cotizacion_inclusion ( parent_row_id INTEGER NOT NULL REFERENCES cat_cotizacion(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.quotes[].conditions; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_cotizacion_condicion ( parent_row_id INTEGER NOT NULL REFERENCES cat_cotizacion(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.checks[].childAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_comprobacion_menor ( parent_row_id INTEGER NOT NULL REFERENCES cat_comprobacion(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.childAgePolicy.infantAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_tarifario_edad_bebe ( parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario_politica_edad(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.childAgePolicy.childAges; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_tarifario_edad_menor ( parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario_politica_edad(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), edad NUMERIC CHECK (edad >= 0 AND edad <= 17), PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_QUOTES.months; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_tarifario_mes ( parent_row_id INTEGER NOT NULL REFERENCES cat_tarifario(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_OWN_OFFERS.offers[].description; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_oferta_propia_descripcion ( parent_row_id INTEGER NOT NULL REFERENCES cat_oferta_propia(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_OWN_RATES{offerId}.description; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_tarifa_propia_descripcion ( parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- Lista ordenada de MM_OWN_RATES{offerId}.detail.fechasSinDisponibilidad; una fila por elemento, sin datos en este fichero. CREATE TABLE IF NOT EXISTS cat_tarifa_propia_fecha_bloqueada ( parent_row_id INTEGER NOT NULL REFERENCES cat_tarifa_propia_detalle(row_id) ON DELETE CASCADE, posicion INTEGER NOT NULL CHECK (posicion >= 0), valor TEXT, PRIMARY KEY (parent_row_id, posicion) ); -- FUNCIONES Y REGLAS DE ESTE MODULO (implementadas en JavaScript, no SQL). -- cotizador.js: markup/mount/update construyen la seleccion y el calendario; hasParty -- permite solo ocupaciones con tarifa total positiva; sameAges compara multiconjuntos -- de edades ordenadas, conservando edades repetidas. El calendario limita la fecha -- por oferta, noches, mes, ocupacion y edad exacta. Sin tarifa no se inventa un importe. -- Las comprobaciones distinguen consulta negativa de combinacion pendiente de consultar. -- Las alternativas se agrupan por habitacion, regimen, variante, dias/nombre del parque. -- partyLabel distingue bebes menores de 2; el selector solicita la edad de cada menor. -- El maximo en el catalogo se deriva de la oferta y las edades disponibles, limitado -- a 17; ofertas Magic parten de 16 y las demas de 11. Esto no genera tarifas nuevas. -- circuitos-ui.js: rates/markup/mount/update permiten 2 adultos sin menores, tarifa -- total positiva, aeropuerto y fecha existentes. price_per_person se muestra separado -- del total; no sustituye el total. return_date_differs informa del regreso distinto. -- own-prices.js: calculateOwn/ownPriceRoute consultan primero el detalle actualizado, -- rechazan servicios, ofertas no publicadas o proveedor externo, y validan ocupacion, -- fechas reales, intervalo de oferta, noches minimas/maximas, todas las fechas -- bloqueadas, cantidad de menores y sus edades enteras hasta edad_maxima_peque. -- El precio final lo devuelve la API, NO la suma local de columnas mensuales: -- floor((pvp + 1e-8) / 5) * 5; se rechazan importes no finitos, nulos o no positivos. -- La seleccion own conserva offerId, checkIn, nights, occupantId, childAges y -- expectedPrice; se recalcula en servidor antes de guardar la peticion pendiente. -- explorar.js: ownFamily usa categorias familiares, tarifas de menores positivas -- o titulo familiar. familyIds/adultIds se calculan con tarifas existentes. -- occasions permite varias temporadas para la misma oferta; planMatch, destinationMatch, -- regionMatches e isMatch combinan plan, region/destino, audiencia, temporada y texto. -- norm normaliza mayusculas/tildes. ownGeography/regionLabels/regionNotes/regionOrder -- y las definiciones seasons/experiences/destinations proporcionan iconos y agrupaciones. -- renderSeasons/renderPlans/renderCircuitPlan/renderRegions/renderResults/renderStep -- solo representan los resultados. setupSearch/searchFromForm/startSearch/chooseSeason -- y reset cambian los filtros; saveJourney/rememberFilters/goStep usan History API. -- interleave alterna ofertas locales y propias. Los contadores se derivan, no se guardan. -- destinationButton/cruiseButton/categoryArt generan accesos e iconos; esc/escape -- escapan texto, money/date formatean moneda/fechas; scrollTo mueve el foco visual. -- Las funciones de interfaz no son procedimientos SQL; los comentarios registran -- su relacion con las entidades sin afirmar que se ejecuten al importar este DDL. -- Vistas documentales equivalentes a filtros precisos del frontend. CREATE VIEW IF NOT EXISTS cat_v_tarifas_total_positivas AS SELECT * FROM cat_cotizacion WHERE price_unit = 'total' AND price > 0; CREATE VIEW IF NOT EXISTS cat_v_tarifas_dos_adultos_sin_menores AS SELECT q.* FROM cat_cotizacion q WHERE q.adults = 2 AND q.price > 0 AND q.price_unit = 'total' AND NOT EXISTS (SELECT 1 FROM cat_cotizacion_menor n WHERE n.parent_row_id = q.row_id); CREATE INDEX IF NOT EXISTS cat_idx_cotizacion_busqueda ON cat_cotizacion(offer_id, check_in, nights, adults); CREATE INDEX IF NOT EXISTS cat_idx_comprobacion_busqueda ON cat_comprobacion(offer_id, check_in, nights, adults); CREATE INDEX IF NOT EXISTS cat_idx_circuito_salida ON cat_cotizacion(offer_id, departure_airport, check_in); -- MAPA EXHAUSTIVO DE PROPIEDADES OBSERVADAS (solo rutas y tipos, sin valores). -- MM_CATALOG [object] -> cat_catalogo -- MM_CATALOG.updatedAt [string] -> cat_catalogo.updated_at -- MM_CATALOG.reviewMode [string] -> cat_catalogo.review_mode -- MM_CATALOG.offers [array] -> cat_oferta -- MM_CATALOG.offers[] [object] -> cat_oferta -- MM_CATALOG.offers[].community [string] -> cat_oferta.community -- MM_CATALOG.offers[].theme [string] -> cat_oferta.theme -- MM_CATALOG.offers[].event [string] -> cat_oferta.event -- MM_CATALOG.offers[].sections [array] -> cat_oferta_seccion -- MM_CATALOG.offers[].sections[] [string] -> cat_oferta_seccion.valor -- MM_CATALOG.offers[].supplier [string] -> cat_oferta.supplier -- MM_CATALOG.offers[].sourceType [string] -> cat_oferta.source_type -- MM_CATALOG.offers[].bookingMode [string] -> cat_oferta.booking_mode -- MM_CATALOG.offers[].status [string] -> cat_oferta.status -- MM_CATALOG.offers[].saleEndsAtConfirmed [boolean] -> cat_oferta.sale_ends_at_confirmed -- MM_CATALOG.offers[].priceUnit [null|string] -> cat_oferta.price_unit -- MM_CATALOG.offers[].priceNote [string] -> cat_oferta.price_note -- MM_CATALOG.offers[].photoNote [string] -> cat_oferta.photo_note -- MM_CATALOG.offers[].conditions [array] -> cat_oferta_condicion -- MM_CATALOG.offers[].conditions[] [string] -> cat_oferta_condicion.valor -- MM_CATALOG.offers[].id [string] -> cat_oferta.id -- MM_CATALOG.offers[].title [string] -> cat_oferta.title -- MM_CATALOG.offers[].city [string] -> cat_oferta.city -- MM_CATALOG.offers[].zone [string] -> cat_oferta.zone -- MM_CATALOG.offers[].province [string] -> cat_oferta.province -- MM_CATALOG.offers[].hotel [string] -> cat_oferta.hotel -- MM_CATALOG.offers[].price [null|number] -> cat_oferta.price -- MM_CATALOG.offers[].nights [null|number] -> cat_oferta.nights -- MM_CATALOG.offers[].board [string] -> cat_oferta.board -- MM_CATALOG.offers[].dateLabel [string] -> cat_oferta.date_label -- MM_CATALOG.offers[].quoteIntro [string] -> cat_oferta.quote_intro -- MM_CATALOG.offers[].includes [array] -> cat_oferta_inclusion -- MM_CATALOG.offers[].includes[] [string] -> cat_oferta_inclusion.valor -- MM_CATALOG.offers[].photos [array] -> cat_oferta_foto -- MM_CATALOG.offers[].photos[] [object] -> cat_oferta_foto -- MM_CATALOG.offers[].photos[].src [string] -> cat_oferta_foto.src -- MM_CATALOG.offers[].photos[].alt [string] -> cat_oferta_foto.alt -- MM_CATALOG.offers[].photos[].caption [string] -> cat_oferta_foto.caption -- MM_CATALOG.offers[].photos[].sourceUrl [string] -> cat_oferta_foto.source_url -- MM_CATALOG.offers[].photos[].originalSrc [string] -> cat_oferta_foto.original_src -- MM_CATALOG.offers[].photos[].author [string] -> cat_oferta_foto.author -- MM_CATALOG.offers[].photos[].license [string] -> cat_oferta_foto.license -- MM_CATALOG.offers[].photos[].licenseUrl [string] -> cat_oferta_foto.license_url -- MM_CATALOG.offers[].image [string] -> cat_oferta.image -- MM_CATALOG.offers[].sourceUrl [string] -> cat_oferta.source_url -- MM_CATALOG.offers[].sourceRows [array] -> cat_oferta_fila_origen -- MM_CATALOG.offers[].sourceRows[] [number] -> cat_oferta_fila_origen.valor -- MM_CATALOG.offers[].sourcePrice [number] -> cat_oferta.source_price -- MM_CATALOG.offers[].photos[].kind [string] -> cat_oferta_foto.kind -- MM_CATALOG.offers[].photos[].credit [string] -> cat_oferta_foto.credit -- MM_CATALOG.offers[].minAdultAge [number] -> cat_oferta.min_adult_age -- MM_CATALOG.offers[].stars [null|number] -> cat_oferta.stars -- MM_CATALOG.offers[].galleryType [string] -> cat_oferta.gallery_type -- MM_CATALOG.offers[].supplierOfferId [string] -> cat_oferta.supplier_offer_id -- MM_CATALOG.offers[].publicUrl [null] -> cat_oferta.public_url -- MM_CATALOG.offers[].saleEndsAt [null|string] -> cat_oferta.sale_ends_at -- MM_CATALOG.offers[].saleEndsAtOrigin [string] -> cat_oferta.sale_ends_at_origin -- MM_CATALOG.offers[].saleEndsAtScope [string] -> cat_oferta.sale_ends_at_scope -- MM_CATALOG.offers[].nightsIsMinimum [boolean] -> cat_oferta.nights_is_minimum -- MM_CATALOG.offers[].sourcePriceUnit [null|string] -> cat_oferta.source_price_unit -- MM_CATALOG.offers[].departureDate [string] -> cat_oferta.departure_date -- MM_CATALOG.offers[].hotelKey [string] -> cat_oferta.hotel_key -- MM_CATALOG.offers[].imageAlt [string] -> cat_oferta.image_alt -- MM_CATALOG.offers[].visible [boolean] -> cat_oferta.visible -- MM_CATALOG.offers[].getawayGroup [string] -> cat_oferta.getaway_group -- MM_CATALOG.offers[].from [boolean] -> cat_oferta.from -- MM_CATALOG.offers[].sourceMetadata [object] -> cat_oferta_procedencia -- MM_CATALOG.offers[].sourceMetadata.supplier [string] -> cat_oferta_procedencia.supplier -- MM_CATALOG.offers[].sourceMetadata.leafletId [string] -> cat_oferta_procedencia.leaflet_id -- MM_CATALOG.offers[].sourceMetadata.yearConfirmed [boolean] -> cat_oferta_procedencia.year_confirmed -- MM_CATALOG.offers[].sourceMetadata.fullLeafletVerified [boolean] -> cat_oferta_procedencia.full_leaflet_verified -- MM_CATALOG.offers[].reviewedAt [string] -> cat_oferta.reviewed_at -- MM_CATALOG.offers[].sourceMetadata.offerId [string] -> cat_oferta_procedencia.offer_id -- MM_CATALOG.offers[].sourceSheet [string] -> cat_oferta.source_sheet -- MM_CATALOG.offers[].sourceFile [string] -> cat_oferta.source_file -- MM_CATALOG.offers[].circuitArea [string] -> cat_oferta.circuit_area -- MM_CATALOG.offers[].circuitZone [string] -> cat_oferta.circuit_zone -- MM_CATALOG.offers[].circuitZoneLabel [string] -> cat_oferta.circuit_zone_label -- MM_CATALOG.offers[].circuitIcon [string] -> cat_oferta.circuit_icon -- MM_CATALOG.offers[].destination [string] -> cat_oferta.destination -- MM_CATALOG.offers[].programDays [number] -> cat_oferta.program_days -- MM_CATALOG.offers[].summary [string] -> cat_oferta.summary -- MM_CATALOG.offers[].departureDates [array] -> cat_oferta_salida -- MM_CATALOG.offers[].departureDates[] [string] -> cat_oferta_salida.valor -- MM_CATALOG.offers[].departureAirports [array] -> cat_oferta_aeropuerto -- MM_CATALOG.offers[].departureAirports[] [string] -> cat_oferta_aeropuerto.valor -- MM_CATALOG.offers[].occupancyLabel [string] -> cat_oferta.occupancy_label -- MM_CATALOG.offers[].sourceMetadata.category [string] -> cat_oferta_procedencia.category -- MM_CATALOG.offers[].sourceMetadata.sourceCheckedAt [string] -> cat_oferta_procedencia.source_checked_at -- MM_CATALOG.pricingRule [object] -> cat_regla_precio -- MM_CATALOG.pricingRule.stepEuros [number] -> cat_regla_precio.step_euros -- MM_CATALOG.pricingRule.direction [string] -> cat_regla_precio.direction -- MM_CATALOG.pricingRule.appliesTo [string] -> cat_regla_precio.applies_to -- MM_CATALOG.photoPolicy [object] -> cat_politica_foto -- MM_CATALOG.photoPolicy.hotelOnly [string] -> cat_politica_foto.hotel_only -- MM_CATALOG.photoPolicy.experienceAndHotel [string] -> cat_politica_foto.experience_and_hotel -- MM_CATALOG.photoPolicy.cover [string] -> cat_politica_foto.cover -- MM_CATALOG.photoPolicy.assignment [string] -> cat_politica_foto.assignment -- MM_CATALOG.suppliers [array] -> cat_proveedor -- MM_CATALOG.suppliers[] [object] -> cat_proveedor -- MM_CATALOG.suppliers[].name [string] -> cat_proveedor.name -- MM_CATALOG.suppliers[].url [string] -> cat_proveedor.url -- MM_CATALOG.suppliers[].accessVerifiedAt [string] -> cat_proveedor.access_verified_at -- MM_CATALOG.childAgePolicy [object] -> cat_politica_edad -- MM_CATALOG.childAgePolicy.min [number] -> cat_politica_edad.min -- MM_CATALOG.childAgePolicy.max [number] -> cat_politica_edad.max -- MM_CATALOG.childAgePolicy.infantAges [array] -> cat_politica_edad_bebe -- MM_CATALOG.childAgePolicy.infantAges[] [number] -> cat_politica_edad_bebe.edad -- MM_CATALOG.childAgePolicy.childAges [array] -> cat_politica_edad_menor -- MM_CATALOG.childAgePolicy.childAges[] [number] -> cat_politica_edad_menor.edad -- MM_CATALOG.childAgePolicy.requireExactAge [boolean] -> cat_politica_edad.require_exact_age -- MM_CATALOG.childAgePolicy.pricing [string] -> cat_politica_edad.pricing -- MM_CATALOG.hotelSelectionPolicy [object] -> cat_politica_seleccion_hotel -- MM_CATALOG.hotelSelectionPolicy.maximumHotelsPerGetaway [number] -> cat_politica_seleccion_hotel.maximum_hotels_per_getaway -- MM_CATALOG.hotelSelectionPolicy.comparison [string] -> cat_politica_seleccion_hotel.comparison -- MM_CATALOG.hotelSelectionPolicy.groups [object] -> cat_comparacion_hoteles.group_id -- MM_CATALOG.hotelSelectionPolicy.groups{groupId} [object] -> cat_comparacion_hoteles -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.selectedOfferIds [array] -> cat_comparacion_oferta -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.selectedOfferIds[] [string] -> cat_comparacion_oferta.oferta_id -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.checkIn [string] -> cat_comparacion_hoteles.check_in -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.checkOut [string] -> cat_comparacion_hoteles.check_out -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.adults [number] -> cat_comparacion_hoteles.adults -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.childAges [array] -> cat_comparacion_edad -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.board [string] -> cat_comparacion_hoteles.board -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.parkDays [number] -> cat_comparacion_hoteles.park_days -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.supplierPvps [array] -> cat_comparacion_pvp -- MM_CATALOG.hotelSelectionPolicy.groups{groupId}.supplierPvps[] [number] -> cat_comparacion_pvp.pvp -- MM_CATALOG.hotelSelectionPolicy.additionalSelections [object] -> cat_seleccion_hoteles.group_id -- MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId} [object] -> cat_seleccion_hoteles -- MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId}.date [string] -> cat_seleccion_hoteles.date -- MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId}.selected [array] -> cat_seleccion_oferta -- MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId}.selected[] [string] -> cat_seleccion_oferta.oferta_id -- MM_CATALOG.hotelSelectionPolicy.additionalSelections{groupId}.basis [string] -> cat_seleccion_hoteles.basis -- MM_CATALOG.roundingPolicy [object] -> cat_politica_redondeo -- MM_CATALOG.roundingPolicy.incrementEuros [number] -> cat_politica_redondeo.increment_euros -- MM_CATALOG.roundingPolicy.direction [string] -> cat_politica_redondeo.direction -- MM_CATALOG.roundingPolicy.scope [string] -> cat_politica_redondeo.scope -- MM_CATALOG.roundingPolicy.decimals [number] -> cat_politica_redondeo.decimals -- MM_QUOTES [object] -> cat_tarifario -- MM_QUOTES.month [string] -> cat_tarifario.month -- MM_QUOTES.updatedAt [string] -> cat_tarifario.updated_at -- MM_QUOTES.reviewMode [string] -> cat_tarifario.review_mode -- MM_QUOTES.quotes [array] -> cat_cotizacion -- MM_QUOTES.quotes[] [object] -> cat_cotizacion -- MM_QUOTES.quotes[].id [string] -> cat_cotizacion.id -- MM_QUOTES.quotes[].offerId [string] -> cat_cotizacion.offer_id -- MM_QUOTES.quotes[].supplier [string] -> cat_cotizacion.supplier -- MM_QUOTES.quotes[].checkIn [string] -> cat_cotizacion.check_in -- MM_QUOTES.quotes[].checkOut [string] -> cat_cotizacion.check_out -- MM_QUOTES.quotes[].nights [number] -> cat_cotizacion.nights -- MM_QUOTES.quotes[].adults [number] -> cat_cotizacion.adults -- MM_QUOTES.quotes[].childAges [array] -> cat_cotizacion_menor -- MM_QUOTES.quotes[].rooms [number] -> cat_cotizacion.rooms -- MM_QUOTES.quotes[].room [string] -> cat_cotizacion.room -- MM_QUOTES.quotes[].board [string] -> cat_cotizacion.board -- MM_QUOTES.quotes[].parkDays [number] -> cat_cotizacion.park_days -- MM_QUOTES.quotes[].supplierPvp [number] -> cat_cotizacion.supplier_pvp -- MM_QUOTES.quotes[].price [number] -> cat_cotizacion.price -- MM_QUOTES.quotes[].priceUnit [string] -> cat_cotizacion.price_unit -- MM_QUOTES.quotes[].currency [string] -> cat_cotizacion.currency -- MM_QUOTES.quotes[].availability [string] -> cat_cotizacion.availability -- MM_QUOTES.quotes[].checkedAt [string] -> cat_cotizacion.checked_at -- MM_QUOTES.quotes[].sourceUrl [string] -> cat_cotizacion.source_url -- MM_QUOTES.quotes[].packageName [string] -> cat_cotizacion.package_name -- MM_QUOTES.quotes[].includes [array] -> cat_cotizacion_inclusion -- MM_QUOTES.quotes[].includes[] [string] -> cat_cotizacion_inclusion.valor -- MM_QUOTES.quotes[].conditions [array] -> cat_cotizacion_condicion -- MM_QUOTES.quotes[].conditions[] [string] -> cat_cotizacion_condicion.valor -- MM_QUOTES.quotes[].pvpVerified [boolean] -> cat_cotizacion.pvp_verified -- MM_QUOTES.quotes[].childAges[] [number] -> cat_cotizacion_menor.edad -- MM_QUOTES.quotes[].supplierBoard [string] -> cat_cotizacion.supplier_board -- MM_QUOTES.quotes[].sourceOfferUrl [string] -> cat_cotizacion.source_offer_url -- MM_QUOTES.quotes[].parkName [string] -> cat_cotizacion.park_name -- MM_QUOTES.quotes[].variantLabel [string] -> cat_cotizacion.variant_label -- MM_QUOTES.quotes[].sourceType [string] -> cat_cotizacion.source_type -- MM_QUOTES.quotes[].sourceRow [number] -> cat_cotizacion.source_row -- MM_QUOTES.quotes[].minAdultAge [number] -> cat_cotizacion.min_adult_age -- MM_QUOTES.quotes[].sourceSheet [string] -> cat_cotizacion.source_sheet -- MM_QUOTES.quotes[].sourceCell [string] -> cat_cotizacion.source_cell -- MM_QUOTES.quotes[].sourceFile [string] -> cat_cotizacion.source_file -- MM_QUOTES.quotes[].sourcePrice [number] -> cat_cotizacion.source_price -- MM_QUOTES.quotes[].programDays [number] -> cat_cotizacion.program_days -- MM_QUOTES.quotes[].departureCity [string] -> cat_cotizacion.departure_city -- MM_QUOTES.quotes[].departureAirport [string] -> cat_cotizacion.departure_airport -- MM_QUOTES.quotes[].pricePerPerson [number] -> cat_cotizacion.price_per_person -- MM_QUOTES.quotes[].returnDateDiffers [boolean] -> cat_cotizacion.return_date_differs -- MM_QUOTES.checks [array] -> cat_comprobacion -- MM_QUOTES.checks[] [object] -> cat_comprobacion -- MM_QUOTES.checks[].offerId [string] -> cat_comprobacion.offer_id -- MM_QUOTES.checks[].checkIn [string] -> cat_comprobacion.check_in -- MM_QUOTES.checks[].checkOut [string] -> cat_comprobacion.check_out -- MM_QUOTES.checks[].adults [number] -> cat_comprobacion.adults -- MM_QUOTES.checks[].childAges [array] -> cat_comprobacion_menor -- MM_QUOTES.checks[].status [string] -> cat_comprobacion.status -- MM_QUOTES.checks[].note [string] -> cat_comprobacion.note -- MM_QUOTES.checks[].nights [number] -> cat_comprobacion.nights -- MM_QUOTES.checks[].checkedAt [string] -> cat_comprobacion.checked_at -- MM_QUOTES.checks[].result [string] -> cat_comprobacion.result -- MM_QUOTES.checks[].childAges[] [number] -> cat_comprobacion_menor.edad -- MM_QUOTES.pendingChildAges [boolean] -> cat_tarifario.pending_child_ages -- MM_QUOTES.childAgePolicy [object] -> cat_tarifario_politica_edad -- MM_QUOTES.childAgePolicy.min [number] -> cat_tarifario_politica_edad.min -- MM_QUOTES.childAgePolicy.max [number] -> cat_tarifario_politica_edad.max -- MM_QUOTES.childAgePolicy.infantAges [array] -> cat_tarifario_edad_bebe -- MM_QUOTES.childAgePolicy.infantAges[] [number] -> cat_tarifario_edad_bebe.edad -- MM_QUOTES.childAgePolicy.childAges [array] -> cat_tarifario_edad_menor -- MM_QUOTES.childAgePolicy.childAges[] [number] -> cat_tarifario_edad_menor.edad -- MM_QUOTES.childAgePolicy.requireExactAge [boolean] -> cat_tarifario_politica_edad.require_exact_age -- MM_QUOTES.childAgePolicy.pricing [string] -> cat_tarifario_politica_edad.pricing -- MM_QUOTES.months [array] -> cat_tarifario_mes -- MM_QUOTES.months[] [string] -> cat_tarifario_mes.valor -- MM_QUOTES.roundingPolicy [object] -> cat_tarifario_redondeo -- MM_QUOTES.roundingPolicy.incrementEuros [number] -> cat_tarifario_redondeo.increment_euros -- MM_QUOTES.roundingPolicy.direction [string] -> cat_tarifario_redondeo.direction -- MM_QUOTES.roundingPolicy.scope [string] -> cat_tarifario_redondeo.scope -- MM_QUOTES.roundingPolicy.decimals [number] -> cat_tarifario_redondeo.decimals -- MM_OWN_OFFERS [object] -> cat_catalogo_propio -- MM_OWN_OFFERS.source [string] -> cat_catalogo_propio.source -- MM_OWN_OFFERS.checkedAt [string] -> cat_catalogo_propio.checked_at -- MM_OWN_OFFERS.stage [string] -> cat_catalogo_propio.stage -- MM_OWN_OFFERS.offers [array] -> cat_oferta_propia -- MM_OWN_OFFERS.offers[] [object] -> cat_oferta_propia -- MM_OWN_OFFERS.offers[].id [string] -> cat_oferta_propia.id -- MM_OWN_OFFERS.offers[].title [string] -> cat_oferta_propia.title -- MM_OWN_OFFERS.offers[].hotel [string] -> cat_oferta_propia.hotel -- MM_OWN_OFFERS.offers[].stars [number] -> cat_oferta_propia.stars -- MM_OWN_OFFERS.offers[].dateLabel [string] -> cat_oferta_propia.date_label -- MM_OWN_OFFERS.offers[].nights [string] -> cat_oferta_propia.nights -- MM_OWN_OFFERS.offers[].board [string] -> cat_oferta_propia.board -- MM_OWN_OFFERS.offers[].category [string] -> cat_oferta_propia.category -- MM_OWN_OFFERS.offers[].destination [string] -> cat_oferta_propia.destination -- MM_OWN_OFFERS.offers[].sourceUrl [string] -> cat_oferta_propia.source_url -- MM_OWN_OFFERS.offers[].priceStatus [string] -> cat_oferta_propia.price_status -- MM_OWN_OFFERS.offers[].photos [array] -> cat_oferta_propia_foto -- MM_OWN_OFFERS.offers[].photos[] [object] -> cat_oferta_propia_foto -- MM_OWN_OFFERS.offers[].photos[].src [string] -> cat_oferta_propia_foto.src -- MM_OWN_OFFERS.offers[].photos[].alt [string] -> cat_oferta_propia_foto.alt -- MM_OWN_OFFERS.offers[].photos[].caption [string] -> cat_oferta_propia_foto.caption -- MM_OWN_OFFERS.offers[].photos[].sourceUrl [string] -> cat_oferta_propia_foto.source_url -- MM_OWN_OFFERS.offers[].photos[].originalSrc [string] -> cat_oferta_propia_foto.original_src -- MM_OWN_OFFERS.offers[].photos[].kind [string] -> cat_oferta_propia_foto.kind -- MM_OWN_OFFERS.offers[].description [array] -> cat_oferta_propia_descripcion -- MM_OWN_OFFERS.offers[].description[] [string] -> cat_oferta_propia_descripcion.valor -- MM_OWN_OFFERS.offers[].migrationPhase [string] -> cat_oferta_propia.migration_phase -- MM_OWN_OFFERS.offers[].poster [string] -> cat_oferta_propia.poster -- MM_OWN_OFFERS.offers[].maxChildAge [number] -> cat_oferta_propia.max_child_age -- MM_OWN_OFFERS.offers[].posterSource [string] -> cat_oferta_propia.poster_source -- MM_OWN_OFFERS.offers[].photoNote [string] -> cat_oferta_propia.photo_note -- MM_OWN_OFFERS.offers[].photos[].credit [string] -> cat_oferta_propia_foto.credit -- MM_OWN_OFFERS.offers[].photos[].license [string] -> cat_oferta_propia_foto.license -- MM_OWN_OFFERS.offers[].posterClean [boolean] -> cat_oferta_propia.poster_clean -- MM_OWN_OFFERS.offers[].posterDesignUrl [string] -> cat_oferta_propia.poster_design_url -- MM_OWN_OFFERS.offers[].photos[].licenseUrl [string] -> cat_oferta_propia_foto.license_url -- MM_OWN_OFFERS.offers[].photos[].originalUrl [string] -> cat_oferta_propia_foto.original_url -- MM_OWN_OFFERS.offers[].photos[].author [string] -> cat_oferta_propia_foto.author -- MM_OWN_OFFERS.offers[].posterVisibleFraction [number] -> cat_oferta_propia.poster_visible_fraction -- MM_OWN_OFFERS.offers[].posterRatio [number] -> cat_oferta_propia.poster_ratio -- MM_OWN_RATES{offerId} [object] -> cat_tarifa_propia -- MM_OWN_RATES{offerId}.apiId [number] -> cat_tarifa_propia.api_id -- MM_OWN_RATES{offerId}.id [string] -> cat_tarifa_propia.id -- MM_OWN_RATES{offerId}.title [string] -> cat_tarifa_propia.title -- MM_OWN_RATES{offerId}.hotel [string] -> cat_tarifa_propia.hotel -- MM_OWN_RATES{offerId}.photos [array] -> cat_tarifa_propia_foto -- MM_OWN_RATES{offerId}.photos[] [object] -> cat_tarifa_propia_foto -- MM_OWN_RATES{offerId}.photos[].src [string] -> cat_tarifa_propia_foto.src -- MM_OWN_RATES{offerId}.photos[].alt [string] -> cat_tarifa_propia_foto.alt -- MM_OWN_RATES{offerId}.photos[].caption [string] -> cat_tarifa_propia_foto.caption -- MM_OWN_RATES{offerId}.photos[].sourceUrl [string] -> cat_tarifa_propia_foto.source_url -- MM_OWN_RATES{offerId}.photos[].originalSrc [string] -> cat_tarifa_propia_foto.original_src -- MM_OWN_RATES{offerId}.photos[].kind [string] -> cat_tarifa_propia_foto.kind -- MM_OWN_RATES{offerId}.board [string] -> cat_tarifa_propia.board -- MM_OWN_RATES{offerId}.service [boolean] -> cat_tarifa_propia.service -- MM_OWN_RATES{offerId}.description [array] -> cat_tarifa_propia_descripcion -- MM_OWN_RATES{offerId}.description[] [string] -> cat_tarifa_propia_descripcion.valor -- MM_OWN_RATES{offerId}.detail [object] -> cat_tarifa_propia_detalle -- MM_OWN_RATES{offerId}.detail.id [number] -> cat_tarifa_propia_detalle.id -- MM_OWN_RATES{offerId}.detail.titulo [string] -> cat_tarifa_propia_detalle.titulo -- MM_OWN_RATES{offerId}.detail.subtitulo [string] -> cat_tarifa_propia_detalle.subtitulo -- MM_OWN_RATES{offerId}.detail.descripcion [string] -> cat_tarifa_propia_detalle.descripcion -- MM_OWN_RATES{offerId}.detail.fechaInicio [string] -> cat_tarifa_propia_detalle.fecha_inicio -- MM_OWN_RATES{offerId}.detail.fechaFin [string] -> cat_tarifa_propia_detalle.fecha_fin -- MM_OWN_RATES{offerId}.detail.urlWeb [null] -> cat_tarifa_propia_detalle.url_web -- MM_OWN_RATES{offerId}.detail.precioEnero [number] -> cat_tarifa_propia_detalle.precio_enero -- MM_OWN_RATES{offerId}.detail.precioFebrero [number] -> cat_tarifa_propia_detalle.precio_febrero -- MM_OWN_RATES{offerId}.detail.precioMarzo [number] -> cat_tarifa_propia_detalle.precio_marzo -- MM_OWN_RATES{offerId}.detail.precioAbril [number] -> cat_tarifa_propia_detalle.precio_abril -- MM_OWN_RATES{offerId}.detail.precioMayo [number] -> cat_tarifa_propia_detalle.precio_mayo -- MM_OWN_RATES{offerId}.detail.precioJunio [number] -> cat_tarifa_propia_detalle.precio_junio -- MM_OWN_RATES{offerId}.detail.precioJulio [number] -> cat_tarifa_propia_detalle.precio_julio -- MM_OWN_RATES{offerId}.detail.precioAgosto [number] -> cat_tarifa_propia_detalle.precio_agosto -- MM_OWN_RATES{offerId}.detail.precioSeptiembre [number] -> cat_tarifa_propia_detalle.precio_septiembre -- MM_OWN_RATES{offerId}.detail.precioOctubre [number] -> cat_tarifa_propia_detalle.precio_octubre -- MM_OWN_RATES{offerId}.detail.precioNoviembre [number] -> cat_tarifa_propia_detalle.precio_noviembre -- MM_OWN_RATES{offerId}.detail.precioDiciembre [number] -> cat_tarifa_propia_detalle.precio_diciembre -- MM_OWN_RATES{offerId}.detail.precioCualquierMes [number] -> cat_tarifa_propia_detalle.precio_cualquier_mes -- MM_OWN_RATES{offerId}.detail.precioAnticipo [number] -> cat_tarifa_propia_detalle.precio_anticipo -- MM_OWN_RATES{offerId}.detail.orden [number] -> cat_tarifa_propia_detalle.orden -- MM_OWN_RATES{offerId}.detail.publicada [boolean] -> cat_tarifa_propia_detalle.publicada -- MM_OWN_RATES{offerId}.detail.imagen [object] -> cat_tarifa_propia_imagen_principal -- MM_OWN_RATES{offerId}.detail.imagen.imagen [null] -> cat_tarifa_propia_imagen_principal.imagen -- MM_OWN_RATES{offerId}.detail.imagen.mime [null] -> cat_tarifa_propia_imagen_principal.mime -- MM_OWN_RATES{offerId}.detail.imagen.url [string] -> cat_tarifa_propia_imagen_principal.url -- MM_OWN_RATES{offerId}.detail.imagen.id [null] -> cat_tarifa_propia_imagen_principal.id -- MM_OWN_RATES{offerId}.detail.imagenes [array] -> cat_tarifa_propia_imagen -- MM_OWN_RATES{offerId}.detail.imagenes[] [object] -> cat_tarifa_propia_imagen -- MM_OWN_RATES{offerId}.detail.imagenes[].imagen [null] -> cat_tarifa_propia_imagen.imagen -- MM_OWN_RATES{offerId}.detail.imagenes[].mime [null] -> cat_tarifa_propia_imagen.mime -- MM_OWN_RATES{offerId}.detail.imagenes[].url [string] -> cat_tarifa_propia_imagen.url -- MM_OWN_RATES{offerId}.detail.imagenes[].id [null] -> cat_tarifa_propia_imagen.id -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeEnero [number] -> cat_tarifa_propia_detalle.precio_primer_peque_enero -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeEnero [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_enero -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeFebrero [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_febrero -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeFebrero [number] -> cat_tarifa_propia_detalle.precio_primer_peque_febrero -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeMarzo [number] -> cat_tarifa_propia_detalle.precio_primer_peque_marzo -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeMarzo [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_marzo -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeAbril [number] -> cat_tarifa_propia_detalle.precio_primer_peque_abril -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeAbril [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_abril -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeMayo [number] -> cat_tarifa_propia_detalle.precio_primer_peque_mayo -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeMayo [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_mayo -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeJunio [number] -> cat_tarifa_propia_detalle.precio_primer_peque_junio -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeJunio [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_junio -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeJulio [number] -> cat_tarifa_propia_detalle.precio_primer_peque_julio -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeJulio [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_julio -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeAgosto [number] -> cat_tarifa_propia_detalle.precio_primer_peque_agosto -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeAgosto [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_agosto -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeSeptiembre [number] -> cat_tarifa_propia_detalle.precio_primer_peque_septiembre -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeSeptiembre [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_septiembre -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeOctubre [number] -> cat_tarifa_propia_detalle.precio_primer_peque_octubre -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeOctubre [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_octubre -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeNoviembre [number] -> cat_tarifa_propia_detalle.precio_primer_peque_noviembre -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeNoviembre [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_noviembre -- MM_OWN_RATES{offerId}.detail.precioPrimerPequeDiciembre [number] -> cat_tarifa_propia_detalle.precio_primer_peque_diciembre -- MM_OWN_RATES{offerId}.detail.precioSegundoPequeDiciembre [number] -> cat_tarifa_propia_detalle.precio_segundo_peque_diciembre -- MM_OWN_RATES{offerId}.detail.disponibilidadEnero [array|null] -> cat_tarifa_propia_detalle.disponibilidad_enero_json -- MM_OWN_RATES{offerId}.detail.disponibilidadFebrero [array|null] -> cat_tarifa_propia_detalle.disponibilidad_febrero_json -- MM_OWN_RATES{offerId}.detail.disponibilidadMarzo [array|null] -> cat_tarifa_propia_detalle.disponibilidad_marzo_json -- MM_OWN_RATES{offerId}.detail.disponibilidadAbril [array|null] -> cat_tarifa_propia_detalle.disponibilidad_abril_json -- MM_OWN_RATES{offerId}.detail.disponibilidadMayo [null] -> cat_tarifa_propia_detalle.disponibilidad_mayo -- MM_OWN_RATES{offerId}.detail.disponibilidadJunio [array|null] -> cat_tarifa_propia_detalle.disponibilidad_junio_json -- MM_OWN_RATES{offerId}.detail.disponibilidadJulio [array|null] -> cat_tarifa_propia_detalle.disponibilidad_julio_json -- MM_OWN_RATES{offerId}.detail.disponibilidadAgosto [array|null] -> cat_tarifa_propia_detalle.disponibilidad_agosto_json -- MM_OWN_RATES{offerId}.detail.disponibilidadSeptiembre [array|null] -> cat_tarifa_propia_detalle.disponibilidad_septiembre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadOctubre [array|null] -> cat_tarifa_propia_detalle.disponibilidad_octubre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadNoviembre [array|null] -> cat_tarifa_propia_detalle.disponibilidad_noviembre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadDiciembre [array|null] -> cat_tarifa_propia_detalle.disponibilidad_diciembre_json -- MM_OWN_RATES{offerId}.detail.minNoches [number] -> cat_tarifa_propia_detalle.min_noches -- MM_OWN_RATES{offerId}.detail.maxNoches [number] -> cat_tarifa_propia_detalle.max_noches -- MM_OWN_RATES{offerId}.detail.nombreHotel [string] -> cat_tarifa_propia_detalle.nombre_hotel -- MM_OWN_RATES{offerId}.detail.numeroEstrellasHotel [null|number] -> cat_tarifa_propia_detalle.numero_estrellas_hotel -- MM_OWN_RATES{offerId}.detail.regimen [string] -> cat_tarifa_propia_detalle.regimen -- MM_OWN_RATES{offerId}.detail.esOferta [boolean] -> cat_tarifa_propia_detalle.es_oferta -- MM_OWN_RATES{offerId}.detail.precioTercerPequeJulio [number] -> cat_tarifa_propia_detalle.precio_tercer_peque_julio -- MM_OWN_RATES{offerId}.detail.precioTercerPequeAgosto [number] -> cat_tarifa_propia_detalle.precio_tercer_peque_agosto -- MM_OWN_RATES{offerId}.detail.tieneTercerPeque [boolean] -> cat_tarifa_propia_detalle.tiene_tercer_peque -- MM_OWN_RATES{offerId}.detail.edadMaximaPeque [number] -> cat_tarifa_propia_detalle.edad_maxima_peque -- MM_OWN_RATES{offerId}.detail.sinPagoPrevio [boolean] -> cat_tarifa_propia_detalle.sin_pago_previo -- MM_OWN_RATES{offerId}.detail.categorias [array] -> cat_tarifa_propia_categoria -- MM_OWN_RATES{offerId}.detail.categorias[] [object] -> cat_tarifa_propia_categoria -- MM_OWN_RATES{offerId}.detail.categorias[].id [number] -> cat_tarifa_propia_categoria.id -- MM_OWN_RATES{offerId}.detail.categorias[].nombre [string] -> cat_tarifa_propia_categoria.nombre -- MM_OWN_RATES{offerId}.detail.categorias[].esExterno [boolean] -> cat_tarifa_propia_categoria.es_externo -- MM_OWN_RATES{offerId}.detail.categorias[].url [null] -> cat_tarifa_propia_categoria.url -- MM_OWN_RATES{offerId}.detail.categorias[].activo [boolean] -> cat_tarifa_propia_categoria.activo -- MM_OWN_RATES{offerId}.detail.categorias[].subCategories [null] -> cat_tarifa_propia_categoria.sub_categories -- MM_OWN_RATES{offerId}.detail.categorias[].orden [number] -> cat_tarifa_propia_categoria.orden -- MM_OWN_RATES{offerId}.detail.categorias[].imagen [null] -> cat_tarifa_propia_categoria.imagen -- MM_OWN_RATES{offerId}.detail.categorias[].imagenNombre [null] -> cat_tarifa_propia_categoria.imagen_nombre -- MM_OWN_RATES{offerId}.detail.proveedorId [null] -> cat_tarifa_propia_detalle.proveedor_id -- MM_OWN_RATES{offerId}.detail.hotelId [null] -> cat_tarifa_propia_detalle.hotel_id -- MM_OWN_RATES{offerId}.detail.proveedorRegimen [null] -> cat_tarifa_propia_detalle.proveedor_regimen -- MM_OWN_RATES{offerId}.detail.fechasSinDisponibilidad [array] -> cat_tarifa_propia_fecha_bloqueada -- MM_OWN_RATES{offerId}.photos[].credit [string] -> cat_tarifa_propia_foto.credit -- MM_OWN_RATES{offerId}.photos[].license [string] -> cat_tarifa_propia_foto.license -- MM_OWN_RATES{offerId}.photos[].licenseUrl [string] -> cat_tarifa_propia_foto.license_url -- MM_OWN_RATES{offerId}.photos[].originalUrl [string] -> cat_tarifa_propia_foto.original_url -- MM_OWN_RATES{offerId}.detail.disponibilidadEnero[] [string] -> cat_tarifa_propia_detalle.disponibilidad_enero_json -- MM_OWN_RATES{offerId}.detail.disponibilidadFebrero[] [string] -> cat_tarifa_propia_detalle.disponibilidad_febrero_json -- MM_OWN_RATES{offerId}.detail.disponibilidadMarzo[] [string] -> cat_tarifa_propia_detalle.disponibilidad_marzo_json -- MM_OWN_RATES{offerId}.detail.fechasSinDisponibilidad[] [string] -> cat_tarifa_propia_fecha_bloqueada.valor -- MM_OWN_RATES{offerId}.photos[].author [string] -> cat_tarifa_propia_foto.author -- MM_OWN_RATES{offerId}.detail.disponibilidadJunio[] [string] -> cat_tarifa_propia_detalle.disponibilidad_junio_json -- MM_OWN_RATES{offerId}.detail.disponibilidadJulio[] [string] -> cat_tarifa_propia_detalle.disponibilidad_julio_json -- MM_OWN_RATES{offerId}.detail.disponibilidadAgosto[] [string] -> cat_tarifa_propia_detalle.disponibilidad_agosto_json -- MM_OWN_RATES{offerId}.detail.disponibilidadSeptiembre[] [string] -> cat_tarifa_propia_detalle.disponibilidad_septiembre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadOctubre[] [string] -> cat_tarifa_propia_detalle.disponibilidad_octubre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadNoviembre[] [string] -> cat_tarifa_propia_detalle.disponibilidad_noviembre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadDiciembre[] [string] -> cat_tarifa_propia_detalle.disponibilidad_diciembre_json -- MM_OWN_RATES{offerId}.detail.disponibilidadAbril[] [string] -> cat_tarifa_propia_detalle.disponibilidad_abril_json -- Actualizacion comercial: visible=false retira ofertas; se eliminan sus tarifas -- solicitables del catalogo publico y privado. Los precios infantiles por noche -- se documentan en conditions[] y las edades autorizadas en childAges[]. -- El total familiar conserva el total adulto exacto y suma incrementos por noche; -- price aplica el redondeo comercial, supplierPvp conserva el total exacto. -- Las tarifas familiares verificadas para una edad concreta conservan childAges exactas. -- No se extrapolan a otras edades sin confirmacion. Las entradas vencidas se retiran -- de quotes y checks, conservando las fechas futuras del mismo alojamiento. -- 30. COTIZADORES, CRUCEROS Y PETICION A MEDIDA -- Modelo documental SQLite 3.31+. No se aplica automaticamente al servidor. -- No contiene registros, precios reales, textos comerciales ni datos personales. -- Los importes *_cents son enteros en centimos. Los JSON conservan la forma -- original documentada al final de este fragmento, sin serializar su contenido. -- Las claves *_id de enlace son claves del modelo, no identificadores de clientes. -- Un precio orientativo no es disponibilidad confirmada. CREATE TABLE IF NOT EXISTS trip_catalog ( id TEXT PRIMARY KEY, name TEXT NOT NULL, eyebrow TEXT, headline TEXT, intro TEXT, hero_json TEXT, stops_json TEXT, nights_json TEXT, included_json TEXT, excluded_json TEXT, properties_json TEXT ); CREATE TABLE IF NOT EXISTS trip_photo ( id INTEGER PRIMARY KEY, src TEXT NOT NULL, alt TEXT, credit TEXT, source TEXT, license TEXT, author_url TEXT, destination TEXT, location TEXT, position TEXT, excursion INTEGER CHECK (excursion IN (0,1)), itinerary_only INTEGER CHECK (itinerary_only IN (0,1)) ); CREATE TABLE IF NOT EXISTS trip_city ( id INTEGER PRIMARY KEY, catalog_id TEXT NOT NULL REFERENCES trip_catalog(id), sort_order INTEGER NOT NULL, name TEXT NOT NULL, caption TEXT, description TEXT, photo_id INTEGER REFERENCES trip_photo(id), UNIQUE(catalog_id, sort_order) ); CREATE TABLE IF NOT EXISTS trip_hotel ( id TEXT PRIMARY KEY, catalog_id TEXT REFERENCES trip_catalog(id), name TEXT NOT NULL, city TEXT, note TEXT, source TEXT, photo TEXT, credit TEXT, verified_at TEXT, occupancies_json TEXT, properties_json TEXT ); CREATE TABLE IF NOT EXISTS trip_hotel_photo ( hotel_id TEXT NOT NULL REFERENCES trip_hotel(id), sort_order INTEGER NOT NULL, photo_id INTEGER NOT NULL REFERENCES trip_photo(id), PRIMARY KEY(hotel_id, sort_order) ); CREATE TABLE IF NOT EXISTS trip_tour ( id TEXT PRIMARY KEY, catalog_id TEXT REFERENCES trip_catalog(id), city TEXT, name TEXT, title TEXT, note TEXT, top INTEGER CHECK (top IN (0,1)), adult_cents INTEGER, child_cents INTEGER, child_min_age INTEGER, child_max_age INTEGER, photo_id INTEGER REFERENCES trip_photo(id) ); -- child_cents NULL no equivale a gratis: esa tarifa infantil no esta disponible. CREATE TABLE IF NOT EXISTS trip_tariff_configuration ( id TEXT PRIMARY KEY, catalog_id TEXT REFERENCES trip_catalog(id), source TEXT, year INTEGER, enabled_months_json TEXT, nights_per_city INTEGER, train_cents_per_traveler_per_leg INTEGER, train_legs INTEGER, transfer_cents_per_service_json TEXT, markup_percent REAL, confirmed_json TEXT, restrictions_json TEXT, departure_range_json TEXT, departure_exclusion_json TEXT, flight_rules_json TEXT, rounding_json TEXT, tariff_basis TEXT, tariff_basis_status TEXT, original_cells_json TEXT ); CREATE TABLE IF NOT EXISTS trip_hotel_tariff ( id INTEGER PRIMARY KEY, configuration_id TEXT NOT NULL REFERENCES trip_tariff_configuration(id), city TEXT NOT NULL, occupancy TEXT NOT NULL, month INTEGER CHECK (month BETWEEN 1 AND 12), year INTEGER, nightly_cents INTEGER, source TEXT, unit TEXT ); CREATE TABLE IF NOT EXISTS trip_cost_item ( id TEXT PRIMARY KEY, catalog_id TEXT REFERENCES trip_catalog(id), unit_cents INTEGER, quantity INTEGER, nights INTEGER ); CREATE TABLE IF NOT EXISTS trip_proposal ( id TEXT PRIMARY KEY, catalog_id TEXT REFERENCES trip_catalog(id), revision INTEGER, kind TEXT, quote_mode TEXT, title TEXT, country TEXT, client TEXT, reference TEXT, departure_date TEXT, arrival_date TEXT, home_date TEXT, travelers INTEGER, intro TEXT, included TEXT, excluded TEXT, conditions TEXT, base_price_cents INTEGER, price_cents INTEGER, price_note TEXT, full_transfers INTEGER CHECK (full_transfers IN (0,1)), agent TEXT, email TEXT, phone TEXT, updated_at TEXT, costs_json TEXT, days_json TEXT, tour_selection_json TEXT, properties_json TEXT ); -- costs_json: $.base:string, $.flight:string, $.extras:string, $.checked:boolean. -- Se mantienen strings de entrada vacios y cantidades editables; no se supone -- que una plantilla con priceCents NULL tenga precio de venta calculado. -- days_json: $[dayOffset].title:string; $[dayOffset].body:string. CREATE TABLE IF NOT EXISTS trip_stage ( proposal_id TEXT NOT NULL REFERENCES trip_proposal(id), id TEXT NOT NULL, sort_order INTEGER NOT NULL, city TEXT, nights INTEGER, hotel TEXT, category TEXT, board TEXT, description TEXT, photo TEXT, hotel_photos_json TEXT, PRIMARY KEY(proposal_id,id), UNIQUE(proposal_id,sort_order) ); CREATE TABLE IF NOT EXISTS trip_flight ( proposal_id TEXT NOT NULL REFERENCES trip_proposal(id), id TEXT NOT NULL, sort_order INTEGER NOT NULL, from_location TEXT, to_location TEXT, carrier TEXT, number TEXT, departure TEXT, arrival TEXT, baggage TEXT, PRIMARY KEY(proposal_id,id) ); -- from_location y to_location representan las propiedades JS from y to. CREATE TABLE IF NOT EXISTS trip_proposal_photo ( proposal_id TEXT NOT NULL REFERENCES trip_proposal(id), sort_order INTEGER NOT NULL, photo_id INTEGER NOT NULL REFERENCES trip_photo(id), PRIMARY KEY(proposal_id,sort_order) ); CREATE TABLE IF NOT EXISTS trip_proposal_tour ( proposal_id TEXT NOT NULL REFERENCES trip_proposal(id), tour_id TEXT NOT NULL REFERENCES trip_tour(id), selected_adults INTEGER, selected_children INTEGER, PRIMARY KEY(proposal_id,tour_id) ); CREATE TABLE IF NOT EXISTS trip_estimate_input ( id TEXT PRIMARY KEY, country TEXT NOT NULL, departure TEXT NOT NULL, nights_json TEXT NOT NULL, tour_selection_json TEXT, full_transfers INTEGER CHECK (full_transfers IN (0,1)), interests_json TEXT ); -- tour_selection_json: $[tourId].adults:integer; $[tourId].children:integer. -- nights_json: $[]:integer. interests_json: $[]:string. CREATE TABLE IF NOT EXISTS trip_estimate ( id TEXT PRIMARY KEY, input_id TEXT NOT NULL REFERENCES trip_estimate_input(id), proposal_id TEXT REFERENCES trip_proposal(id), services_price_cents INTEGER, package_price_cents INTEGER, options_price_cents INTEGER, total_price_cents INTEGER, travelers INTEGER, total_nights INTEGER, notice TEXT, tariff_fallback INTEGER CHECK (tariff_fallback IN (0,1)) ); CREATE TABLE IF NOT EXISTS trip_booking_input ( request_id TEXT PRIMARY KEY, input_id TEXT NOT NULL REFERENCES trip_estimate_input(id), expected_total_cents INTEGER, contact_name TEXT, contact_email TEXT, contact_phone TEXT, contact_notes TEXT, accepted INTEGER CHECK (accepted IN (0,1)) ); -- Contrato de formulario encontrado en el codigo. Su existencia no activa la -- ruta de reserva del cotizador: el puente publicado bloquea /api/webs/reservas. -- El contrato /api/webs/estimar si calcula presupuestos orientativos. CREATE TABLE IF NOT EXISTS trip_local_draft ( country TEXT PRIMARY KEY, departure TEXT, nights_json TEXT, selected_json TEXT, full_transfers INTEGER CHECK (full_transfers IN (0,1)) ); -- Refleja sessionStorage. selected_json: $[]:string (intereses seleccionados). CREATE TABLE IF NOT EXISTS trip_local_options ( proposal_id TEXT NOT NULL, proposal_updated_at TEXT NOT NULL, selection_json TEXT, transfers INTEGER CHECK (transfers IN (0,1)), PRIMARY KEY(proposal_id,proposal_updated_at) ); -- Refleja localStorage; no implica persistencia en servidor. CREATE TABLE IF NOT EXISTS trip_destination_guide ( slug TEXT PRIMARY KEY, name TEXT, kicker TEXT, headline TEXT, intro TEXT, photos_json TEXT, sections_json TEXT, tips_json TEXT, source TEXT ); -- sections_json: $[].title:string; $[].text:string. tips_json: $[]:string. -- Funciones del dominio de viajes (implementadas en JavaScript, no en SQL): -- MMTravel.travelBudget / gt: valida entrada, calcula servicios + paquete + -- opciones y construye propuesta, etapas, vuelos previstos y galerias. -- dt: fecha futura valida; 3 etapas Japon o 4 Tailandia, 1..60 noches por etapa, -- maximo total 110. Cotizador web actual para dos adultos. Comprueba tours, -- participantes y traslados; puede usar tarifa hotelera alternativa orientativa. -- Ve/ue: catalogo de tours, reglas infantiles, descripcion y prioridad. -- Je: filtra seleccion valida de tours. Qe: suma importes por participantes. -- Re: transforma coste en PVP segun regla vigente. pe: suma dias a fecha valida. -- travelQuote: recalcula y compara expectedTotalCents; produce offer y quote. -- travelRoute: valida origen, formato/tamano JSON, entrada y contacto; distingue -- estimacion y solicitud. La publicacion limita sus rutas como se indica arriba. -- mp: proyecta start/end de cada etapa desde arrivalDate y sus noches. -- rM: genera dias del itinerario y aplica las notas days[dayOffset]. -- gp/ml: validacion de participantes y suma de excursiones en la interfaz. -- createProposalPdf/downloadProposalPdf/loadPdfImages/pdfPhotoSources: -- composicion y descarga de resumen PDF y sus fotos; impresion del resumen. -- Galeria: apertura, anterior/siguiente y cierre con retorno de foco. -- Configurador: borrador de sesion, intereses, noches, tours, traslados, -- presupuesto por URL y descarga de idea en texto. No bloquea plazas. -- Proyecciones transitorias, sin persistencia propia: -- trip.stageProjection[].start:string(date); trip.stageProjection[].end:string(date) -- trip.dayProjection[].index:integer; trip.dayProjection[].noteKey:string; -- trip.dayProjection[].date:string(date); trip.dayProjection[].title:string; -- trip.dayProjection[].body:string; trip.dayProjection[].stage:object(trip_stage); -- trip.dayProjection[].transfer:boolean. -- trip.pdfPhotoSources.hero:object(trip_photo); trip.pdfPhotoSources.hotels[]:object(trip_photo). -- trip.pdfImages.*.data:string(data-url); trip.pdfImages.*.width:integer; -- trip.pdfImages.*.height:integer. Solo recursos transitorios para componer PDF. -- CRUCEROS: catalogo y calculo de demostracion. Barcos pueden ser reales; -- las salidas, tarifas, disponibilidad, promociones y camarotes son simulados. -- Favoritos y simulaciones se conservan en el navegador; sin reservas ni pagos. CREATE TABLE IF NOT EXISTS cruise_line ( id TEXT PRIMARY KEY, name TEXT NOT NULL, simulated INTEGER CHECK (simulated IN (0,1)) ); CREATE TABLE IF NOT EXISTS cruise_ship ( id TEXT PRIMARY KEY, name TEXT NOT NULL, cruise_line_id TEXT NOT NULL REFERENCES cruise_line(id), simulated INTEGER CHECK (simulated IN (0,1)), information_json TEXT ); CREATE TABLE IF NOT EXISTS cruise_port ( id TEXT PRIMARY KEY, name TEXT NOT NULL, country TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS cruise_itinerary ( id TEXT PRIMARY KEY ); CREATE TABLE IF NOT EXISTS cruise_itinerary_day ( itinerary_id TEXT NOT NULL REFERENCES cruise_itinerary(id), day INTEGER NOT NULL, port_id TEXT REFERENCES cruise_port(id), description TEXT, PRIMARY KEY(itinerary_id,day) ); CREATE TABLE IF NOT EXISTS cruise_product ( id TEXT PRIMARY KEY, title TEXT NOT NULL, ship_id TEXT NOT NULL REFERENCES cruise_ship(id), destination TEXT, itinerary_id TEXT NOT NULL REFERENCES cruise_itinerary(id), nights INTEGER, family INTEGER CHECK (family IN (0,1)), luxury INTEGER CHECK (luxury IN (0,1)), image TEXT, image_alt TEXT ); CREATE TABLE IF NOT EXISTS cruise_sailing ( id TEXT PRIMARY KEY, cruise_id TEXT NOT NULL REFERENCES cruise_product(id), departure_date TEXT NOT NULL, departure_port_id TEXT REFERENCES cruise_port(id), availability TEXT, price_rounding TEXT, price_currency TEXT, price_adult_fare_cents INTEGER, price_port_tax_cents INTEGER, price_service_per_night_cents INTEGER, price_simulated INTEGER CHECK (price_simulated IN (0,1)) ); CREATE TABLE IF NOT EXISTS cruise_cabin_category ( id TEXT PRIMARY KEY, name TEXT, description TEXT, fare_multiplier REAL, capacity INTEGER, features_json TEXT ); CREATE TABLE IF NOT EXISTS cruise_extra ( id TEXT PRIMARY KEY, name TEXT, description TEXT, unit TEXT, unit_price_cents INTEGER, unit_label TEXT, simulated INTEGER CHECK (simulated IN (0,1)) ); CREATE TABLE IF NOT EXISTS cruise_photo ( id INTEGER PRIMARY KEY, ship_id TEXT REFERENCES cruise_ship(id), sort_order INTEGER, properties_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS cruise_search ( id TEXT PRIMARY KEY, destination TEXT, port_id TEXT, date_from TEXT, date_to TEXT, adults INTEGER, child_ages_json TEXT, duration TEXT, category TEXT, max_total_cents INTEGER, line_ids_json TEXT, sort TEXT ); -- Borrador de busqueda y filtros de pantalla, no historial servidor. CREATE TABLE IF NOT EXISTS cruise_selection ( id TEXT PRIMARY KEY, sailing_id TEXT REFERENCES cruise_sailing(id), adults INTEGER, child_ages_json TEXT, cabin_category_id TEXT REFERENCES cruise_cabin_category(id), extra_ids_json TEXT ); CREATE TABLE IF NOT EXISTS cruise_estimate ( id TEXT PRIMARY KEY, selection_id TEXT NOT NULL REFERENCES cruise_selection(id), rounding TEXT, adult_unit_cents INTEGER, child_unit_cents_json TEXT, base_fare_cents INTEGER, cabin_supplement_cents INTEGER, fare_cents INTEGER, port_tax_cents INTEGER, service_cents INTEGER, extra_lines_json TEXT, extras_cents INTEGER, total_cents INTEGER, cabin_count INTEGER, allocations_json TEXT, simulated INTEGER CHECK (simulated IN (0,1)) ); -- allocations_json: $[].index:integer; $[].adults:integer; -- $[].childAges[]:integer. extra_lines_json: $[].extra:cruise_extra; -- $[].quantity:integer; $[].totalCents:integer. CREATE TABLE IF NOT EXISTS cruise_snapshot ( id TEXT PRIMARY KEY, catalog_json TEXT NOT NULL ); -- catalog_json: $.cruises[]:cruise_product; $.lines[]:cruise_line; -- $.ships[]:cruise_ship; $.sailings[]:cruise_sailing; -- $.itineraries[]:cruise_itinerary+days[]; $.ports[]:cruise_port; -- $.cabins[]:cruise_cabin_category; $.extras[]:cruise_extra. CREATE TABLE IF NOT EXISTS cruise_local_workspace ( id TEXT PRIMARY KEY, version INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS cruise_favorite ( workspace_id TEXT NOT NULL REFERENCES cruise_local_workspace(id), key TEXT NOT NULL, snapshot_id TEXT NOT NULL REFERENCES cruise_snapshot(id), PRIMARY KEY(workspace_id,key) ); CREATE TABLE IF NOT EXISTS cruise_promotion ( offer_key TEXT PRIMARY KEY, workspace_version INTEGER, started_at TEXT, expires_at TEXT ); CREATE TABLE IF NOT EXISTS cruise_promotion_estimate ( id TEXT PRIMARY KEY, estimate_id TEXT NOT NULL REFERENCES cruise_estimate(id), promotion_key TEXT REFERENCES cruise_promotion(offer_key), eligible_cents INTEGER, active INTEGER CHECK (active IN (0,1)), percentage_discount_cents INTEGER, rounding_cents INTEGER, discount_cents INTEGER, total_cents INTEGER ); CREATE TABLE IF NOT EXISTS cruise_simulation ( id TEXT PRIMARY KEY, workspace_id TEXT REFERENCES cruise_local_workspace(id), created_at TEXT, updated_at TEXT, status TEXT, origin TEXT, snapshot_id TEXT NOT NULL REFERENCES cruise_snapshot(id), selection_id TEXT NOT NULL REFERENCES cruise_selection(id), promotion_json TEXT ); -- promotion_json: $.startedAt:string(datetime); $.expiresAt:string(datetime). -- No tabla de reservas reales de cruceros: no existe ese servicio en la DEMO. -- Funciones del dominio de cruceros (JavaScript): -- jf: genera catalogo DEMO de navieras, barcos, rutas, puertos y salidas. -- getCatalog/getSailing/search: relaciona catalogo y filtra destino, puerto, -- fechas, duracion, categoria, naviera, precio y ultima hora simulada. -- Qm/Pf: validan fechas/grupo (adultos 1..8; menores 0..17, maximo 6). -- Ff: distribuye grupo en camarotes con un adulto minimo y capacidad maxima. -- Nf: factor infantil de la demostracion. Mf: redondeo inferior de PVP a 5 EUR. -- If/quoteSimulation: calculo base, suplemento, tasas, servicios, extras, -- reparto y total. Rechaza extras duplicados; no consulta disponibilidad real. -- gh/_h/vh: crea/reconstruye/valida instantanea y sus relaciones. -- mh/hh/Sh: claves de oferta y normalizacion de seleccion para deduplicar. -- yh/bh/xh: validan/leen/escriben almacenamiento local con limites. -- Ch/Th/wh: guarda simulacion, genera ejemplos y compara con catalogo actual. -- Dm/Em: vigencia de promocion local y total simulado con descuento/redondeo. -- Favoritos: agregar/quitar (maximo 60); simulaciones: maximo 50, editar, -- eliminar, cambiar estado, filtrar y borrar datos locales. Comparador: hasta 3. -- Galeria y detalle: cambio de foto, ruta diaria, categoria, viajeros, extras. -- search_demo_cruises: herramienta de interfaz para filtrar el catalogo DEMO. CREATE TABLE IF NOT EXISTS tailor_request_draft ( id TEXT PRIMARY KEY, destination TEXT, unsure INTEGER CHECK (unsure IN (0,1)), dates TEXT, adults INTEGER CHECK (adults BETWEEN 1 AND 12), children INTEGER CHECK (children BETWEEN 0 AND 4), child_ages_json TEXT, budget_eur INTEGER, notes TEXT ); -- child_ages_json: $[]:integer (0..17); formulario childAge{index}. -- MMTailor.open: abre dialogo y restaura foco al cerrar. -- Cambio unsure: deshabilita destino cuando se solicitan ideas. -- Cambio children: genera selector de edad por menor y conserva seleccion. -- Submit: valida campos; compone resumen y enlace de WhatsApp codificado. -- El cliente revisa y envia desde WhatsApp. No hay envio automatico ni registro -- de esta peticion en servidor. Esta tabla es una representacion documental. CREATE INDEX IF NOT EXISTS trip_stage_order_idx ON trip_stage(proposal_id,sort_order); CREATE INDEX IF NOT EXISTS trip_hotel_tariff_lookup_idx ON trip_hotel_tariff(configuration_id,city,occupancy,year,month); CREATE INDEX IF NOT EXISTS cruise_sailing_departure_idx ON cruise_sailing(departure_date,departure_port_id); CREATE INDEX IF NOT EXISTS cruise_simulation_status_idx ON cruise_simulation(workspace_id,status,updated_at); -- DICCIONARIO DE PROPIEDADES ORIGINALES (sin valores). Se completa mediante -- inspeccion estructural del motor y de los contratos originales de la UI. -- cruise.catalog : object -- cruise.catalog.cabins : array -- cruise.catalog.cabins[] : object -- cruise.catalog.cabins[].capacity : number -- cruise.catalog.cabins[].description : string -- cruise.catalog.cabins[].fareMultiplier : number -- cruise.catalog.cabins[].features : array -- cruise.catalog.cabins[].features[] : string -- cruise.catalog.cabins[].id : string -- cruise.catalog.cabins[].name : string -- cruise.catalog.cruises : array -- cruise.catalog.cruises[] : object -- cruise.catalog.cruises[].destination : string -- cruise.catalog.cruises[].family : boolean -- cruise.catalog.cruises[].id : string -- cruise.catalog.cruises[].image : string -- cruise.catalog.cruises[].imageAlt : string -- cruise.catalog.cruises[].itineraryId : string -- cruise.catalog.cruises[].luxury : boolean -- cruise.catalog.cruises[].nights : number -- cruise.catalog.cruises[].shipId : string -- cruise.catalog.cruises[].title : string -- cruise.catalog.extras : array -- cruise.catalog.extras[] : object -- cruise.catalog.extras[].description : string -- cruise.catalog.extras[].id : string -- cruise.catalog.extras[].name : string -- cruise.catalog.extras[].simulated : boolean -- cruise.catalog.extras[].unit : string -- cruise.catalog.extras[].unitLabel : string -- cruise.catalog.extras[].unitPriceCents : number -- cruise.catalog.itineraries : array -- cruise.catalog.itineraries[] : object -- cruise.catalog.itineraries[].days : array -- cruise.catalog.itineraries[].days[] : object -- cruise.catalog.itineraries[].days[].day : number -- cruise.catalog.itineraries[].days[].description : string -- cruise.catalog.itineraries[].days[].portId : null|string -- cruise.catalog.itineraries[].id : string -- cruise.catalog.lines : array -- cruise.catalog.lines[] : object -- cruise.catalog.lines[].id : string -- cruise.catalog.lines[].name : string -- cruise.catalog.lines[].simulated : boolean -- cruise.catalog.ports : array -- cruise.catalog.ports[] : object -- cruise.catalog.ports[].country : string -- cruise.catalog.ports[].id : string -- cruise.catalog.ports[].name : string -- cruise.catalog.sailings : array -- cruise.catalog.sailings[] : object -- cruise.catalog.sailings[].availability : string -- cruise.catalog.sailings[].cruiseId : string -- cruise.catalog.sailings[].departureDate : string -- cruise.catalog.sailings[].departurePortId : string -- cruise.catalog.sailings[].id : string -- cruise.catalog.sailings[].price : object -- cruise.catalog.sailings[].price.adultFareCents : number -- cruise.catalog.sailings[].price.currency : string -- cruise.catalog.sailings[].price.portTaxCents : number -- cruise.catalog.sailings[].price.rounding : string -- cruise.catalog.sailings[].price.servicePerNightCents : number -- cruise.catalog.sailings[].price.simulated : boolean -- cruise.catalog.ships : array -- cruise.catalog.ships[] : object -- cruise.catalog.ships[].cruiseLineId : string -- cruise.catalog.ships[].id : string -- cruise.catalog.ships[].name : string -- cruise.catalog.ships[].simulated : boolean -- cruise.estimate : object -- cruise.estimate.adultUnitCents : number -- cruise.estimate.allocations : array -- cruise.estimate.allocations[] : object -- cruise.estimate.allocations[].adults : number -- cruise.estimate.allocations[].childAges : array -- cruise.estimate.allocations[].childAges[] : number -- cruise.estimate.allocations[].index : number -- cruise.estimate.baseFareCents : number -- cruise.estimate.cabinCount : number -- cruise.estimate.cabinSupplementCents : number -- cruise.estimate.childUnitCents : array -- cruise.estimate.childUnitCents[] : number -- cruise.estimate.extraLines : array -- cruise.estimate.extraLines[] : object -- cruise.estimate.extraLines[].extra : object -- cruise.estimate.extraLines[].extra.description : string -- cruise.estimate.extraLines[].extra.id : string -- cruise.estimate.extraLines[].extra.name : string -- cruise.estimate.extraLines[].extra.simulated : boolean -- cruise.estimate.extraLines[].extra.unit : string -- cruise.estimate.extraLines[].extra.unitLabel : string -- cruise.estimate.extraLines[].extra.unitPriceCents : number -- cruise.estimate.extraLines[].quantity : number -- cruise.estimate.extraLines[].totalCents : number -- cruise.estimate.extrasCents : number -- cruise.estimate.fareCents : number -- cruise.estimate.portTaxCents : number -- cruise.estimate.rounding : string -- cruise.estimate.serviceCents : number -- cruise.estimate.totalCents : number -- cruise.promotionEstimate.active : boolean -- cruise.promotionEstimate.discountCents : integer -- cruise.promotionEstimate.eligibleCents : integer -- cruise.promotionEstimate.percentageDiscountCents : integer -- cruise.promotionEstimate.roundingCents : integer -- cruise.promotionEstimate.totalCents : integer -- cruise.promotions.offers[].key : string -- cruise.promotions.offers[].promotion.expiresAt : string(datetime) -- cruise.promotions.offers[].promotion.startedAt : string(datetime) -- cruise.promotions.version : integer -- cruise.quoteSimulation.cabin : object(cruise.catalog.cabins[]) -- cruise.quoteSimulation.estimate : object(cruise.estimate) -- cruise.quoteSimulation.selection : object(cruise.selection) -- cruise.quoteSimulation.simulated : boolean -- cruise.search.category : string -- cruise.search.destination : string -- cruise.search.duration : string -- cruise.search.from : string(date) -- cruise.search.lineIds[] : string -- cruise.search.maxTotalCents : integer|undefined -- cruise.search.passengers.adults : integer -- cruise.search.passengers.childAges[] : integer -- cruise.search.portId : string -- cruise.search.sort : string -- cruise.search.to : string(date) -- cruise.selection.cabinCategoryId : string -- cruise.selection.extraIds[] : string -- cruise.selection.passengers.adults : integer -- cruise.selection.passengers.childAges[] : integer -- cruise.selection.sailingId : string -- cruise.shipInformation : object -- cruise.shipInformation.id : string -- cruise.shipInformation.line : string -- cruise.shipInformation.name : string -- cruise.shipInformation.officialUrl : string -- cruise.shipInformation.photos : array -- cruise.shipInformation.photos[] : object -- cruise.shipInformation.photos[].caption : string -- cruise.shipInformation.photos[].credit : string -- cruise.shipInformation.photos[].height : number -- cruise.shipInformation.photos[].kind : string -- cruise.shipInformation.photos[].observedUrl : string -- cruise.shipInformation.photos[].pageUrl : string -- cruise.shipInformation.photos[].rightsNote : string -- cruise.shipInformation.photos[].sha256 : string -- cruise.shipInformation.photos[].sourceUrl : string -- cruise.shipInformation.photos[].src : string -- cruise.shipInformation.photos[].verificationNote : string -- cruise.shipInformation.photos[].width : number -- cruise.workspace.favorites[].key : string -- cruise.workspace.favorites[].snapshot : object(cruise.catalog) -- cruise.workspace.simulations[].createdAt : string(datetime) -- cruise.workspace.simulations[].id : string -- cruise.workspace.simulations[].origin : string -- cruise.workspace.simulations[].promotion.expiresAt : string(datetime) -- cruise.workspace.simulations[].promotion.startedAt : string(datetime) -- cruise.workspace.simulations[].selection : object(cruise.selection) -- cruise.workspace.simulations[].snapshot : object(cruise.catalog) -- cruise.workspace.simulations[].status : string -- cruise.workspace.simulations[].updatedAt : string(datetime) -- cruise.workspace.version : integer -- tailor.adults : integer -- tailor.budget : integer|undefined -- tailor.childAges[] : integer -- tailor.children : integer -- tailor.dates : string -- tailor.destination : string -- tailor.notes : string -- tailor.unsure : boolean -- trip.bookingInput.accepted : boolean -- trip.bookingInput.contact.email : string(email) -- trip.bookingInput.contact.name : string -- trip.bookingInput.contact.notes : string -- trip.bookingInput.contact.phone : string -- trip.bookingInput.expectedTotalCents : integer -- trip.bookingInput.requestId : string(uuid) -- trip.bookingInput.trip : object(trip.estimate.input) -- trip.catalog : object -- trip.catalog.* : object -- trip.catalog.*.cities : array -- trip.catalog.*.cities[] : object -- trip.catalog.*.cities[].caption : string -- trip.catalog.*.cities[].description : string -- trip.catalog.*.cities[].name : string -- trip.catalog.*.cities[].photo : object -- trip.catalog.*.cities[].photo.alt : string -- trip.catalog.*.cities[].photo.credit : string -- trip.catalog.*.cities[].photo.destination : string -- trip.catalog.*.cities[].photo.excursion : boolean -- trip.catalog.*.cities[].photo.license : string -- trip.catalog.*.cities[].photo.location : string -- trip.catalog.*.cities[].photo.position : string -- trip.catalog.*.cities[].photo.source : string -- trip.catalog.*.cities[].photo.src : string -- trip.catalog.*.excluded : array -- trip.catalog.*.excluded[] : string -- trip.catalog.*.eyebrow : string -- trip.catalog.*.headline : string -- trip.catalog.*.hero : object -- trip.catalog.*.hero.alt : string -- trip.catalog.*.hero.credit : string -- trip.catalog.*.hero.destination : string -- trip.catalog.*.hero.excursion : boolean -- trip.catalog.*.hero.license : string -- trip.catalog.*.hero.location : string -- trip.catalog.*.hero.position : string -- trip.catalog.*.hero.source : string -- trip.catalog.*.hero.src : string -- trip.catalog.*.hotels : array -- trip.catalog.*.hotels[] : object -- trip.catalog.*.hotels[].city : string -- trip.catalog.*.hotels[].name : string -- trip.catalog.*.hotels[].note : string -- trip.catalog.*.hotels[].photo : object -- trip.catalog.*.hotels[].photo.alt : string -- trip.catalog.*.hotels[].photo.credit : string -- trip.catalog.*.hotels[].photo.license : string -- trip.catalog.*.hotels[].photo.source : string -- trip.catalog.*.hotels[].photo.src : string -- trip.catalog.*.hotels[].photos : array -- trip.catalog.*.hotels[].photos[] : object -- trip.catalog.*.hotels[].photos[].alt : string -- trip.catalog.*.hotels[].photos[].credit : string -- trip.catalog.*.hotels[].photos[].license : string -- trip.catalog.*.hotels[].photos[].source : string -- trip.catalog.*.hotels[].photos[].src : string -- trip.catalog.*.id : string -- trip.catalog.*.included : array -- trip.catalog.*.included[] : string -- trip.catalog.*.intro : string -- trip.catalog.*.name : string -- trip.catalog.*.nights : array -- trip.catalog.*.nights[] : number -- trip.catalog.*.stops : array -- trip.catalog.*.stops[] : string -- trip.catalog.*.tours : array -- trip.catalog.*.tours[] : object -- trip.catalog.*.tours[].adultCents : number -- trip.catalog.*.tours[].city : string -- trip.catalog.*.tours[].id : string -- trip.catalog.*.tours[].note : string -- trip.catalog.*.tours[].photo : object -- trip.catalog.*.tours[].photo.alt : string -- trip.catalog.*.tours[].photo.authorUrl : string -- trip.catalog.*.tours[].photo.credit : string -- trip.catalog.*.tours[].photo.destination : string -- trip.catalog.*.tours[].photo.excursion : boolean -- trip.catalog.*.tours[].photo.itineraryOnly : boolean -- trip.catalog.*.tours[].photo.license : string -- trip.catalog.*.tours[].photo.location : string -- trip.catalog.*.tours[].photo.position : string -- trip.catalog.*.tours[].photo.source : string -- trip.catalog.*.tours[].photo.src : string -- trip.catalog.*.tours[].title : string -- trip.catalog.*.tours[].top : boolean|undefined -- trip.costItems : array -- trip.costItems[] : object -- trip.costItems[].id : string -- trip.costItems[].nights : number -- trip.costItems[].quantity : number -- trip.costItems[].unitCents : number -- trip.destinationGuide.headline : string -- trip.destinationGuide.intro : string -- trip.destinationGuide.kicker : string -- trip.destinationGuide.name : string -- trip.destinationGuide.photos[] : object(trip.photo) -- trip.destinationGuide.sections[].text : string -- trip.destinationGuide.sections[].title : string -- trip.destinationGuide.source : string -- trip.destinationGuide.tips[] : string -- trip.estimate : object -- trip.estimate.input : object -- trip.estimate.input.country : string -- trip.estimate.input.departure : string -- trip.estimate.input.fullTransfers : boolean -- trip.estimate.input.interests : array -- trip.estimate.input.interests[] : string -- trip.estimate.input.nights : array -- trip.estimate.input.nights[] : number -- trip.estimate.input.tourSelection : object -- trip.estimate.input.tourSelection.* : object -- trip.estimate.input.tourSelection.*.adults : number -- trip.estimate.input.tourSelection.*.children : number -- trip.estimate.notice : string -- trip.estimate.optionsPriceCents : number -- trip.estimate.packagePriceCents : number -- trip.estimate.proposal : object -- trip.estimate.proposal.agent : string -- trip.estimate.proposal.arrivalDate : string -- trip.estimate.proposal.basePriceCents : number -- trip.estimate.proposal.client : string -- trip.estimate.proposal.conditions : string -- trip.estimate.proposal.country : string -- trip.estimate.proposal.days : object -- trip.estimate.proposal.departureDate : string -- trip.estimate.proposal.email : string -- trip.estimate.proposal.excluded : string -- trip.estimate.proposal.flights : array -- trip.estimate.proposal.flights[] : object -- trip.estimate.proposal.flights[].arrival : string -- trip.estimate.proposal.flights[].baggage : string -- trip.estimate.proposal.flights[].carrier : string -- trip.estimate.proposal.flights[].departure : string -- trip.estimate.proposal.flights[].from : string -- trip.estimate.proposal.flights[].id : string -- trip.estimate.proposal.flights[].number : string -- trip.estimate.proposal.flights[].to : string -- trip.estimate.proposal.fullTransfers : boolean -- trip.estimate.proposal.homeDate : string -- trip.estimate.proposal.id : string -- trip.estimate.proposal.included : string -- trip.estimate.proposal.intro : string -- trip.estimate.proposal.kind : string -- trip.estimate.proposal.phone : string -- trip.estimate.proposal.photos : array -- trip.estimate.proposal.photos[] : object -- trip.estimate.proposal.photos[].alt : string -- trip.estimate.proposal.photos[].authorUrl : string -- trip.estimate.proposal.photos[].credit : string -- trip.estimate.proposal.photos[].destination : string -- trip.estimate.proposal.photos[].excursion : boolean -- trip.estimate.proposal.photos[].license : string -- trip.estimate.proposal.photos[].location : string -- trip.estimate.proposal.photos[].position : string -- trip.estimate.proposal.photos[].source : string -- trip.estimate.proposal.photos[].src : string -- trip.estimate.proposal.priceCents : number -- trip.estimate.proposal.priceNote : string -- trip.estimate.proposal.quoteMode : string -- trip.estimate.proposal.reference : string -- trip.estimate.proposal.stages : array -- trip.estimate.proposal.stages[] : object -- trip.estimate.proposal.stages[].board : string -- trip.estimate.proposal.stages[].category : string -- trip.estimate.proposal.stages[].city : string -- trip.estimate.proposal.stages[].description : string -- trip.estimate.proposal.stages[].hotel : string -- trip.estimate.proposal.stages[].hotelPhotos : array -- trip.estimate.proposal.stages[].hotelPhotos[] : object -- trip.estimate.proposal.stages[].hotelPhotos[].alt : string -- trip.estimate.proposal.stages[].hotelPhotos[].credit : string -- trip.estimate.proposal.stages[].hotelPhotos[].license : string -- trip.estimate.proposal.stages[].hotelPhotos[].source : string -- trip.estimate.proposal.stages[].hotelPhotos[].src : string -- trip.estimate.proposal.stages[].id : string -- trip.estimate.proposal.stages[].nights : number -- trip.estimate.proposal.stages[].photo : string -- trip.estimate.proposal.title : string -- trip.estimate.proposal.tours : array -- trip.estimate.proposal.tours[] : object -- trip.estimate.proposal.tours[].adultCents : number -- trip.estimate.proposal.tours[].childCents : null -- trip.estimate.proposal.tours[].childMaxAge : number -- trip.estimate.proposal.tours[].childMinAge : number -- trip.estimate.proposal.tours[].city : string -- trip.estimate.proposal.tours[].id : string -- trip.estimate.proposal.tours[].name : string -- trip.estimate.proposal.tours[].note : string -- trip.estimate.proposal.tours[].top : boolean -- trip.estimate.proposal.tourSelection : object -- trip.estimate.proposal.tourSelection.* : object -- trip.estimate.proposal.tourSelection.*.adults : number -- trip.estimate.proposal.tourSelection.*.children : number -- trip.estimate.proposal.travelers : number -- trip.estimate.proposal.updatedAt : string -- trip.estimate.servicesPriceCents : number -- trip.estimate.tariffFallback : boolean -- trip.estimate.totalNights : number -- trip.estimate.totalPriceCents : number -- trip.estimate.travelers : number -- trip.hotels : array -- trip.hotels[] : object -- trip.hotels[].city : string -- trip.hotels[].credit : string -- trip.hotels[].id : string -- trip.hotels[].name : string -- trip.hotels[].note : string -- trip.hotels[].occupancies : array -- trip.hotels[].occupancies[] : string -- trip.hotels[].photo : string -- trip.hotels[].photos : array -- trip.hotels[].photos[] : object -- trip.hotels[].photos[].alt : string -- trip.hotels[].photos[].credit : string -- trip.hotels[].photos[].source : string -- trip.hotels[].photos[].src : string -- trip.hotels[].source : string -- trip.hotels[].verifiedAt : string -- trip.hotelTariffs : object -- trip.hotelTariffs.confirmed : array -- trip.hotelTariffs.confirmed[] : string -- trip.hotelTariffs.departureExclusion : object -- trip.hotelTariffs.departureExclusion.end : string -- trip.hotelTariffs.departureExclusion.inclusive : boolean -- trip.hotelTariffs.departureExclusion.start : string -- trip.hotelTariffs.departureRange : object -- trip.hotelTariffs.departureRange.end : string -- trip.hotelTariffs.departureRange.start : string -- trip.hotelTariffs.enabledMonths : array -- trip.hotelTariffs.enabledMonths[] : number -- trip.hotelTariffs.flightRules : object -- trip.hotelTariffs.flightRules.airlineBasis : string -- trip.hotelTariffs.flightRules.maxConnectionMinutes : number -- trip.hotelTariffs.flightRules.maxConnectionsPerDirection : number -- trip.hotelTariffs.flightRules.sameAirline : boolean -- trip.hotelTariffs.markupPercent : number -- trip.hotelTariffs.nightsPerCity : number -- trip.hotelTariffs.originalCells : array -- trip.hotelTariffs.originalCells[] : object -- trip.hotelTariffs.originalCells[].cell : string -- trip.hotelTariffs.originalCells[].value : number|string -- trip.hotelTariffs.restrictions : object -- trip.hotelTariffs.restrictions.april : string -- trip.hotelTariffs.restrictions.march : string -- trip.hotelTariffs.restrictions.may : string -- trip.hotelTariffs.restrictions.oneChild : string -- trip.hotelTariffs.restrictions.twoChildren : string -- trip.hotelTariffs.rounding : object -- trip.hotelTariffs.rounding.basis : string -- trip.hotelTariffs.rounding.direction : string -- trip.hotelTariffs.rounding.source : string -- trip.hotelTariffs.rounding.stepCents : number -- trip.hotelTariffs.source : string -- trip.hotelTariffs.tariffBasis : string -- trip.hotelTariffs.tariffBasisStatus : string -- trip.hotelTariffs.tariffs : array -- trip.hotelTariffs.tariffs[] : object -- trip.hotelTariffs.tariffs[].city : string -- trip.hotelTariffs.tariffs[].month : number -- trip.hotelTariffs.tariffs[].nightlyCents : number -- trip.hotelTariffs.tariffs[].occupancy : string -- trip.hotelTariffs.tariffs[].source : string -- trip.hotelTariffs.tariffs[].unit : string -- trip.hotelTariffs.tariffs[].year : number -- trip.hotelTariffs.trainCentsPerTravelerPerLeg : number -- trip.hotelTariffs.trainLegs : number -- trip.hotelTariffs.transferCentsPerService : object -- trip.hotelTariffs.transferCentsPerService.* : number -- trip.hotelTariffs.transferCentsPerService.*1n : number -- trip.hotelTariffs.transferCentsPerService.*2n : number -- trip.hotelTariffs.year : number -- trip.localDraft.country : string -- trip.localDraft.departure : string(date) -- trip.localDraft.fullTransfers : boolean -- trip.localDraft.nights[] : integer -- trip.localDraft.selected[] : string -- trip.localOptions.selection.*.adults : integer -- trip.localOptions.selection.*.children : integer -- trip.localOptions.transfers : boolean -- trip.proposal.days.*.body : string -- trip.proposal.days.*.title : string -- trip.proposal.tourSelection.*.adults : integer -- trip.proposal.tourSelection.*.children : integer -- trip.template : object -- trip.template.agent : string -- trip.template.arrivalDate : string -- trip.template.client : string -- trip.template.conditions : string -- trip.template.costs : object -- trip.template.costs.base : string -- trip.template.costs.checked : boolean -- trip.template.costs.extras : string -- trip.template.costs.flight : string -- trip.template.country : string -- trip.template.days : object -- trip.template.departureDate : string -- trip.template.email : string -- trip.template.excluded : string -- trip.template.flights : array -- trip.template.flights[] : object -- trip.template.flights[].arrival : string -- trip.template.flights[].baggage : string -- trip.template.flights[].carrier : string -- trip.template.flights[].departure : string -- trip.template.flights[].from : string -- trip.template.flights[].id : string -- trip.template.flights[].number : string -- trip.template.flights[].to : string -- trip.template.homeDate : string -- trip.template.id : string -- trip.template.included : string -- trip.template.intro : string -- trip.template.kind : string -- trip.template.phone : string -- trip.template.photos : array -- trip.template.photos[] : object -- trip.template.photos[].alt : string -- trip.template.photos[].authorUrl : string -- trip.template.photos[].credit : string -- trip.template.photos[].destination : string -- trip.template.photos[].excursion : boolean -- trip.template.photos[].license : string -- trip.template.photos[].location : string -- trip.template.photos[].position : string -- trip.template.photos[].source : string -- trip.template.photos[].src : string -- trip.template.priceCents : null -- trip.template.reference : string -- trip.template.revision : number -- trip.template.stages : array -- trip.template.stages[] : object -- trip.template.stages[].board : string -- trip.template.stages[].category : string -- trip.template.stages[].city : string -- trip.template.stages[].description : string -- trip.template.stages[].hotel : string -- trip.template.stages[].hotelPhotos : array -- trip.template.stages[].hotelPhotos[] : object -- trip.template.stages[].hotelPhotos[].alt : string -- trip.template.stages[].hotelPhotos[].credit : string -- trip.template.stages[].hotelPhotos[].license : string -- trip.template.stages[].hotelPhotos[].source : string -- trip.template.stages[].hotelPhotos[].src : string -- trip.template.stages[].id : string -- trip.template.stages[].nights : number -- trip.template.stages[].photo : string -- trip.template.title : string -- trip.template.travelers : number -- trip.template.updatedAt : string -- trip.tours : array -- trip.tours[] : object -- trip.tours[].adultCents : number -- trip.tours[].childCents : null|number -- trip.tours[].childMaxAge : number -- trip.tours[].childMinAge : number -- trip.tours[].city : string -- trip.tours[].id : string -- trip.tours[].name : string -- trip.tours[].note : string -- trip.tours[].top : boolean -- 40. INTERFAZ Y PUBLICACIONES: MODELO DOCUMENTAL, NO PERSISTENCIA SQL ACTIVA. -- Navegación: dist/app.js, dist/explorar.js y dist/portada.js. -- Los filtros viven en memoria; favoritos en localStorage, sin cuenta ni -- sincronización. Las tablas siguientes formalizan su estructura, no cambian -- el lugar donde se guardan actualmente. No se crean identidades de clientes. CREATE TABLE ui_navigation_state ( component_key TEXT PRIMARY KEY NOT NULL, query TEXT, community TEXT, zone TEXT, theme TEXT, sort TEXT, favorites_only INTEGER CHECK (favorites_only IN (0,1)), quotes_only INTEGER CHECK (quotes_only IN (0,1)), audience TEXT, destination TEXT, occasion TEXT, experience TEXT, region TEXT, more_regions INTEGER CHECK (more_regions IN (0,1)), visible_limit INTEGER CHECK (visible_limit >= 0), step INTEGER CHECK (step BETWEEN 0 AND 2), category TEXT ); -- app.state: query,community,zone,theme,sort,favoritesOnly,quotesOnly,audience. -- app.visibleLimit -> visible_limit; explorer.state.limit -> visible_limit. -- explorer.state: destination,occasion,experience,region,zone,query,audience, -- sort,moreRegions,limit,step. portada: category,query. -- opener, activeElement, DOM nodes y callbacks de eventos son referencias de -- ejecución, no propiedades persistibles; no se convierten en datos SQL. CREATE TABLE ui_favorite ( storage_scope TEXT NOT NULL, offer_id TEXT NOT NULL, position INTEGER NOT NULL CHECK (position >= 0), PRIMARY KEY (storage_scope, offer_id) ); -- localStorage 'mundomania-favorites': array de IDs. No se incluye -- su contenido. offer_id puede proceder del catálogo o de ofertas propias; -- no se fuerza una FK a uno solo de esos dos orígenes. CREATE TABLE ui_plan_definition ( id TEXT PRIMARY KEY NOT NULL, label TEXT, title TEXT, symbol TEXT, css_class TEXT, sprite INTEGER, art TEXT, selection_rule TEXT, group_key TEXT, position INTEGER NOT NULL CHECK (position >= 0) ); -- themes.{id}.{label,symbol}; seasons[].{id,title,class,sprite,art}; -- experiences[]: [id,label,symbol]. Íconos SVG/arte son recursos visuales. CREATE TABLE ui_destination_definition ( id TEXT PRIMARY KEY NOT NULL, label TEXT NOT NULL, note TEXT, group_key TEXT, region TEXT, zone TEXT, icon TEXT, selection_rule TEXT, position INTEGER NOT NULL CHECK (position >= 0) ); -- destinations[].{id,label,note,group,match}: match es un predicado JavaScript, -- documentado mediante selection_rule; no es un procedimiento SQL activo. -- ownGeography.{destination}:[region,zone], regionLabels,regionNotes,regionOrder -- se representan por region,zone,label,note,position. Sin destinos concretos. CREATE TABLE ui_offer_result ( offer_id TEXT NOT NULL, origin_kind TEXT NOT NULL, region TEXT, zone TEXT, family INTEGER CHECK (family IN (0,1)), couple INTEGER CHECK (couple IN (0,1)), experience TEXT, occasions_json TEXT, PRIMARY KEY (offer_id, origin_kind) ); -- explorer.rows[].{offer,own,region,zone,family,couple,experience,occasions}. -- offer se resuelve en el catálogo correspondiente; own -> origin_kind. -- occasions_json: array. Es una proyección calculada, no datos fuente. -- FUNCIONES DE INTERFAZ (contratos, no procedimientos almacenados): -- themeOf/occasions/planMatch/ownFamily: clasifican y cruzan planes, -- ocasiones, destinos y disponibilidad familiar; permiten múltiples secciones. -- audienceOf/audienceLabel: etiqueta según tarifas reales de adultos/menores. -- normalized search: quita tildes, pasa a minúsculas y compara texto. -- filter+sort+pagination: combina filtros, ordena y limita tarjetas visibles. -- promotionExpired/confirmedDeadline: validez y caducidad de la promoción. -- offerCard/galleryMarkup/factsMarkup/starsMarkup: representación HTML. -- navigation: paso plan -> destino -> resultados; volver conserva contexto. -- favorites: alterna IDs y los guarda en el navegador; se tolera bloqueo de -- almacenamiento conservándolos durante la visita. -- photo viewer: selección de imagen, anterior/siguiente y volver a la oferta. -- safeUrl/escape: validación de enlaces y escape de salida HTML. -- Los calendarios y cálculos de ocupación se documentan en catálogo/tarifas. -- Publicaciones: almacenamiento ACTUAL en archivos JSON/MD del proyecto; -- estas entidades reflejan esos documentos, no tablas usadas por el servidor. CREATE TABLE web_release ( version TEXT PRIMARY KEY NOT NULL, previous_version TEXT REFERENCES web_release(version), release_date TEXT NOT NULL, prepared_at TEXT, documentation TEXT, changelog TEXT, structure_document TEXT, structure_filename TEXT ); CREATE TABLE web_release_change ( version TEXT NOT NULL REFERENCES web_release(version), position INTEGER NOT NULL CHECK (position >= 0), description TEXT NOT NULL, PRIMARY KEY (version, position) ); -- releases.json.releases[].{version,previousVersion,date,preparedAt,changes[]}. -- VERSION.json: version,previousVersion,date,changes,documentation,changelog, -- structure,structureFilename. Ningún registro de versión se inserta aquí. CREATE TABLE web_document ( path TEXT NOT NULL, version TEXT NOT NULL REFERENCES web_release(version), content_type TEXT NOT NULL, content TEXT NOT NULL, PRIMARY KEY (path, version) ); -- Funcionalidades, historial y estructura: contenido documental de la versión. CREATE TABLE web_export ( version TEXT PRIMARY KEY NOT NULL REFERENCES web_release(version), format_version INTEGER NOT NULL, generated_at TEXT NOT NULL, public_origin TEXT NOT NULL, gateway_sha256 TEXT, root_directory INTEGER CHECK (root_directory IN (0,1)), apply_response_headers INTEGER CHECK (apply_response_headers IN (0,1)), exact_html_route_aliases INTEGER CHECK (exact_html_route_aliases IN (0,1)), note TEXT ); CREATE TABLE web_public_asset ( version TEXT NOT NULL REFERENCES web_export(version), path TEXT NOT NULL, bytes INTEGER NOT NULL CHECK (bytes >= 0), sha256 TEXT NOT NULL, PRIMARY KEY (version, path) ); CREATE TABLE web_export_route ( version TEXT NOT NULL, route TEXT NOT NULL, file_path TEXT NOT NULL, PRIMARY KEY (version, route), FOREIGN KEY (version, file_path) REFERENCES web_public_asset(version, path) ); CREATE TABLE web_route_header ( version TEXT NOT NULL, route TEXT NOT NULL, header_name TEXT NOT NULL, header_value TEXT NOT NULL, PRIMARY KEY (version, route, header_name), FOREIGN KEY (version, route) REFERENCES web_export_route(version, route) ); CREATE TABLE web_export_requirement ( version TEXT NOT NULL REFERENCES web_export(version), kind TEXT NOT NULL, position INTEGER NOT NULL CHECK (position >= 0), endpoint TEXT NOT NULL, PRIMARY KEY (version, kind, position) ); CREATE TABLE web_skipped_asset ( version TEXT NOT NULL REFERENCES web_export(version), route TEXT NOT NULL, status INTEGER NOT NULL, PRIMARY KEY (version, route) ); CREATE TABLE web_publication_receipt ( version TEXT PRIMARY KEY NOT NULL REFERENCES web_release(version), published_at TEXT NOT NULL, url TEXT NOT NULL, files_changed INTEGER NOT NULL CHECK (files_changed >= 0) ); -- Manifiesto: formatVersion,generatedAt,publicOrigin,gatewaySha256,release, -- files[].{path,bytes,sha256},routes[].{route,file,headers.{name}}, -- skippedUnavailableImages[].{route,status},requirements.{rootDirectory, -- applyResponseHeaders,exactHtmlRouteAliases,backendEndpoints[], -- blockedInExistingPreview[],note}. release reutiliza web_release. -- published-release: version,publishedAt,url,filesChanged. -- FUNCIONES: create-release incrementa major/minor/patch y exige cambios; -- bloquea otra versión mientras haya una pendiente. releaseDocuments conserva -- todo el historial; stampHtml añade versión/enlaces y renueva caché CSS/JS. -- export-public filtra datos internos y genera inventario; upload_release -- comprueba hashes/documentos/versión remota y publica VERSION.json al final. -- build-schema mantiene este contrato; no aplica DDL a una base en servicio. -- structure-download: GET/HEAD entrega exclusivamente ESTRUCTURA.sql como -- archivo adjunto. No acepta nombres de fichero del usuario ni accede a BD. -- Publicación principal 2026-10-01: www.viajesmundomania.com utiliza un almacén -- privado independiente de solicitudes y enlaces de presupuesto. La API acepta -- únicamente su origen canónico. No se migran reservas históricas ni se ejecuta -- este contrato DDL en producción; las migraciones privadas inicializan la base. -- DOCUMENTAL. Contexto de WhatsApp, sin persistencia ni confirmacion de entrega. CREATE TABLE ui_whatsapp_context ( context_key TEXT PRIMARY KEY, title TEXT, hotel TEXT, destination TEXT, check_in TEXT, check_out TEXT, nights INTEGER, adults INTEGER, child_ages_json TEXT, departure_city TEXT, room TEXT, board TEXT, final_total_eur REAL, quote_reference TEXT, includes_json TEXT, itinerary_text TEXT, excursions_text TEXT, page_url TEXT, price_pending INTEGER NOT NULL DEFAULT 1 ); -- Propiedades opcionales: solo las disponibles en la seleccion actual. -- quoteMessage(offer,quote,fields): titulo, destino, hotel y tarifa seleccionada; -- fechas, noches, viajeros/edades, aeropuerto, habitacion, regimen, total e incluye. -- Sin quote valido: seleccion parcial y precio pendiente, nunca precio desde. -- messageFor: adapta oferta/circuito, crucero, Nueva York o Japon/Tailandia. -- Cruceros conserva itinerario, vistas del camarote, bebidas y condiciones. -- Nueva York conserva excursiones y participantes, sin copiar datos internos. -- whatsappUrl(message,page): destinatario fijo, texto codificado, estado pendiente -- y enlace sin parametros de consulta. El usuario pulsa Enviar en WhatsApp. -- prepareLink: recalcula al enfocar o pulsar; evita selecciones anteriores. -- refresh/decorate: boton verde accesible, movil, actualizado con la interfaz. -- Los campos del formulario de contacto no se recogen automaticamente. -- Cobertura: debajo de Reservar en tarifas/formularios y al final del presupuesto; -- sin boton verde en tarjetas, cabeceras ni configuradores iniciales. -- Si hay varias tarifas, solo el boton de cada tarifa cita su total. -- Cruceros mantiene su accion Solicitar reserva y WhatsApp secundario debajo. -- travelBudgetMessage: lee la propuesta visible de Japon/Tailandia, recorrido, -- fechas, alojamientos, servicios seleccionados y total para dos adultos. -- No recoge nombre, email, telefono ni comentarios privados del formulario. -- DOCUMENTAL: Nueva York. Estado calculado en el navegador, sin tablas activas -- ni registros de clientes en el servidor. Importes finales en céntimos EUR. -- quoteNewYork valida ocupación, fecha real de entrada, noches y edades; -- devuelve precio del paquete, suplemento de estancia, excursiones y total. -- El paquete depende del número de personas; las noches adicionales cobran -- dos primeras plazas y tercera/cuarta plazas. No añade margen ni redondeo. -- La tarifa infantil de excursión usa edad al viajar, distinta del paquete. -- normalizeNewYorkFlight fija equipaje y escala en ambos sentidos. -- renderProposal muestra vuelos, alojamiento, servicios y opciones; -- wireTours cambia participantes dentro del grupo y recalcula el total. -- createNewYorkPdf conserva todos los tours, indicando cuáles suma al total. -- downloadNewYorkPdf: POST /nueva-york/descargar.php, campo pdf en base64. -- Solo acepta POST del mismo origen cuando existe Origin, tamaño limitado, -- cabecera y cierre PDF válidos; devuelve attachment sin almacenar el archivo. -- sharedTrip valida y selecciona client, adults, children, childAges, nights, -- checkin, origin y selections (adultos/niños por tour); exige fecha completa. -- encodeSharedTrip/decodeSharedTrip usan JSON UTF-8 y base64url, version 1. -- budgetLink crea /nueva-york/#presupuesto=TOKEN, sin persistencia servidor. -- La apertura ignora borradores locales y recalcula las tarifas del paquete. -- El destinatario ve el presupuesto, cambia tours y descarga PDF por POST. -- Los enlaces inválidos muestran error y deshabilitan compartir/descargar. -- Copiar enlace usa portapapeles o selección manual; abrir usa enlace HTTPS. -- El HTML adjunto se sustituye por este enlace para compatibilidad móvil. -- populate/render/setMode/childAgeFields gestionan formulario y presentación. -- validateState valida borrador local; guardar no envía datos al servidor. CREATE TABLE ny_quote_document ( id TEXT PRIMARY KEY, client TEXT NOT NULL DEFAULT '', adults INTEGER NOT NULL CHECK (adults BETWEEN 2 AND 4), children INTEGER NOT NULL CHECK (children BETWEEN 0 AND 2), nights INTEGER NOT NULL CHECK (nights >= 7), hotel_id TEXT NOT NULL, checkin TEXT, origin TEXT NOT NULL CHECK (origin IN ('valencia','madrid','barcelona')), selections_json TEXT NOT NULL, CHECK (adults + children BETWEEN 2 AND 4) ); CREATE TABLE ny_child_age_document ( quote_id TEXT NOT NULL REFERENCES ny_quote_document(id), child_index INTEGER NOT NULL, age INTEGER NOT NULL CHECK (age BETWEEN 0 AND 18), PRIMARY KEY (quote_id,child_index) ); CREATE TABLE ny_flight_document ( quote_id TEXT NOT NULL REFERENCES ny_quote_document(id), id TEXT NOT NULL, label TEXT NOT NULL, origin TEXT NOT NULL, destination TEXT NOT NULL, carrier TEXT, number TEXT, departure_date TEXT, departure TEXT, arrival_date TEXT, arrival TEXT, baggage TEXT NOT NULL, stop INTEGER NOT NULL, stop_city TEXT NOT NULL, hours INTEGER NOT NULL, minutes INTEGER NOT NULL, PRIMARY KEY (quote_id,id) ); CREATE TABLE ny_price_document ( quote_id TEXT PRIMARY KEY REFERENCES ny_quote_document(id), package_cents INTEGER NOT NULL, extra_nights INTEGER NOT NULL, extra_night_group_cents INTEGER NOT NULL, extra_nights_cents INTEGER NOT NULL, base_pvp_cents INTEGER NOT NULL, extras_cents INTEGER NOT NULL, total_cents INTEGER NOT NULL, travelers INTEGER NOT NULL, tour_adults INTEGER NOT NULL, tour_children INTEGER NOT NULL, complete INTEGER NOT NULL ); CREATE TABLE ny_tour_document ( id TEXT PRIMARY KEY, name TEXT NOT NULL, adult_cents INTEGER NOT NULL, child_cents INTEGER NOT NULL, description TEXT NOT NULL, source TEXT, photos_json TEXT NOT NULL ); CREATE TABLE ny_hotel_document ( id TEXT PRIMARY KEY, name TEXT NOT NULL, cancellation TEXT NOT NULL, photos_json TEXT NOT NULL ); -- Galerías: source, originalUrl, credit, alt, file, mime, width, height, src. -- Media: checkedAt, hero, hotel, tours. Imágenes del PDF: hero, logo, -- hotel[], contrastes y manhattan. No se incluyen imágenes ni datos en SQL. -- ACTUAL (archivos JSON privados, representación SQL documental): enlaces cortos. -- POST /p/ valida exclusivamente los datos de viaje y devuelve URL del mismo -- dominio con identificador aleatorio de 96 bits (16 caracteres base64url). -- GET /p/?ID abre /nueva-york/#p=ID; GET con data=1 recupera version y trip. -- createShortBudgetLink/loadShortBudgetLink conservan el cálculo y tours. -- Se admiten también los fragmentos largos anteriores, sin modificarlos. -- Almacén fuera del directorio público, permisos privados, índice hash→id -- para reutilizar enlaces idénticos; escritura bajo bloqueo, sin sobrescribir -- otro presupuesto. No hay listado público ni redirecciones a otros dominios. -- Límite: petición 8000 bytes, 10000 presupuestos y 1000 nuevos por hora. -- PDF sin cambios: no se almacena. No se modifica la base activa de reservas. CREATE TABLE ny_short_link_document ( id TEXT PRIMARY KEY CHECK(length(id)=16), version INTEGER NOT NULL, trip_json TEXT NOT NULL, payload_sha256 TEXT NOT NULL UNIQUE ); CREATE TABLE ny_short_link_rate_document ( hour_utc TEXT PRIMARY KEY, count INTEGER NOT NULL CHECK(count>=0) ); -- 60. CRUCEROS VERIFICADOS: MODELO DOCUMENTAL AISLADO, NO UNA MIGRACION. -- Fuentes: work/cruceros-2027/ui/pricing.mjs y work/cruceros-2027/ui/app.js. -- No hay tablas activas, registros ni reservas persistentes en este modulo. -- Todas las tablas siguientes representan documentos o estado efimero del -- navegador; ejecutar este DDL NO conecta navieras ni confirma disponibilidad. -- Este fragmento esta pendiente de integrar sobre el proyecto publicado 1.1.4. -- No sustituye ni modifica aun 30-viajes-cruceros.sql ni el crucero de demostracion -- anterior. La nueva pantalla solo se considera publicada tras su integracion. -- La agencia debe reconfirmar precio, plazas y condiciones antes de reservar. -- No se modelan clientes, pagos, correos enviados, bloqueos de plazas ni reservas. -- Raiz: schemaVersion, updatedAt, promotion, products[] y quotes[]. Los arrays -- se normalizan abajo. Metadatos describen la forma y generacion del catalogo, -- no son numero de version de despliegue ni fecha de consulta de cada tarifa. CREATE TABLE cruise_verified_catalog_document ( document_id INTEGER PRIMARY KEY CHECK (document_id = 1), schema_version INTEGER NOT NULL CHECK (typeof(schema_version) = 'integer' AND schema_version > 0), updated_at TEXT NOT NULL CHECK (julianday(updated_at) IS NOT NULL) ); -- promotion, products[] y quotes[]. Los arrays se normalizan -- abajo. document_id solo identifica el documento, no una campana nueva. -- Politica de esta campana: percent=5; inicio 2026-09-29T00:00:00+02:00; -- fin inclusivo 2026-10-15T23:59:59+02:00 (Europe/Madrid en ambas fechas). -- Los valores de la campana pertenecen al JSON fuente; este SQL no los inserta. CREATE TABLE cruise_verified_promotion_document ( document_id INTEGER PRIMARY KEY CHECK (document_id = 1), percent REAL NOT NULL CHECK (percent BETWEEN 0 AND 100), start_at TEXT NOT NULL, end_at TEXT NOT NULL, CHECK (julianday(start_at) IS NOT NULL), CHECK (julianday(end_at) IS NOT NULL), CHECK (julianday(end_at) >= julianday(start_at)) ); CREATE TABLE cruise_verified_product_document ( id TEXT PRIMARY KEY, line TEXT NOT NULL CHECK (line IN ('msc','royal','costa')), ship TEXT NOT NULL, title TEXT NOT NULL, departure_port TEXT NOT NULL CHECK (departure_port IN ('Barcelona','Valencia')), arrival_port TEXT, nights INTEGER CHECK (nights IS NULL OR (typeof(nights) = 'integer' AND nights > 0)), image TEXT, image_alt TEXT, source_url TEXT NOT NULL CHECK (length(trim(source_url)) > 0) ); -- route[] conserva el orden del recorrido publicado; no implica una salida -- vendible. No se generan fechas por repeticion semanal de un itinerario. CREATE TABLE cruise_verified_route_stop_document ( product_id TEXT NOT NULL REFERENCES cruise_verified_product_document(id), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), port TEXT NOT NULL, PRIMARY KEY (product_id, position) ); -- product.gallery[]: hasta diez fotografias documentadas del barco, con texto -- alternativo y pagina oficial de procedencia. Las imagenes de camarotes son -- orientativas: no acreditan categoria, vistas ni servicios incluidos en tarifa. CREATE TABLE cruise_verified_gallery_photo_document ( product_id TEXT NOT NULL REFERENCES cruise_verified_product_document(id), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position BETWEEN 0 AND 9), url TEXT NOT NULL CHECK (length(trim(url)) > 0), alt TEXT NOT NULL, source_url TEXT, PRIMARY KEY (product_id, position) ); -- sailing.date es la salida; arrivalDate puede exceder septiembre. El rango -- comercial esta limitado a 2027-06-01..2027-09-30, ambos incluidos. -- El JSON debe aportar una fecha real; findQuote comprueba el patron de -- ano/mes y la coincidencia con la fecha de calendario interpretada. CREATE TABLE cruise_verified_sailing_document ( product_id TEXT NOT NULL REFERENCES cruise_verified_product_document(id), id TEXT NOT NULL, departure_date TEXT NOT NULL, arrival_date TEXT, arrival_port TEXT, supplier_itinerary_code TEXT, verification_status TEXT, source_url TEXT NOT NULL CHECK (length(trim(source_url)) > 0), PRIMARY KEY (product_id, id), CHECK (length(departure_date) = 10 AND departure_date BETWEEN '2027-06-01' AND '2027-09-30'), CHECK (date(departure_date, '+0 days') IS NOT NULL AND date(departure_date, '+0 days') = departure_date), CHECK (arrival_date IS NULL OR (length(arrival_date) = 10 AND date(arrival_date, '+0 days') IS NOT NULL AND date(arrival_date, '+0 days') = arrival_date AND arrival_date >= departure_date)) ); -- verificationStatus es provenance del calendario, no quote.status ni plazas. -- published_calendar_booking_validation_pending y published_schedule_prices_pending -- indican que faltan comprobaciones de tarifa; nunca activan precio por si solos. -- sailing.route[] opcional prevalece cuando no esta vacio; si falta se usa -- product.route[]. sailing.arrivalPort prevalece sobre product.arrivalPort y, -- si ambos faltan, sobre product.departurePort. No mezclar rutas de salidas. CREATE TABLE cruise_verified_sailing_route_stop_document ( product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), port TEXT NOT NULL, PRIMARY KEY (product_id, sailing_id, position), FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_sailing_document(product_id, id) ); -- sailing.itinerary es informacion de la salida elegida, independiente de la -- tarifa. summaryOnly=1 muestra stops[] sin asignar dias/horarios. summaryOnly=0 -- muestra days[]: admite varias escalas o experiencias en un mismo dia. -- Una jornada sin evidencia se rotula pendiente, nunca se inventa navegacion. -- note conserva las limitaciones de la fuente. No se copia un horario de otra -- fecha aunque el barco o el itinerario comercial tengan el mismo nombre. CREATE TABLE cruise_verified_itinerary_document ( product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, summary_only INTEGER NOT NULL CHECK (summary_only IN (0,1)), source_url TEXT NOT NULL, note TEXT, PRIMARY KEY (product_id, sailing_id), FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_sailing_document(product_id, id) ); CREATE TABLE cruise_verified_itinerary_day_document ( product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), day INTEGER NOT NULL CHECK (typeof(day) = 'integer' AND day > 0), port TEXT NOT NULL, arrival TEXT, departure TEXT, description TEXT, PRIMARY KEY (product_id, sailing_id, position), FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_itinerary_document(product_id, sailing_id) ); CREATE TABLE cruise_verified_itinerary_summary_stop_document ( product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), port TEXT NOT NULL, PRIMARY KEY (product_id, sailing_id, position), FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_itinerary_document(product_id, sailing_id) ); -- Solo se ofrecen los planes documentados para ese producto. Contrato entre -- tablas: Royal -> full-board; MSC -> full-board/easy; Costa -> full-board/mydrinks. -- Las bebidas son un paquete concreto con sus limites, no una promesa generica -- de que todo servicio, marca o establecimiento esta incluido. CREATE TABLE cruise_verified_plan_document ( product_id TEXT NOT NULL REFERENCES cruise_verified_product_document(id), id TEXT NOT NULL CHECK (id IN ('full-board','easy','mydrinks')), label TEXT NOT NULL, description TEXT NOT NULL, PRIMARY KEY (product_id, id) ); -- plan.details: explicacion del regimen, no una cotizacion ni disponibilidad. -- Los tres arrays includes/excludes/notes se guardan ordenados y separados por -- list_kind. La fuente oficial y los avisos acompanan lo mostrado. Tener detalle -- de Easy no habilita un precio Easy ni extiende una tarifa doble a familias. CREATE TABLE cruise_verified_plan_details_document ( product_id TEXT NOT NULL, plan_id TEXT NOT NULL, summary TEXT NOT NULL, source_url TEXT, PRIMARY KEY (product_id, plan_id), FOREIGN KEY (product_id, plan_id) REFERENCES cruise_verified_plan_document(product_id, id) ); CREATE TABLE cruise_verified_plan_details_item_document ( product_id TEXT NOT NULL, plan_id TEXT NOT NULL, list_kind TEXT NOT NULL CHECK (list_kind IN ('includes','excludes','notes')), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), description TEXT NOT NULL, PRIMARY KEY (product_id, plan_id, list_kind, position), FOREIGN KEY (product_id, plan_id) REFERENCES cruise_verified_plan_details_document(product_id, plan_id) ); -- quote es una observacion trazable de la fuente identificada (naviera o -- agencia). No representa disponibilidad en vivo. published_public_price -- admite exclusivamente la tarifa por persona en doble para dos adultos; -- price_basis distingue publicacion oficial de publicacion de agencia. -- No se aplica esa conversion a familias ni cuatro adultos; inventory_availability -- permanece unverified y availability_note se muestra junto al precio. -- supplier_group_quote identifica un total exacto devuelto por el flujo publico -- MSC para grupo, fecha, categoria y regimen observados. price_basis es -- observed_group_total. inventory_availability sigue unverified: una cotizacion -- no retiene plazas y el proveedor advierte de reconfirmacion antes del pago. -- normalizeMscGroupQuotes(document,research,options) importa esas capturas: -- document={schemaVersion,market,currency,observations[]}; cada observation -- contiene productId,date,cabin,plan,checkedAt,sourceUrl,group,cabinName, -- supplierFareName,totalCents,taxesCents,mandatoryFeesCents,serviceChargesIncluded, -- fareComponents[],evidence y opcionales rangeEvidence,supplierCabinCategory, -- supplierFareCode,expiresAt. group={adults,childAges[]|childAgeRanges[{min,max}]}. -- fareComponents[{role,amountCents}] conserva un importe observado por viajero: -- la suma de tarifas, tasas y cuota de servicio debe igualar totalCents. -- evidence={stage,evidenceFile,productId,date,cabin,plan,group,currency,totalCents} -- debe coincidir con las dimensiones solicitadas y usar cabin_category_priced; -- un precio desde o un resultado para otra ocupacion no habilita la tarifa. -- rangeEvidence={kind,sourceUrl,text,evidenceFile}, cuando se usan rangos, -- exige supplier-quoted-age-band acreditado en el selector/cotizacion oficial. -- No se convierte una promocion generica de ninos gratis en un rango de precio. -- options.verifyEvidenceFile verifica capturas locales. -- assertMscCaptureUsable(text) rechaza capturas vacias o con dialogos de error, -- incluidos los marcados como dialog [active]; CLI aplica esa validacion a -- capturas TXT/JSON/HTML antes de habilitar una cotizacion. -- La salida privada del normalizador es -- {schemaVersion,provider,generatedAt,quotes[],evidence[],contract}; evidence[] -- contiene quoteId,evidenceFile,fareComponents y, cuando corresponde, -- rangeEvidenceFile/rangeEvidenceText. Son ficheros fuente de importacion, no -- tablas activas ni contenido del catalogo publico. El constructor publica solo -- propiedades de quotes ya documentadas en las tablas de este fragmento. -- Normalizador rechaza duplicados, importes no enteros, desgloses incoherentes, -- fechas fuera de alcance y grupos no autorizados; nunca multiplica adultos -- para estimar familias. Descuento y redondeo siguen en priceQuote. -- normalizeVayaPublicPrices(rows,research,officialPrices,readers) admite solo -- publicaciones para dos adultos en cabina doble. La fuente del PVP es la -- agencia Vayacruceros, no MSC; quote_type=published_public_price y price_basis -- =agency_double_occupancy_per_person. inventory_availability=unverified. -- Su base excluye tasas y propinas, acreditado en la pagina de la agencia. -- Total grupo=2*(base por persona+tasas por persona+cuota hotel por adulto). -- Cuotas requieren evidencia MSC del mismo barco/fecha; para siete noches, -- las nuevas reservas posteriores al 2/7/2026 tienen 12 EUR/adulto/noche. -- officialChargeCalendarSourceUrl identifica el calendario oficial que incluye -- esa salida y cuotas, aunque la URL abra por defecto otra fecha del calendario. -- serviceSourceUrl acredita ademas la politica de cuota obligatoria por noche. -- La cuota se suma una sola vez y permanece dentro de la base del descuento. -- parseVayaFareOptions(response) lee opciones explicitas del modal publico, -- su precio y prestaciones marcadas; conserva producto, fecha, categoria, -- edades y codigo de tarifa devueltos. No sigue enlaces de seleccion/reserva. -- Solo Easy nombrado y marcado habilita easy. PC puede usar la celda publica -- seleccionable para dos adultos si no hay modal; status0 no prueba agotado. -- Opciones Easy/PC se aceptan desde ambas paginas si la respuesta las acredita. -- TARIFA BEBIDAS tambien puede ser Easy cuando la prestacion exacta Paquete de -- bebidas Easy aparece marcada; nunca Premium ni un todo incluido ambiguo. -- Un producto TI distinto devuelto en el modal PC requiere una fila publica -- hermana del mismo barco, salida, categoria y dos adultos que acredite su ID. -- Duplicados de igual barco, salida, camarote y plan conservan el menor total -- comprobado. cabinName mantiene el nombre completo y limitaciones de vista. -- supersedeGenericMscPublicPrices(existing,agency) sustituye una publicacion -- anterior de igual barco/salida/ocupacion/camarote/plan por la nueva tarifa -- con categoria acreditada, incluso si el precio generico antiguo es menor. -- Una publicacion posterior no se elimina por evidencia de agencia mas vieja. -- Nunca se multiplican medias familiares ni se utiliza un precio oculto cuando -- la pantalla de grupo dice Consultar. No se accede al paso precioFinal. -- Entrada saneada privada={schemaVersion,market,currency,agency,records[]}; -- records conserva metadatos de consulta y opciones observadas, sin sesiones, -- cookies ni HTML bruto. Sus propiedades y subestructuras estan inventariadas. -- Constructor publica solo las propiedades existentes de quote; HTML bruto, -- datos de consulta y componentes de investigacion no entran en la vista web. -- supersedeMscPublishedPrices(publishedQuotes,groupQuotes): al construir, -- sustituye el precio publico doble anterior por una cotizacion exacta de -- grupo mas reciente de igual producto, salida, adultos, camarote y plan. -- No elimina otras combinaciones ni modifica las observaciones fuente. -- children_count deriva de childAges.length, o de childAgeRanges.length si no -- existe childAges. age_match_mode documenta esa eleccion; childAges presente -- tiene prioridad. Ambos arrays se normalizan en sus tablas; la longitud del -- utilizado debe coincidir con children_count (contrato, no un trigger SQL). -- Solo se admiten rangos con rangeEvidenceUrl del proveedor. No se extrapola -- una edad particular a un rango: la igualdad de importe debe estar acreditada. -- Importe EUR total del grupo en centimos enteros seguros para JavaScript. -- Solo taxes_cents queda fuera del descuento propio de Mundomania. -- discountable_cents = total_cents - taxes_cents; incluye la cuota de servicio. -- Costa: mandatory_fees_cents recoge la cuota oficial de servicio de referencia -- por ocupacion, ya incluida en el total y en la base del descuento. -- Es informativa: no se suma ni se resta otra vez. No es un desglose contractual -- separado efectivamente cobrado a cada pasajero (especialmente con promociones). -- Royal: mandatory_fees_cents puede ser cero al ser discrecionales las propinas -- segun condiciones del mercado ES. Estas quedan fuera del total y se explican -- en exclusions[], con politica oficial en exclusionsSourceUrl; no son gratis. -- La base es una regla comercial de la agencia sobre el PVP total comprobado, -- no una comision ni una tarifa neta atribuida a la naviera. -- available exige importes completos; unavailable conserva la comprobacion, -- aunque no tenga importes. Ausencia de quote = pendiente de comprobar, no agotado. CREATE TABLE cruise_verified_quote_document ( id TEXT PRIMARY KEY, product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, adults INTEGER NOT NULL CHECK (typeof(adults) = 'integer'), children_count INTEGER NOT NULL CHECK (typeof(children_count) = 'integer'), age_match_mode TEXT NOT NULL CHECK (age_match_mode IN ('exact','ranges')), range_evidence_url TEXT, cabin TEXT NOT NULL CHECK (cabin IN ('interior','exterior','balcon')), plan TEXT NOT NULL CHECK (plan IN ('full-board','easy','mydrinks')), status TEXT NOT NULL CHECK (status IN ('available','unavailable')), currency TEXT NOT NULL CHECK (currency = 'EUR'), checked_at TEXT NOT NULL, expires_at TEXT, total_cents INTEGER, taxes_cents INTEGER, mandatory_fees_cents INTEGER, discountable_cents INTEGER, discount_basis TEXT CHECK (discount_basis IS NULL OR discount_basis IN ('excluding_taxes','total_when_taxes_unitemized')), source_url TEXT NOT NULL CHECK (length(trim(source_url)) > 0), cabin_name TEXT, supplier_fare_name TEXT, supplier_fare_code TEXT, supplier_cabin_category TEXT, supplier_occupancy_code TEXT, quote_type TEXT, inventory_availability TEXT CHECK (inventory_availability IS NULL OR inventory_availability IN ('unverified','unavailable')), price_basis TEXT, availability_note TEXT, service_charges_included INTEGER CHECK (service_charges_included IS NULL OR service_charges_included IN (0,1)), exclusions_source_url TEXT, FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_sailing_document(product_id, id), FOREIGN KEY (product_id, plan) REFERENCES cruise_verified_plan_document(product_id, id), CHECK ((adults = 2 AND children_count IN (0,1,2)) OR (adults = 3 AND children_count = 1) OR (adults = 4 AND children_count = 0)), CHECK (age_match_mode <> 'ranges' OR (range_evidence_url IS NOT NULL AND length(trim(range_evidence_url)) > 0)), CHECK (julianday(checked_at) IS NOT NULL), CHECK (expires_at IS NULL OR julianday(expires_at) IS NOT NULL), CHECK (total_cents IS NULL OR (typeof(total_cents) = 'integer' AND total_cents BETWEEN 0 AND 9007199254740991)), CHECK (taxes_cents IS NULL OR (typeof(taxes_cents) = 'integer' AND taxes_cents BETWEEN 0 AND 9007199254740991)), CHECK (mandatory_fees_cents IS NULL OR (typeof(mandatory_fees_cents) = 'integer' AND mandatory_fees_cents BETWEEN 0 AND 9007199254740991)), CHECK (discountable_cents IS NULL OR (typeof(discountable_cents) = 'integer' AND discountable_cents BETWEEN 0 AND 9007199254740991)), CHECK (status <> 'available' OR (total_cents IS NOT NULL AND total_cents > 0 AND discountable_cents IS NOT NULL AND ((discount_basis IS 'total_when_taxes_unitemized' AND taxes_cents IS NULL AND service_charges_included IS 1 AND discountable_cents=total_cents) OR (discount_basis IS NOT 'total_when_taxes_unitemized' AND taxes_cents IS NOT NULL AND mandatory_fees_cents IS NOT NULL)))), CHECK (taxes_cents + discountable_cents = total_cents), CHECK (mandatory_fees_cents <= discountable_cents) ); CREATE INDEX cruise_verified_quote_lookup_idx ON cruise_verified_quote_document (product_id, sailing_id, adults, children_count, cabin, plan, status); -- Las edades son las cumplidas al viajar, enteras 0..17. La naviera decide el -- importe de cada edad. No se asume gratuidad del bebe ni un precio universal. CREATE TABLE cruise_verified_quote_child_age_document ( quote_id TEXT NOT NULL REFERENCES cruise_verified_quote_document(id), child_index INTEGER NOT NULL CHECK (typeof(child_index) = 'integer' AND child_index BETWEEN 0 AND 1), age INTEGER NOT NULL CHECK (typeof(age) = 'integer' AND age BETWEEN 0 AND 17), PRIMARY KEY (quote_id, child_index) ); -- childAgeRanges[]: cada menor debe poder asignarse a un rango diferente. -- Se conserva la multiplicidad; el orden de hermanos no cambia la tarifa. -- En las cotizaciones Costa acreditadas se usa 2..17, incluidos. No extiende -- ese rango a bebes, otras navieras ni otras cotizaciones sin evidencia. -- Las cotizaciones familiares Royal acreditadas usan 1..12; se exige su propia -- rangeEvidenceUrl. La edad cumplida se sigue seleccionando individualmente. CREATE TABLE cruise_verified_quote_child_age_range_document ( quote_id TEXT NOT NULL REFERENCES cruise_verified_quote_document(id), child_index INTEGER NOT NULL CHECK (typeof(child_index) = 'integer' AND child_index BETWEEN 0 AND 1), min_age INTEGER NOT NULL CHECK (typeof(min_age) = 'integer' AND min_age BETWEEN 0 AND 17), max_age INTEGER NOT NULL CHECK (typeof(max_age) = 'integer' AND max_age BETWEEN 0 AND 17), PRIMARY KEY (quote_id, child_index), CHECK (min_age <= max_age) ); CREATE TABLE cruise_verified_quote_inclusion_document ( quote_id TEXT NOT NULL REFERENCES cruise_verified_quote_document(id), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), description TEXT NOT NULL, PRIMARY KEY (quote_id, position) ); -- exclusions[]: condiciones/costes excluidos que acompanan el total mostrado. -- No son datos de viajeros. La fuente de la politica reside en quote. CREATE TABLE cruise_verified_quote_exclusion_document ( quote_id TEXT NOT NULL REFERENCES cruise_verified_quote_document(id), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), description TEXT NOT NULL, PRIMARY KEY (quote_id, position) ); -- comparedSupplierFares[] conserva alternativas comprobadas para acreditar -- la seleccion de la tarifa barata. No autoriza otras ocupaciones o fechas. CREATE TABLE cruise_verified_compared_supplier_fare_document ( quote_id TEXT NOT NULL REFERENCES cruise_verified_quote_document(id), position INTEGER NOT NULL CHECK (typeof(position) = 'integer' AND position >= 0), code TEXT NOT NULL, total_cents INTEGER NOT NULL CHECK (typeof(total_cents) = 'integer' AND total_cents BETWEEN 0 AND 9007199254740991), PRIMARY KEY (quote_id, position) ); -- Estado de busqueda efimero. Puede estar incompleto mientras se edita; -- solo findQuote determina si una seleccion completa tiene tarifa utilizable. -- No es una tabla de solicitudes ni se conserva informacion de viajeros. CREATE TABLE cruise_verified_selection_document ( document_id INTEGER PRIMARY KEY CHECK (document_id = 1), port TEXT NOT NULL CHECK (port IN ('Barcelona','Valencia')), month INTEGER CHECK (month IS NULL OR month IN (6,7,8,9)), line TEXT NOT NULL CHECK (line IN ('all','msc','royal','costa')), party TEXT NOT NULL CHECK (party IN ('2-0','2-1','2-2','3-1','4-0')), product_id TEXT, sailing_id TEXT, detail_month INTEGER CHECK (detail_month IS NULL OR detail_month IN (6,7,8,9)), adults INTEGER NOT NULL CHECK (adults IN (2,3,4)), children_count INTEGER NOT NULL CHECK (children_count BETWEEN 0 AND 2), cabin TEXT CHECK (cabin IN ('interior','exterior','balcon')), plan TEXT CHECK (plan IN ('full-board','easy','mydrinks')), detail_view TEXT NOT NULL CHECK (detail_view IN ('overview','gallery','itinerary','package')), photo_index INTEGER NOT NULL CHECK (typeof(photo_index) = 'integer' AND photo_index BETWEEN 0 AND 9), package_id TEXT CHECK (package_id IS NULL OR package_id IN ('full-board','easy','mydrinks')), detail_scroll REAL NOT NULL CHECK (detail_scroll >= 0), FOREIGN KEY (product_id, sailing_id) REFERENCES cruise_verified_sailing_document(product_id, id), FOREIGN KEY (product_id, plan) REFERENCES cruise_verified_plan_document(product_id, id), FOREIGN KEY (product_id, package_id) REFERENCES cruise_verified_plan_document(product_id, id), CHECK ((adults = 2 AND children_count IN (0,1,2)) OR (adults = 3 AND children_count = 1) OR (adults = 4 AND children_count = 0)) ); CREATE TABLE cruise_verified_selection_child_age_document ( selection_id INTEGER NOT NULL REFERENCES cruise_verified_selection_document(document_id), child_index INTEGER NOT NULL CHECK (child_index BETWEEN 0 AND 1), age INTEGER CHECK (age IS NULL OR (typeof(age) = 'integer' AND age BETWEEN 0 AND 17)), PRIMARY KEY (selection_id, child_index) ); -- Resultado derivado de priceQuote: no se altera la cotizacion fuente. -- round(total) = floor((totalCents-discountCents)/500)*500. -- discountCents = floor((totalCents-taxesCents)*percent/100) durante la campana; -- discountableCents debe coincidir exactamente con totalCents-taxesCents. -- mandatoryFeesCents es informativo, no reduce esa base ni aumenta el total. -- fuera de ella = 0. roundingCents se informa aparte del descuento comercial. CREATE TABLE cruise_verified_calculated_price_document ( quote_id TEXT PRIMARY KEY REFERENCES cruise_verified_quote_document(id), total_cents INTEGER NOT NULL CHECK (total_cents >= 0 AND total_cents % 500 = 0), original_total_cents INTEGER NOT NULL CHECK (original_total_cents > 0), discount_cents INTEGER NOT NULL CHECK (discount_cents >= 0), rounding_cents INTEGER NOT NULL CHECK (rounding_cents BETWEEN 0 AND 499), promotion_applied INTEGER NOT NULL CHECK (promotion_applied IN (0,1)), CHECK (total_cents + discount_cents + rounding_cents = original_total_cents), CHECK (promotion_applied = 1 OR discount_cents = 0) ); -- CONTRATOS FUNCIONALES (comentarios: SQLite no implementa estas funciones JS). -- Auditoria de construccion en roundtrip-review.json. Estas tablas son solo -- documentales y no pertenecen al catalogo publico ni a una base activa. Las -- referencias de producto/salida pueden pertenecer a evidencia retirada, por -- eso no tienen claves foraneas contra el catalogo visible ya filtrado. CREATE TABLE cruise_verified_route_filter_document ( document_id INTEGER PRIMARY KEY CHECK (document_id = 1), rule TEXT NOT NULL, removed_products_count INTEGER CHECK (removed_products_count >= 0), removed_sailings_count INTEGER CHECK (removed_sailings_count >= 0), unverified_sailings_count INTEGER CHECK (unverified_sailings_count >= 0) ); CREATE TABLE cruise_verified_route_removed_product_document ( product_id TEXT PRIMARY KEY ); CREATE TABLE cruise_verified_route_review_document ( product_id TEXT NOT NULL, sailing_id TEXT NOT NULL, sailing_date TEXT, status TEXT NOT NULL CHECK (status IN ('roundtrip','one_way','unverified')), reason TEXT, departure_port TEXT, arrival_port TEXT, source_url TEXT, PRIMARY KEY (product_id,sailing_id) ); -- normalizePort(value): elimina diferencias de mayusculas, acentos, espacios -- y calificadores de pais/ciudad entre parentesis o tras coma/barra vertical; -- conserva diferencias de puertos fisicos, con alias explicitos Ravena/Ravenna -- y Roma/Rome. Navegacion y valores pendientes no identifican un puerto. -- assessRoundTrip(product,sailing): exige itinerario con summaryOnly=false, -- sourceUrl HTTP(S), duracion positiva y puertos de dia 1 y dia nights+1. -- Excluye experiencias descritas sin desembarque; el inicio debe coincidir -- con el puerto de salida. Devuelve status roundtrip/one_way y extremos/fuente, -- o unverified con reason. No usa arrivalPort copiado por defecto ni el ultimo -- puerto de una ruta resumida o truncada como prueba de desembarque final. -- filterVerifiedOneWay(products,quotes): retira solo salidas one_way, elimina -- productos sin salidas y tarifas que queden sin producto/salida visible. -- Conserva sin modificaciones las fotos, planes, precios y fuentes de las -- salidas restantes. No muta la investigacion ni los documentos originales. -- Devuelve products/quotes y review: rule, removedProducts[], removedSailings[] -- y unverifiedSailings[]. Cada revision conserva productId,sailingId,date, -- status,reason cuando procede,departurePort,arrivalPort y sourceUrl. -- build-preview escribe esta auditoria fuera de preview, sin publicarla. -- CABINS: interior, exterior, balcon. allowedParties: 2A, 2A1N, 2A2N, 3A1N, 4A. -- amount(n): Number.isSafeInteger(n) && n>=0. -- agesKey(ages): acepta solo array de enteros 0..17; ordena copia numericamente -- y une con coma. Compara multiconjuntos exactos, conservando edades repetidas. -- Las edades sin seleccionar no se convierten en 0 ni en una edad por defecto. -- validDate(value): fecha real del calendario dentro de junio-septiembre 2027. -- matchesAges(quote,ages): si childAges existe compara edades exactas. En otro -- caso exige rangeEvidenceUrl y tantos rangos childAgeRanges como menores, -- con min/max enteros 0..17 y min<=max. fits() busca una asignacion uno-a-uno -- de cada edad seleccionada a un rango inclusivo; sin asignacion no hay precio. -- priceQuote(quote,promotion,now): null si estado distinto de available, -- moneda distinta de EUR, fuente vacia, checkedAt invalido, componentes invalidos, -- suma de componentes > total, now invalido o expiresAt invalido/vencido. -- El instante exacto expiresAt sigue siendo valido. No hay caducidad implicita -- calculada desde checkedAt. Promocion valida: percent numerico 0..100 e instante -- dentro de startAt..endAt (inclusivos). Devuelve los cinco campos de price. -- findQuote(data,selection,now): exige ocupacion permitida, edades concretas, -- cabina permitida, producto/salida/plan existentes, puerto permitido y meses -- junio-septiembre 2027. Royal solo admite full-board. Busca coincidencia de -- productId,sailingId,adults,edades exactas o rangos acreditados,cabin,plan; -- cabinViewPriority(quote): interior=0; exterior/balcon normal=0; -- vista obstruida o parcial=2; vista interior, generica o garantizada=3. -- Respeta expresiones de negacion de obstruccion. findQuote prioriza vista normal -- aunque sea mas cara, y solo usa otras vistas si no hay normal con precio valido -- para esa misma seleccion. Dentro de cada prioridad elige el menor total final. -- normalizeVayaPublicPrices conserva alternativas por codigo de categoria del -- proveedor: no colapsa un exterior/balcon normal en otro obstruido mas barato. -- Si ninguna cotizacion sirve, devuelve la observacion -- unavailable mas reciente con fuente, fecha comprobable y expiresAt no -- vencido si existe. Evidencia negativa vencida o con caducidad invalida no -- produce agotado; si no hay otra evidencia utilizable, devuelve null. -- null NO significa agotado; no permitir reserva con precio ausente o generico. -- Los filtros visuales no crean tarifas ni implican disponibilidad. Una opcion -- confirmada unavailable puede deshabilitarse conservando su evidencia. -- El resultado disponible sigue sujeto a reconfirmacion de la agencia. -- app.js state: port,month,line,party,ages,productId,sailingId,detailMonth,cabin, -- plan,detailView,photoIndex,packageId,detailScroll. selection() deriva adults de -- party y copia ages como childAges. Los -- campos de filtro y seleccion conviven en selection_document; ages usa la -- tabla selection_child_age_document. month=null representa todos los meses. -- party()/agesComplete(): ocupacion seleccionada y edades completas enteras. -- productById()/selectedSailing(): referencias de catalogo, sin consulta remota. -- matchQuote()/priced(): delegan en findQuote/priceQuote; sin edades completas -- no se obtiene precio. availablePlans() excluye bebidas para Royal. -- sailingsFor(): filtra junio-septiembre 2027 y ordena por fecha. -- lowestPrice(): minimo calculable para ocupacion/edades entre salidas filtradas, -- planes del producto y tres categorias; es el desde de la tarjeta, no una -- tarifa aplicable a toda fecha/cabina/ocupacion. -- renderCards identifica junto al minimo su fecha, categoria y regimen. -- openProduct inicia esa misma oferta, sin heredar una seleccion previa -- de camarote/regimen; la restauracion explicita del historial se conserva. -- renderFilters()/renderCards(): filtran puerto, mes y naviera; no suprimen un -- itinerario por carecer de tarifa. Las edades pendientes muestran su aviso. -- renderPromotion(): aviso segun ventana temporal; hora final Europe/Madrid. -- partyControls(): selector de ocupacion y edad 0..17, sin edad predeterminada. -- renderCalendar(): habilita solamente dias de salidas publicadas. Un dia -- activo no garantiza plazas para la seleccion; renderQuote muestra su estado. -- renderDetail()/renderQuote()/choiceAvailability(): exponen fecha, ocupacion, -- camarote y plan. choiceAvailability devuelve disabled,label; compara todos -- los regimenes para una cabina o todas las cabinas para un regimen. Solo -- deshabilita si TODAS esas combinaciones tienen unavailable comprobado. -- Mantiene accesible una opcion con tarifa en otra combinacion o pendiente. -- sin precio utilizable deshabilitan Solicitar reserva. Fuente y checkedAt -- son visibles, y el total incluye las tasas y los cargos obligatorios. -- whatsAppLink(): construye un borrador con barco, fecha, ruta, duracion, -- ocupacion, edades, camarote, regimen, supplierFareName, inclusions[], -- referencia y total si hay tarifa. Solo abre al -- pulsar el enlace; no envia mensajes ni crea reservas o registros locales. -- occupancyText() compone viajeros/edades. La solicitud pide reconfirmacion. -- openProduct(): abre la combinacion del menor precio mostrado si existe; -- si no, primera salida filtrada, interior/pension completa; -- conserva foco e inserta estado de historial. Puede restaurar una seleccion -- comprobando producto, salida, mes, cabina, plan, ocupacion y edades. -- routeFor()/arrivalPortFor(): resuelven ruta/puerto por salida con los fallbacks -- descritos. La tarjeta avisa cuando las rutas varian; detalle usa la elegida. -- snapshot() copia estado y edades; saveHistory() conserva snapshot en -- history.state.mundomaniaCruises y producto abierto en history.state.cruiseDetail. -- closeDetail()/finishClose() cierran con volver/Escape; popstate permite -- reabrir con Avanzar sin insertar otro historial y restaura seleccion/foco. -- window.MundomaniaCruises expone getState(snapshot), openProduct y closeDetail. -- No es una API de servidor ni una funcion que reserve plazas. -- restoreFilterState valida puerto, mes, naviera, ocupacion y edades guardadas. -- nightsText muestra noches conocidas o Duracion a confirmar si faltan. -- quoteExclusions(quote): acepta exclusions[] y filtra textos no vacios. -- renderExclusions(quote,compact): muestra exclusiones junto al desde en la -- tarjeta o bajo el total en detalle; el detalle enlaza exclusionsSourceUrl. -- Las propinas discrecionales Royal quedan explicitamente fuera del total; -- no se convierten en mandatoryFeesCents ni se presentan como incluidas. -- whatsAppLink tambien incorpora exclusions[] y usa cabinName de la tarifa -- cuando existe, conservando los avisos al solicitar la reconfirmacion. -- Cambio de ocupacion/edades reconstruye filtros, tarjetas y detalle; reset de -- filtros limpia mes/naviera, conservando puerto y viajeros. Error de imagen -- muestra un icono en su lugar. Estado en navegador/historial, sin base de datos, -- localStorage, motor de reservas ni persistencia de solicitudes en servidor. -- Utilidades: esc/safeUrl protegen HTML/URLs; money/dateObject/dateText/ -- monthNumber formatean importes/fechas; rememberFocus/restoreFocus preservan -- navegacion por teclado; imageMarkup crea foto o icono alternativo. -- Contrato: normalizeMscPublishedPrices valida fuente oficial, fechas y doble -- ocupacion; un fallo generico del motor no demuestra agotado. El calendario -- con agotado expreso produce unavailable para su camarote y ocupacion exactos. -- Royal SSR: una respuesta FALLBACK_ROOM nunca presta su precio al camarote -- solicitado. El aviso explicito de falta de habitaciones produce unavailable -- solamente para la consulta realizada. No se guarda ni se confirma reserva. -- availabilityNote se conserva en tarjeta, resumen de precio y borrador WhatsApp. -- Texto del detalle con promocion vigente: descuento venta anticipada verano -- 2027, solo hasta el 15 de octubre o fin de plazas. No modifica el calculo. -- galleryFor(): filtra URL seguras y limita a diez fotografias por producto. -- Construccion del catalogo: product.image y product.imageAlt proceden de -- gallery[0].url y gallery[0].alt. Portadas y galerias usan archivos locales -- verificados incluidos en la web; sourceUrl conserva la referencia oficial. -- portIcon(): icono identificativo de Barcelona/Valencia, sin datos comerciales. -- dateAlternatives(): obtiene y ordena precios validos para la misma fecha, -- viajeros y edades en otras combinaciones de camarote/plan del producto. -- alternativesMarkup(): si la seleccion no tiene precio, ofrece hasta tres -- alternativas verificadas, con importe total y boton de seleccion explicito. -- Nunca cambia fecha, ocupacion o tarifa automaticamente ni fabrica precios. -- openExtra(view,value): abre gallery/itinerary/package; valida fotos o plan, -- guarda scroll del resumen y agrega una entrada al historial con snapshot. -- detailHeading()/renderExtra(): subpantallas dentro del mismo dialogo, titulo -- accesible y botones Volver al crucero arriba y abajo. Gallery muestra foto, -- contador, miniaturas, texto alternativo y fuente. photoIndex se limita al -- rango disponible; botones y flechas del teclado cambian foto sin crear una -- entrada de historial por imagen. Itinerary usa days solo cuando summaryOnly -- es falso y hay dias; en otro caso stops o ruta publicada, sin inventar dias. -- Package distingue incluye, no incluye, notas y fuente de plan.details. -- Ver un paquete no lo selecciona ni habilita una cotizacion pendiente. -- packageName(plan): nombre legible de Paquete Easy, Paquete My Drinks o -- Pension completa. packageEntry(plan): boton informativo accesible que abre -- que incluye, condiciones y opciones de menores; no cambia el plan elegido. -- cabinQualification(quote) deriva del nombre completo las limitaciones de -- vista (obstruida, parcial, posible obstruccion, paseo, Central Park, barrio, -- sin mar o por asignar) y camarote garantizado, respetando "sin obstruccion". -- cabinLabel combina tipo y calificacion; qualifiedPrice y priceCabinNote -- colocan esa calificacion junto al importe. Se conserva en selector, tarjeta, -- resumen, alternativas y borrador WhatsApp, sin cambiar categoria ni precio. -- closeDetail()/popstate: desde una subpantalla vuelve al resumen conservando -- producto, fecha, viajeros, edades, camarote, plan y desplazamiento. Desde el -- resumen regresa al listado. Escape tiene el mismo comportamiento. Avanzar -- restaura la subpantalla y foto guardadas. No hay persistencia en servidor. -- Eventos gallery/itinerary/package abren subpantalla; photo/prev/next actualizan -- foto; priced-option aplica la alternativa elegida; port limpia el filtro de -- naviera solo si esa naviera no tiene productos en el nuevo puerto. -- buildPreviewResourceVersioning: la importacion de pricing.mjs incorpora su -- huella de contenido; app.js se versiona por su contenido exportado con esa -- dependencia. Un cambio solo de reglas invalida ambas claves de cache. -- parseBookDetail(file,text,research): importacion privada de cotizaciones de -- MSC Book. Coteja barco, ruta circular, fecha, categoria, ocupacion y regimen -- con la captura previa del buscador. El total de cada pasajero debe coincidir -- con crucero + tasas + CSH y la suma con el total exacto por camarote. -- Cada detalle acredita sus propias tasas: no se trasladan a otras salidas, -- ocupaciones ni categorias. Las edades por rango requieren la cotizacion -- previa explicita por tramo infantil; una edad individual no crea un rango. -- normalizeMscGroupQuotes admite MSC Book solo mediante URL canonica publica -- del buscador, sin query, fragmento ni credenciales. Capturas y componentes -- permanecen en entrega privada. No se crea tabla activa ni se retiene cabina. -- Una tarifa generica de bebidas solo se clasifica como Easy si el mismo -- detalle enumera EASY PACKAGE y MINORS PACKAGE; de lo contrario queda pendiente. -- Categorias admitidas por el importador: IR1/IR2, OR1/OR2 y BR1..BR4, -- siempre acreditadas por la busqueda de esa fecha, ocupacion y regimen. -- La reimportacion conserva checkedAt del archivo de captura, no renueva -- artificialmente su antiguedad. Las fechas de calendario por si solas no -- habilitan precios, plazas ni ocupaciones que no tengan cotizacion propia. -- updateMscProgress: control privado por fecha del producto World Asia. -- Compara las 12 combinaciones de 2 adultos con 1/2 menores 2-11, tres tipos -- de camarote y PC/Easy contra detalles importados. Publica solo un informe -- local de cobertura (date, verifiedFamilyCombinations, -- missingFamilyCombinations, familyCheckComplete), nunca precios inferidos. -- complete significa cobertura de ese alcance familiar, no todas las -- ocupaciones posibles ni disponibilidad reconfirmada o publicacion. -- Limite de la comprobacion solicitado: hasta el 10 de septiembre incluido. -- product.ship: el constructor aplica el nombre comercial visible solicitado -- por Mundomania, Costa Esmeralda, al producto costa-smeralda-barcelona. -- Se aplica tambien a imageAlt/gallery.alt. No cambia identidad, referencias -- de proveedor, enlaces, importes ni evidencias tarifarias originales. -- Logitravel exact group quote contract (2026-10-01) -- normalizeMscGroupQuotes admite sourceProvider=logitravel con URL publica canonica. -- Edades exactas; no extrapolar. sourceProvider, observedAgencyTotalCents y -- excludedAgencyDiscountCents son propiedades de entrada privada, sin nueva tabla. -- observedAgencyTotalCents + excludedAgencyDiscountCents = totalCents. -- totalCents = suma tarifas individuales + tasas + servicio obligatorio. -- quote_type=agency_group_quote, price_basis=observed_group_total; origen Logitravel -- y plazas pendientes de reconfirmar. Descuento propio de agencia excluido antes -- del 5% de Mundomania sin tasas y redondeo final a multiplos de cinco. -- Regla comercial autorizada 2026-10-01: discountBasis/discount_basis -- total_when_taxes_unitemized aplica el porcentaje sobre todo el total -- comprobado con tasas y servicio incluidos; taxes_cents es NULL, nunca cero -- inventado. discountable_cents=total_cents. Si las tasas estan desglosadas -- se conserva la base anterior total_cents-taxes_cents. Sin datos INSERT. COMMIT;