@@ -178,8 +178,8 @@ pub async fn run_reminder_loop(pool: SqlitePool, secret_key: [u8; 32]) {
178178 ) in & due
179179 {
180180 let date = start_at. get ( ..10 ) . unwrap_or ( start_at) . to_string ( ) ;
181- let start_time = format_time_from_dt ( start_at) ;
182- let end_time = format_time_from_dt ( end_at) ;
181+ let start_time = extract_time_24h ( start_at) ;
182+ let end_time = extract_time_24h ( end_at) ;
183183
184184 let location = location_value. as_ref ( ) . filter ( |v| !v. is_empty ( ) ) . cloned ( ) ;
185185
@@ -299,6 +299,19 @@ fn format_time_from_dt(dt_str: &str) -> String {
299299 }
300300}
301301
302+ /// Extract HH:MM (24-hour) from a booking datetime for ICS generation.
303+ /// format_time_from_dt returns 12-hour display format ("2:00 PM") which
304+ /// convert_to_utc cannot parse. This function returns "14:00" format.
305+ fn extract_time_24h ( dt_str : & str ) -> String {
306+ if let Some ( ndt) = parse_booking_datetime ( dt_str) {
307+ ndt. time ( ) . format ( "%H:%M" ) . to_string ( )
308+ } else if dt_str. len ( ) >= 16 {
309+ dt_str[ 11 ..16 ] . to_string ( )
310+ } else {
311+ "00:00" . to_string ( )
312+ }
313+ }
314+
302315/// Parse availability windows from the form.
303316/// Supports new `avail_windows` format ("09:00-12:00,13:00-17:00") with fallback to
304317/// legacy single `avail_start`/`avail_end` pair. Returns at least one window.
@@ -1293,8 +1306,8 @@ async fn cancel_booking(
12931306 {
12941307 // Extract date and times from start_at/end_at
12951308 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
1296- let start_time = format_time_from_dt ( & start_at) ;
1297- let end_time = format_time_from_dt ( & end_at) ;
1309+ let start_time = extract_time_24h ( & start_at) ;
1310+ let end_time = extract_time_24h ( & end_at) ;
12981311
12991312 let reason = form. reason . filter ( |r| !r. trim ( ) . is_empty ( ) ) ;
13001313
@@ -1377,8 +1390,8 @@ async fn confirm_booking(
13771390 tracing:: info!( booking_id = %bid, "booking confirmed by host" ) ;
13781391
13791392 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
1380- let start_time = format_time_from_dt ( & start_at) ;
1381- let end_time = format_time_from_dt ( & end_at) ;
1393+ let start_time = extract_time_24h ( & start_at) ;
1394+ let end_time = extract_time_24h ( & end_at) ;
13821395
13831396 let details = crate :: email:: BookingDetails {
13841397 event_title,
@@ -6844,8 +6857,8 @@ async fn approve_booking_by_token(
68446857
68456858 let date_label = format_date_label ( & start_at) ;
68466859 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
6847- let start_time = format_time_from_dt ( & start_at) ;
6848- let end_time = format_time_from_dt ( & end_at) ;
6860+ let start_time = extract_time_24h ( & start_at) ;
6861+ let end_time = extract_time_24h ( & end_at) ;
68496862
68506863 // Get host email for BookingDetails
68516864 let host_email: String =
@@ -6945,8 +6958,8 @@ async fn decline_booking_form(
69456958
69466959 let date_label = format_date_label ( & start_at) ;
69476960 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
6948- let start_time = format_time_from_dt ( & start_at) ;
6949- let end_time = format_time_from_dt ( & end_at) ;
6961+ let start_time = extract_time_24h ( & start_at) ;
6962+ let end_time = extract_time_24h ( & end_at) ;
69506963
69516964 let tmpl = match state. templates . get_template ( "booking_decline_form.html" ) {
69526965 Ok ( t) => t,
@@ -7034,8 +7047,8 @@ async fn decline_booking_by_token(
70347047
70357048 let date_label = format_date_label ( & start_at) ;
70367049 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
7037- let start_time = format_time_from_dt ( & start_at) ;
7038- let end_time = format_time_from_dt ( & end_at) ;
7050+ let start_time = extract_time_24h ( & start_at) ;
7051+ let end_time = extract_time_24h ( & end_at) ;
70397052
70407053 let reason = form. reason . filter ( |r| !r. trim ( ) . is_empty ( ) ) ;
70417054
@@ -7137,8 +7150,8 @@ async fn guest_cancel_form(
71377150
71387151 let date_label = format_date_label ( & start_at) ;
71397152 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
7140- let start_time = format_time_from_dt ( & start_at) ;
7141- let end_time = format_time_from_dt ( & end_at) ;
7153+ let start_time = extract_time_24h ( & start_at) ;
7154+ let end_time = extract_time_24h ( & end_at) ;
71427155
71437156 let tmpl = match state. templates . get_template ( "booking_cancel_form.html" ) {
71447157 Ok ( t) => t,
@@ -7234,8 +7247,8 @@ async fn guest_cancel_booking(
72347247
72357248 let date_label = format_date_label ( & start_at) ;
72367249 let date = start_at. get ( ..10 ) . unwrap_or ( & start_at) . to_string ( ) ;
7237- let start_time = format_time_from_dt ( & start_at) ;
7238- let end_time = format_time_from_dt ( & end_at) ;
7250+ let start_time = extract_time_24h ( & start_at) ;
7251+ let end_time = extract_time_24h ( & end_at) ;
72397252
72407253 let reason = form. reason . filter ( |r| !r. trim ( ) . is_empty ( ) ) ;
72417254
@@ -8212,6 +8225,21 @@ mod tests {
82128225 assert_eq ! ( format_time_from_dt( "" ) , "00:00" ) ;
82138226 }
82148227
8228+ // --- extract_time_24h tests ---
8229+
8230+ #[ test]
8231+ fn extract_time_24h_returns_24h_format ( ) {
8232+ assert_eq ! ( extract_time_24h( "2026-03-15T14:30:00" ) , "14:30" ) ;
8233+ assert_eq ! ( extract_time_24h( "2026-03-15 09:05:00" ) , "09:05" ) ;
8234+ assert_eq ! ( extract_time_24h( "2026-03-15T00:00:00" ) , "00:00" ) ;
8235+ }
8236+
8237+ #[ test]
8238+ fn extract_time_24h_short_string ( ) {
8239+ assert_eq ! ( extract_time_24h( "short" ) , "00:00" ) ;
8240+ assert_eq ! ( extract_time_24h( "" ) , "00:00" ) ;
8241+ }
8242+
82158243 // --- parse_datetime edge cases ---
82168244
82178245 #[ test]
0 commit comments