The mist hung low over the valley of Eldritch, a thick, silver shroud that swallowed the roots of the ancient oaks. Elias stood at the precipice of the ridge, his worn leather satchel heavy against his hip.
It had been three years since he last smelled the damp earth of his homeland, and the scent was exactly as he remembered: clover, wet slate, and the faint, metallic tang of an approaching storm.
He did not come for the scenery. He came because the letters had stopped. His father, a man whose life was measured by the rhythmic scratching of a quill across parchment, had gone silent.
At the gate of the old library, a sprawling, ivy-choked stone structure that housed the collective memory of the valley, he paused. The great oak doors were slightly ajar.
From within, he could hear it. Not a voice, not a footstep, but a hollow, resonant thrumming that vibrated in his very marrow.
“Father?” he whispered. The word did not echo. It was swallowed whole by the darkness of the hall.
He stepped inside. The air was dry and smelled of old paper and ozone. To his left, the shelves were empty. Thousands of volumes, gone. Only the dust remained, settled in the shape of missing knowledge.
He moved toward the central atrium, where the Great Ledger was supposed to sit. Instead, he found a pillar of shifting, ink-black smoke.
As Elias approached, the smoke coalesced into a shape. A hand, reaching out. Not for help, but for a pen. The silent echo had found its author, and now it was hungry for a new story.
Prompt para agente de IA: UI y cálculos de reserva por unidad de alquiler
Contexto del sistema
Esta plataforma de alquiler tiene propiedades/assets donde cada asset tiene UNA sola unidad de alquiler fija, asignada en su configuración (campo price_unit, valores posibles: night, day, week, hour). El usuario nunca elige la unidad en la pantalla de reserva — la unidad ya viene determinada por el asset que está viendo. La UI de selección de fecha/hora debe renderizar una variante distinta según ese valor.
No construyas un selector de unidad visible al usuario. La variante de input se resuelve en el momento de renderizar el componente de reserva, leyendo asset.price_unit.
Variante 1: price_unit = "night" (alojamiento tipo Airbnb)
UI
- Componente: calendario doble (dos meses visibles), selección de rango por dos clicks (check-in, luego check-out).
- Durante el hover/drag entre el primer y segundo click, resaltar visualmente las noches del rango en preview.
- Las fechas de check-in y check-out se marcan con un estilo distinto al de las noches intermedias (ej. medio-círculo o borde), para indicar visualmente que son los bordes del rango y no noches completas en sí mismas.
- Fechas pasadas: deshabilitadas.
- Fechas ya reservadas: deshabilitadas, con estilo visual distinto (tachado o gris oscuro) al de fechas pasadas.
- Si el asset define
min_nights, al seleccionar el check-in, deshabilitar automáticamente cualquier check-out que resulte en menos demin_nights. No permitir la selección inválida y mostrar el error después — prevenir la selección inválida desde el calendario mismo. - Si hay una fecha ocupada entre el check-in elegido y el intento de check-out, cortar la selección en esa fecha (no permitir “saltar” sobre fechas ocupadas).
- Mostrar precio en vivo debajo del calendario mientras se selecciona el rango:
precio_por_noche × N noches, actualizado en cada cambio de selección. - Si solo se seleccionó check-in y falta check-out, mostrar mensaje guía (ej. “Selecciona tu fecha de salida”) en vez de dejar el estado ambiguo sin feedback.
Backend / cálculo
noches = fecha_checkout - fecha_checkin // en días, SIN sumar 1
subtotal = noches * precio_por_noche
- El día de checkout NO se cuenta como noche habitada. No se cobra.
- Validar en backend (no solo en frontend) que
noches >= min_nightsantes de crear la reserva. - Validar que no haya overlap con reservas existentes para ese asset en ese rango.
Variante 2: price_unit = "day" (alquiler de objetos: autos, equipo, herramientas)
UI
- Componente: calendario doble, visualmente IDÉNTICO al de noches (mismo patrón de dos clicks, mismo resaltado de rango).
- Diferencia obligatoria respecto a noches: debajo del calendario, mostrar de forma fija y no editable por el usuario: “Devolución antes de las {hora_corte}” (ej. 18:00). Este valor viene de una constante global de la plataforma en v1 (no configurable por asset todavía).
- El cálculo de días debe mostrarse explícito y desglosado, no solo el rango de fechas: ej.
4 días × $25.000 = $100.000. El usuario tiene que ver el número de días contado, no inferirlo del rango visual. - Fechas pasadas y ya reservadas: mismas reglas que la variante de noches.
- Si el asset define
min_days, aplicar la misma lógica de prevención de selección inválida que en noches, pero ajustada a la fórmula de días (ver abajo).
Backend / cálculo
dias = (fecha_fin - fecha_inicio) + 1 // SÍ se suma 1, a diferencia de noches
subtotal = dias * precio_por_dia
- Razón de negocio: el objeto está fuera de inventario disponible tanto el día de inicio como el día de devolución, aunque la devolución sea solo por unas horas. Por eso ambos días cuentan completos.
- Cargo por devolución tardía: si la devolución real ocurre después de
hora_cortedel último día, aplicar cargo adicional de un día completo (+ 1 * precio_por_dia). Este chequeo ocurre en el flujo de cierre/devolución de la reserva, no en el momento de la reserva inicial — déjalo como hook/TODO si esa función de devolución aún no existe en el sistema. - Validar en backend overlap de fechas igual que en noches.
No reutilices la misma función de cálculo que noches. Son fórmulas distintas (fin - inicio vs fin - inicio + 1). Si ya existe una función compartida de “calcular unidades entre fechas”, sepárala en dos funciones explícitas: calcularNoches() y calcularDias(), cada una con su propio test unitario que verifique el caso borde (mismo día de inicio y fin, rango de 1 unidad, etc.).
Variante 3: price_unit = "week" (alquiler de larga estancia)
UI
- Componente: NO usar calendario de rango completo. Usar:
- Un selector de fecha única para el inicio (date picker simple).
- Un stepper o dropdown numérico para la cantidad de semanas (ej. 1, 2, 3… con botones +/- o select).
- La fecha de fin se calcula y se muestra como texto derivado, no editable directamente: “Del {fecha_inicio} al {fecha_fin_calculada}”.
- Alternativa aceptable si el equipo de diseño prefiere mantener un calendario visual: calendario de rango pero con snapping forzado a incrementos de 7 días desde la fecha de inicio elegida (el usuario no puede soltar el rango en un día intermedio).
- Mostrar precio en vivo:
N semanas × precio_por_semana. - Validar disponibilidad contra reservas existentes igual que las otras variantes.
Backend / cálculo
fecha_fin = fecha_inicio + (N_semanas * 7) dias
subtotal = N_semanas * precio_por_semana
N_semanases un input directo del usuario (entero, mínimo 1), no se calcula a partir de un rango de fechas elegido libremente.- Validar overlap de disponibilidad sobre el rango resultante (
fecha_inicioafecha_fin).
Variante 4: price_unit = "hour" (salas, estudios, espacios por hora)
UI
- Componente:
- Selector de fecha única (date picker simple, no rango).
- Selector de franja horaria: usar slots predefinidos (ej. bloques de “9:00–10:00”, “10:00–11:00”…) en vez de un time picker de hora libre, salvo que el negocio requiera granularidad distinta a la hora completa.
- Si se requiere granularidad libre (ej. reservas de 30 min o duración variable), usar un slider de rango horario sobre una línea de 24 horas, con los bloques ya ocupados sombreados/deshabilitados visualmente.
- Mostrar slots ya reservados como deshabilitados, no ocultos — el usuario debe ver que existen pero no están disponibles, igual que las fechas ocupadas en las otras variantes.
- Si el asset define
min_hours, prevenir selección de franjas menores a ese mínimo desde el componente mismo. - Mostrar precio en vivo:
N horas × precio_por_hora.
Backend / cálculo
horas = hora_fin - hora_inicio // en horas, sobre la misma fecha
subtotal = horas * precio_por_hora
- Validar que
hora_inicioyhora_finestén dentro del horario operativo del asset (opening_time/closing_time), si ese dato existe. - Validar overlap contra slots ya reservados para ese asset en esa fecha.
- Validar
horas >= min_hours.
Cálculo final común a las 4 variantes (capa compartida)
Una vez calculado subtotal según la variante correspondiente, aplicar la misma lógica de totales para las 4:
service_fee = calcularServiceFee(subtotal) // función ya existente, no la reescribas
deposit = asset.deposit_amount // monto fijo definido por el asset, no calculado
total = subtotal + service_fee + deposit
- El desglose debe mostrarse siempre explícito en la UI antes de confirmar:
subtotal,service_fee,deposit,total, cada uno en su propia línea. No mostrar solo el total. - El
depositse muestra pero se debe indicar claramente si es reembolsable o no (campodeposit_refundabledel asset), con un texto breve aclaratorio debajo del monto.
Requisitos técnicos transversales (aplican a las 4 variantes)
- No confíes solo en el frontend para validar disponibilidad o mínimos. Toda validación de rango, overlap, y mínimos debe repetirse en el backend al momento de crear la reserva, porque el frontend puede ser bypaseado.
- Separá las funciones de cálculo de unidades por tipo. No uses una sola función genérica
calcularUnidades(inicio, fin)para las 4 variantes — son fórmulas distintas con casos borde distintos. Crea:calcularNoches(),calcularDias(),calcularSemanas(),calcularHoras(), cada una testeada por separado. - Tests unitarios obligatorios para casos borde de cada función:
- Noches: mismo día de check-in y check-out (debería ser inválido, no 0 noches silencioso).
- Días: un solo día de alquiler (inicio = fin, debe dar 1 día, no 0).
- Semanas: 1 semana exacta.
- Horas: franja de exactamente 1 hora, y franja que cruza medianoche (decidir si se permite o no, y manejarlo explícitamente en vez de dejarlo como comportamiento indefinido).
- El componente de UI debe leer
asset.price_unituna sola vez al cargar la pantalla de reserva y renderizar la variante correspondiente. No debe haber lógica condicional repartida en múltiples componentes — centralizá la decisión de qué variante renderizar en un único punto (ej. un componenteBookingDateSelectorque recibeprice_unitcomo prop y hace el switch internamente). - Reutilizá el componente de calendario doble entre noches y días (son visualmente idénticos), pero parametrizá la función de cálculo que se le pasa como prop, para no duplicar el componente visual ni mezclar la lógica de cálculo.
Para regocijo de los caballeros dornienses, Dunk no dejaba de llorar mientras cavaba.
—Aquí el agua vale mucho, señor —le dijo uno—. No es bueno desperdiciarla así.
—¿Por qué lloráis? —añadió otro tras reírle la gracia al primero—. No era más que un caballo, y de los malos.
«Tostada —pensó Dunk mientras cavaba—. Se llamaba Tostada. Me llevó a lomos muchos años, y nunca corcoveó ni lanzó bocados.» Al lado de los lustrosos corceles de la arena que montaban los dornienses, caballos de testuz elegante, cuello largo y crines sedosas, el penco había resultado ridículo, pero había dado todo lo que podía ofrecer.
—¿Estás llorando por un rocín de lomo ensillado? —le preguntó ser Arlan con su voz de anciano—. Pues por mí no derramaste ni una lágrima, muchacho, y fui yo quien te puso a lomos de ese animal. —Soltó una risita para que quedara claro que no era un reproche—. Así es Dunk el Tocho, seso de corcho.
—Por mí tampoco lloró —repuso Baelor Rompelanzas desde la tumba—, y eso que era su príncipe, la esperanza de Poniente. Los dioses no querían que muriera tan joven.
—Mi padre solo tenía treinta y nueve años —añadió el príncipe Valarr—. Habría sido un gran rey, el mejor desde Aegon el Dragón. —Miró a Dunk con gélidos ojos azules—. ¿Por qué se lo han llevado a él los dioses y no a vos? —El Príncipe Joven tenía el cabello castaño claro de su padre, pero surcado por un mechón de oro blanco.
«Estáis muertos —habría querido gritar Dunk—. Los tres estáis muertos, ¿por qué no me dejáis en paz?» A ser Arlan se lo había llevado un enfriamiento; al príncipe Baelor, el golpe que su hermano le asestó durante el juicio a siete de Dunk; a su hijo Valarr, la peste de la gran primavera. «De eso no tuve yo la culpa. Estábamos en Dorne; ni siquiera nos enteramos.»
—Estás loco —le dijo el anciano—. Vas a matarte por hacer el idiota, y te juro que no te cavaremos una tumba. En el mar de arena, el agua es un tesoro.
—Fuera de mi presencia, ser Duncan —añadió Valarr—. Fuera.
Egg lo ayudó a cavar. Como no tenía pala, se servía de las manos, pero el viento volvía a meter la arena en la tumba tan deprisa como la sacaban. Era como intentar hacer un agujero en el mar.
«Tengo que seguir cavando —se dijo Dunk, aunque le dolían la espalda y los hombros—. Tengo que enterrarlo muy hondo para que los perros de la arena no lo encuentren. Tengo que…»
—¿… morir? —preguntó Rob el Grandullón, el bobo, desde el fondo de la tumba. No parecía tan grande allí tumbado, tan quieto y frío, con aquel tajo rojo en el vientre.
Dunk se detuvo y lo miró.
—Tú no estás muerto. Estás abajo, durmiendo en la bodega. —Pidió ayuda a ser Arlan con los ojos—. Decídselo vos, señor —suplicó—. Decidle que salga de la tumba.
Pero el que se encontraba a su lado no era ser Arlan del Árbol de la Moneda, sino ser Bennis del Escudo Pardo. El caballero pardo soltó un cloqueo.
—Dunk el Tocho —bufó—, la muerte por destripamiento es lenta, pero segura. No sé de nadie que haya sobrevivido con las entrañas colgando.
Tenía espumilla roja en los labios; se volvió y escupió, y las arenas blancas absorbieron el salivazo. Trabu estaba detrás con una flecha en el ojo y derramaba lentas lágrimas rojas. También se encontraba allí Wat Aguado, con la cabeza cortada casi en dos; y el viejo Lem, y Pate, el de los ojos enrojecidos, y los demás. Todos habían estado mascando hojamarga, como Bennis…, o eso le pareció a Dunk, pero enseguida se dio cuenta de que lo que les salía de la boca era sangre.
«Están muertos. Todos están muertos.»
Y el caballero pardo soltó una carcajada como un rebuzno.
—Eso es, así que venga, Tocho, ¡a trabajar! Tienes que cavar más tumbas. Ocho para ellos, una para mí y otra para ser Estulto, y no te olvides de otra para el crío pelón.
La pala se le resbaló de las manos.
—¡Corre, Egg! —gritó—. ¡Corre, vámonos!
Pero la arena cedía bajo sus pies. Cuando el niño intentó salir del agujero, las paredes se desmoronaron, y Dunk vio como sepultaban a Egg, que abrió la boca para gritar. Trató de alcanzarlo, pero la arena se amontonaba a su alrededor y lo arrastraba a la tumba, llenándole la boca, la nariz, los ojos…
Al amanecer, ser Bennis se dispuso a enseñar a los reclutas a formar un muro de escudos. Los alineó hombro con hombro, con los broqueles arrimados y las lanzas apuntando hacia delante como afilados dientes de madera. Y, entonces, Dunk y Egg montaron y cargaron contra ellos.
Maestre se paró en seco a cuatro pasos de las lanzas, pero Trueno estaba bien entrenado. El imponente caballo de batalla galopó en línea recta cada vez más deprisa, mientras las gallinas aleteaban y cacareaban espantadas entre sus patas. El pánico debía de ser contagioso, porque una vez más Rob el Grandullón soltó la lanza y echó a correr, con lo que dejó un hueco en mitad del muro. En vez de cerrarlo, los demás guerreros de Tenaz también huyeron, y Trueno aplastó los escudos abandonados antes de que Dunk tuviera tiempo de tirar de las riendas. Las ramas entrelazadas crujieron y se astillaron bajo las herraduras del caballo. En medio de la desbandada de gallinas y campesinos, ser Bennis soltó una retahíla de tacos y maldiciones, y Egg se esforzó cuanto pudo por contener la risa, aunque al final fracasó.
—Ya basta. —Dunk detuvo a Trueno, se desabrochó el yelmo y se lo quitó—. Si se comportan así en combate, acabarán todos muertos.
«Y tú y yo, también, desde luego.» Era temprano, pero el sol ya picaba, y se sentía sucio y pegajoso como si no se hubiera bañado en la vida. Le dolía la cabeza y no era capaz de olvidar el sueño de la noche anterior. «No fue así —se decía una y otra vez—. Las cosas ocurrieron de otra manera.» Tostada había muerto en el largo y seco trayecto a Vaith, eso sí era verdad. Egg y él tuvieron que compartir montura hasta que el hermano de Egg les dio a Maestre. Pero lo demás…
«No lloré. Tenía ganas, pero no lloré.» Y, sí, había querido enterrar al caballo, pero los dornienses se negaron a esperar.
—Los perros de la arena tienen que comer y alimentar a sus cachorros —le dijo un caballero dorniense mientras lo ayudaba a quitar la silla y la brida al rocín—. Los perros o las arenas se comerán la carne, y antes de un año solo quedarán los huesos mondos. Esto es Dorne, amigo mío.
Dunk recordó todo aquello y no pudo evitar preguntarse quién se comería la carne de Wat, y la de Wat, y la de Wat.
«A lo mejor en el Jaquel hay peces jaquelados.»
Cabalgó de vuelta a la torre y desmontó.
—Egg, ve con ser Bennis a buscarlos y traedlos de vuelta. —Le tiró el yelmo de malos modos y se dirigió hacia las escaleras.
Ser Eustace se encontraba en la penumbra de sus habitaciones.
—No ha estado muy bien.
—No, mi señor —convino Dunk—. No nos valen.
«Una espada leal debe obediencia a su señor, pero esto es una locura.»
—Ha sido su primera vez. Sus padres y sus hermanos eran iguales o peores cuando empezaron a entrenarse. Mis hijos trabajaron con ellos día tras día durante dos semanas antes de que acudiéramos a ayudar al rey. Los convirtieron en soldados.
—Y cuando llegó la batalla, mi señor, ¿qué tal les fue? —quiso saber Dunk—. ¿Cuántos volvieron con vos?
El anciano caballero se quedó mirándolo.
—Lem, Pate y Dake —acabó por responder—. Dake era nuestro forrajeador, el mejor que he visto nunca. Siempre teníamos la barriga llena. Volvieron tres, señor. Tres y yo. —Le tembló el bigote—. Tal vez nos lleve más de dos semanas.