From a951c246b4bb9991547fec5e98c0b48f107c1c93 Mon Sep 17 00:00:00 2001 From: gersonsebastianx <306426556+gersonsebastianx@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:51:00 -0500 Subject: [PATCH 1/2] feat(domain): build the Cineplanet purchase link for a showtime The README states that buying happens outside the CLI. This keeps that boundary but removes the tedious part: instead of redoing city, movie, date, venue and time by hand on the website, the link lands straight on the seat map of the chosen showtime. Cineplanet exposes the movie slug as `movieDetailsUrl`, so `Movie` now carries it and `Showtime::purchase_url` assembles the URL. The live contract test asserts the slug still arrives, since a missing slug would break the link silently. Co-Authored-By: Claude Opus 5 --- src/demo.rs | 6 ++--- src/domain.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/live.rs | 6 +++++ 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/demo.rs b/src/demo.rs index bc38614..1372e7f 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -7,7 +7,7 @@ pub fn catalog() -> Catalog { Movie { id: "spider-man".into(), title: "Spider-Man: Un nuevo día".into(), - movie_details_url: None, + movie_details_url: Some("spider-man-un-nuevo-dia".into()), duration_minutes: Some(145), genre: Some("Acción".into()), rating: Some("APT".into()), @@ -15,7 +15,7 @@ pub fn catalog() -> Catalog { Movie { id: "odyssey".into(), title: "La Odisea".into(), - movie_details_url: None, + movie_details_url: Some("la-odisea".into()), duration_minutes: Some(132), genre: Some("Aventura".into()), rating: Some("+14".into()), @@ -23,7 +23,7 @@ pub fn catalog() -> Catalog { Movie { id: "toy-story".into(), title: "Toy Story 5".into(), - movie_details_url: None, + movie_details_url: Some("toy-story-5".into()), duration_minutes: Some(104), genre: Some("Animación".into()), rating: Some("APT".into()), diff --git a/src/domain.rs b/src/domain.rs index ea7c112..cc70b3c 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -74,6 +74,27 @@ pub struct Showtime { pub seat_map: SeatMap, } +impl Showtime { + /// Enlace al mapa de butacas de esta función en Cineplanet. + /// + /// La compra sigue ocurriendo fuera de la CLI, pero este enlace evita + /// rehacer a mano la elección de ciudad, película, fecha, sede y hora: cae + /// directo en la función ya elegida. Cineplanet retiene las butacas unos + /// minutos desde que se abre. + /// + /// `id` viene compuesto como `sede-sesión`; la URL sólo quiere la sesión. + pub fn purchase_url(&self, movie_slug: &str) -> String { + let session_id = self + .id + .rsplit_once('-') + .map_or(self.id.as_str(), |(_, id)| id); + format!( + "https://www.cineplanet.com.pe/compra/{movie_slug}/{}/{session_id}/asientos", + self.venue_id + ) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct Preferences { @@ -141,7 +162,51 @@ pub struct Catalog { #[cfg(test)] mod tests { - use super::{Recommendation, SeatingArrangement}; + use super::{Modality, Recommendation, SeatMap, SeatingArrangement, Showtime}; + use chrono::DateTime; + + fn showtime_with_id(id: &str, venue_id: &str) -> Showtime { + Showtime { + id: id.into(), + movie_id: "movie-1".into(), + movie_title: "La Odisea".into(), + movie_details_url: Some("la-odisea".into()), + session_id: Some("66776".into()), + venue_id: venue_id.into(), + venue_name: "CP Salaverry".into(), + starts_at: DateTime::parse_from_rfc3339("2026-08-16T16:30:00-05:00").unwrap(), + modality: Modality { + projection_format: "2D".into(), + language: "SUBTITULADA".into(), + room_type: "Regular".into(), + }, + seat_map: SeatMap { + rows: 0, + columns: 0, + seats: Vec::new(), + }, + } + } + + #[test] + fn builds_the_purchase_url_from_the_session_half_of_the_id() { + let showtime = showtime_with_id("0000000026-95087", "0000000026"); + + assert_eq!( + showtime.purchase_url("la-odisea"), + "https://www.cineplanet.com.pe/compra/la-odisea/0000000026/95087/asientos" + ); + } + + #[test] + fn falls_back_to_the_whole_id_when_it_carries_no_venue_prefix() { + let showtime = showtime_with_id("95087", "0000000026"); + + assert_eq!( + showtime.purchase_url("la-odisea"), + "https://www.cineplanet.com.pe/compra/la-odisea/0000000026/95087/asientos" + ); + } #[test] fn serializes_each_seating_arrangement() { diff --git a/src/live.rs b/src/live.rs index 26329ef..73fca18 100644 --- a/src/live.rs +++ b/src/live.rs @@ -816,6 +816,12 @@ mod tests { assert!(!catalog.movies.is_empty()); assert!(!catalog.venues.is_empty()); assert!(!catalog.showtimes.is_empty()); + // El slug alimenta el enlace de compra: si Cineplanet dejara de + // mandarlo, el enlace quedaría roto en silencio. + assert!(catalog + .movies + .iter() + .any(|movie| movie.movie_details_url.as_deref().is_some_and(|s| !s.is_empty()))); } #[tokio::test] From c795281ae51d429eb27e491c09ffc89d4c4511f7 Mon Sep 17 00:00:00 2001 From: gersonsebastianx <306426556+gersonsebastianx@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:51:00 -0500 Subject: [PATCH 2/2] feat(ui): show the purchase link on the seat map Closes the loop the seat map opens: after picking a showtime and seeing where the good seats are, the link is right there instead of forcing a manual search back on the website. Rendered only when the slug is known, so a catalog without it degrades to the previous behaviour rather than printing a broken URL. Co-Authored-By: Claude Opus 5 --- src/ui.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/ui.rs b/src/ui.rs index b94ee1a..fa909e1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -862,6 +862,28 @@ fn render_seat_map(frame: &mut Frame<'_>, area: Rect, app: &App) { Span::raw("accesibilidad"), ])); + // La compra sigue ocurriendo en Cineplanet; este enlace ahorra rehacer allá + // la elección de ciudad, película, fecha, sede y hora. + if let Some(slug) = app + .catalog() + .movies + .iter() + .find(|movie| movie.id == showtime.movie_id) + .and_then(|movie| movie.movie_details_url.as_deref()) + .filter(|slug| !slug.is_empty()) + { + lines.push(Line::from("")); + lines.push(Line::from(vec![ + Span::styled("Comprar en Cineplanet: ", Style::default().fg(MUTED)), + Span::styled( + showtime.purchase_url(slug), + Style::default() + .fg(PLANET_BLUE) + .add_modifier(Modifier::UNDERLINED), + ), + ])); + } + frame.render_widget( Paragraph::new(lines) .alignment(Alignment::Center) @@ -1371,6 +1393,30 @@ mod tests { assert_eq!(analysis.showtime.id, "unfavorable"); } + #[test] + fn seat_map_offers_the_purchase_link_for_the_chosen_showtime() { + let mut app = app_with_results(); + app.apply(Action::Down).unwrap(); + app.apply(Action::Confirm).unwrap(); + assert_eq!(app.screen(), Screen::SeatMap); + + let showtime = app.current_result_showtime().unwrap().clone(); + let slug = app + .catalog() + .movies + .iter() + .find(|movie| movie.id == showtime.movie_id) + .and_then(|movie| movie.movie_details_url.clone()) + .unwrap(); + let url = showtime.purchase_url(&slug); + let screen = rendered_lines(&app).join(""); + + assert!( + screen.contains(&url), + "expected the seat map to show {url}, got: {screen}" + ); + } + #[test] fn results_show_no_group_tag_for_a_nonempty_map_with_zero_available_seats() { let mut catalog = demo::catalog();