Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ 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()),
},
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()),
},
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()),
Expand Down
67 changes: 66 additions & 1 deletion src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions src/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
46 changes: 46 additions & 0 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand Down