1use bon::Builder;
26use serde::{Deserialize, Serialize};
27
28use crate::ocpi_enum;
29use crate::types::validate_fields;
30use crate::types::{
31 CiString, ContractId, CountryCode, DateTime, Extensions, Number, OcpiText, PartyId, PartyRef, Url,
32 Validate, Validator, ViolationCode,
33};
34
35use super::locations::{ConnectorFormat, ConnectorType, EvsePosition, PowerType, VehicleType};
36use super::tokens::TokenType;
37use super::types::Role;
38
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[builder(on(_, into))]
45pub struct Booking {
46 pub id: CiString<36>,
48 pub country_code: CountryCode,
50 pub party_id: PartyId,
52 pub request_id: CiString<36>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub booking_option: Option<BookingOption>,
59 pub location_id: CiString<36>,
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 #[builder(default)]
64 pub booking_tokens: Vec<BookingToken>,
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 #[builder(default)]
68 pub tariff_ids: Vec<CiString<36>>,
69 pub period: Timeslot,
71 pub reservation_status: ReservationStatus,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub canceled: Option<Cancellation>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 #[builder(default)]
79 pub access_information: Vec<AccessInformation>,
80 pub authorization_reference: CiString<36>,
82 pub booking_terms: BookingTerms,
84 pub booking_requests: Vec<BookingRequestStatus>,
86 pub last_updated: DateTime,
88 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
90 #[builder(default)]
91 pub extensions: Extensions,
92}
93
94impl Booking {
95 #[must_use]
97 pub fn owner_party(&self) -> PartyRef {
98 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
99 }
100
101 #[must_use]
103 pub fn is_final(&self) -> bool {
104 self.reservation_status.is_terminal()
105 }
106}
107
108impl Validate for Booking {
109 fn validate_in(&self, v: &mut Validator) {
110 validate_fields!(
111 self,
112 v,
113 id,
114 country_code,
115 party_id,
116 request_id,
117 booking_option,
118 location_id,
119 booking_tokens,
120 tariff_ids,
121 period,
122 reservation_status,
123 canceled,
124 access_information,
125 authorization_reference,
126 booking_terms,
127 booking_requests,
128 last_updated,
129 );
130 if self.booking_requests.is_empty() {
131 v.report_at(
132 "booking_requests",
133 ViolationCode::EmptyRequiredList,
134 "a Booking has cardinality `+` booking_requests: the request that created it is \
135 always one of them",
136 );
137 }
138 match (self.reservation_status, self.canceled.is_some()) {
140 (ReservationStatus::Canceled, false) => v.report_at(
141 "canceled",
142 ViolationCode::MissingConditional,
143 "a CANCELED booking should say why and by whom",
144 ),
145 (status, true) if status != ReservationStatus::Canceled => v.report_at(
146 "reservation_status",
147 ViolationCode::Inconsistent,
148 format!("a cancellation is recorded, but the status is {status}"),
149 ),
150 _ => {}
151 }
152 }
153}
154
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
162#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
163#[builder(on(_, into))]
164pub struct BookingLocation {
165 pub country_code: CountryCode,
167 pub party_id: PartyId,
169 pub id: CiString<36>,
171 pub location_id: CiString<36>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub booking_option: Option<BookingOption>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub policy: Option<Policy>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 #[builder(default)]
182 pub tariff_ids: Vec<CiString<36>>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub booking_terms: Option<BookingTerms>,
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
188 #[builder(default)]
189 pub calendars: Vec<Calendar>,
190 pub last_updated: DateTime,
192 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
194 #[builder(default)]
195 pub extensions: Extensions,
196}
197
198impl BookingLocation {
199 #[must_use]
201 pub fn owner_party(&self) -> PartyRef {
202 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
203 }
204}
205
206impl Validate for BookingLocation {
207 fn validate_in(&self, v: &mut Validator) {
208 validate_fields!(
209 self,
210 v,
211 country_code,
212 party_id,
213 id,
214 location_id,
215 booking_option,
216 policy,
217 tariff_ids,
218 booking_terms,
219 calendars,
220 last_updated,
221 );
222 let names_an_evse = self.booking_option.as_ref().is_some_and(|o| o.evse_uid.is_some());
225 if self.booking_option.is_none() && !names_an_evse {
226 v.report_at(
227 "booking_option",
228 ViolationCode::MissingConditional,
229 "either `booking_option` or an EVSE must be given; one of them is mandatory",
230 );
231 }
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
239#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
240#[builder(on(_, into))]
241pub struct Calendar {
242 pub id: CiString<36>,
244 pub begin_from: DateTime,
246 pub end_before: DateTime,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub timeslot_increment: Option<u32>,
251 pub available_timeslots: Vec<Timeslot>,
253 pub last_updated: DateTime,
255 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
257 #[builder(default)]
258 pub extensions: Extensions,
259}
260
261impl Calendar {
262 #[must_use]
264 pub fn can_accommodate(&self, slot: &Timeslot) -> bool {
265 self.available_timeslots.iter().any(|available| {
266 slot.start_date_time >= available.start_date_time && slot.end_date_time <= available.end_date_time
267 })
268 }
269}
270
271impl Validate for Calendar {
272 fn validate_in(&self, v: &mut Validator) {
273 validate_fields!(self, v, id, begin_from, end_before, available_timeslots, last_updated);
274 if self.end_before <= self.begin_from {
275 v.report_at(
276 "end_before",
277 ViolationCode::Inconsistent,
278 "a calendar must cover a non-empty period",
279 );
280 }
281 if self.available_timeslots.is_empty() {
282 v.report_at(
283 "available_timeslots",
284 ViolationCode::EmptyRequiredList,
285 "a Calendar has cardinality `+` available_timeslots",
286 );
287 }
288 }
289}
290
291#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
295#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
296#[builder(on(_, into))]
297pub struct Timeslot {
298 pub start_date_time: DateTime,
300 pub end_date_time: DateTime,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub min_power: Option<Number>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub max_power: Option<Number>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub green_energy_support: Option<bool>,
311 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
313 #[builder(default)]
314 pub extensions: Extensions,
315}
316
317impl Timeslot {
318 #[must_use]
320 pub fn duration_minutes(&self) -> Option<i64> {
321 let seconds = self.end_date_time.unix_timestamp() - self.start_date_time.unix_timestamp();
322 (seconds > 0).then_some(seconds / 60)
323 }
324}
325
326impl Validate for Timeslot {
327 fn validate_in(&self, v: &mut Validator) {
328 validate_fields!(self, v, start_date_time, end_date_time, min_power, max_power);
329 if self.end_date_time <= self.start_date_time {
330 v.report_at(
331 "end_date_time",
332 ViolationCode::Inconsistent,
333 "a timeslot must cover a non-empty period",
334 );
335 }
336 if let (Some(min), Some(max)) = (self.min_power, self.max_power)
337 && max < min
338 {
339 v.report_at(
340 "max_power",
341 ViolationCode::Inconsistent,
342 "the maximum power cannot be below the guaranteed minimum",
343 );
344 }
345 }
346}
347
348#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Builder)]
352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
353#[builder(on(_, into))]
354pub struct BookingOption {
355 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub evse_uid: Option<CiString<36>>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub connector_id: Option<CiString<36>>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub parking_id: Option<CiString<36>>,
364 #[serde(default, skip_serializing_if = "Vec::is_empty")]
366 #[builder(default)]
367 pub evse_position: Vec<EvsePosition>,
368 #[serde(default, skip_serializing_if = "Vec::is_empty")]
370 #[builder(default)]
371 pub vehicle_types: Vec<VehicleType>,
372 #[serde(default, skip_serializing_if = "Vec::is_empty")]
374 #[builder(default)]
375 pub connector_format: Vec<ConnectorFormat>,
376 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 #[builder(default)]
379 pub connector_types: Vec<ConnectorType>,
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
382 #[builder(default)]
383 pub power_types: Vec<PowerType>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub max_vehicle_weight: Option<Number>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub max_vehicle_height: Option<Number>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub max_vehicle_length: Option<Number>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub max_vehicle_width: Option<Number>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub min_parking_space_length: Option<Number>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub min_parking_space_width: Option<Number>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub dangerous_goods_allowed: Option<bool>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub drive_through: Option<bool>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub refrigeration_outlet: Option<bool>,
411 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
413 #[builder(default)]
414 pub extensions: Extensions,
415}
416
417impl Validate for BookingOption {
418 fn validate_in(&self, v: &mut Validator) {
419 validate_fields!(
420 self,
421 v,
422 evse_uid,
423 connector_id,
424 parking_id,
425 evse_position,
426 vehicle_types,
427 connector_format,
428 connector_types,
429 power_types,
430 max_vehicle_weight,
431 max_vehicle_height,
432 max_vehicle_length,
433 max_vehicle_width,
434 min_parking_space_length,
435 min_parking_space_width,
436 );
437 }
438}
439
440#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
444#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
445#[builder(on(_, into))]
446pub struct BookingRequestStatus {
447 pub request_status: ReservationRequestStatus,
449 pub booking_request: BookingRequest,
451 pub request_received: DateTime,
453 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
455 #[builder(default)]
456 pub extensions: Extensions,
457}
458
459impl Validate for BookingRequestStatus {
460 fn validate_in(&self, v: &mut Validator) {
461 validate_fields!(self, v, request_status, booking_request, request_received);
462 }
463}
464
465#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
469#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
470#[builder(on(_, into))]
471pub struct BookingRequest {
472 pub country_code: CountryCode,
474 pub party_id: PartyId,
476 pub request_id: CiString<36>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub booking_option: Option<BookingOption>,
481 pub location_id: CiString<36>,
483 pub booking_location_id: CiString<36>,
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
487 #[builder(default)]
488 pub tokens: Vec<BookingToken>,
489 #[serde(default, skip_serializing_if = "Vec::is_empty")]
491 #[builder(default)]
492 pub access_information: Vec<AccessInformation>,
493 pub period: Period,
495 pub authorization_reference: CiString<36>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub power_required: Option<u32>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub canceled: Option<Cancellation>,
505 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
507 #[builder(default)]
508 pub extensions: Extensions,
509}
510
511impl BookingRequest {
512 #[must_use]
514 pub fn requester(&self) -> PartyRef {
515 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
516 }
517}
518
519impl Validate for BookingRequest {
520 fn validate_in(&self, v: &mut Validator) {
521 validate_fields!(
522 self,
523 v,
524 country_code,
525 party_id,
526 request_id,
527 booking_option,
528 location_id,
529 booking_location_id,
530 tokens,
531 access_information,
532 period,
533 authorization_reference,
534 canceled,
535 );
536 }
537}
538
539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
543#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
544pub struct Period {
545 pub start_date_time: DateTime,
547 pub end_date_time: DateTime,
549 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
551 pub extensions: Extensions,
552}
553
554impl Validate for Period {
555 fn validate_in(&self, v: &mut Validator) {
556 validate_fields!(self, v, start_date_time, end_date_time);
557 if self.end_date_time <= self.start_date_time {
558 v.report_at(
559 "end_date_time",
560 ViolationCode::Inconsistent,
561 "a period must cover a non-empty span of time",
562 );
563 }
564 }
565}
566
567#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
571#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
572#[builder(on(_, into))]
573pub struct BookingToken {
574 pub country_code: CountryCode,
576 pub party_id: PartyId,
578 pub uid: CiString<36>,
580 #[serde(rename = "type")]
582 pub token_type: TokenType,
583 pub contract_id: ContractId,
585 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
587 #[builder(default)]
588 pub extensions: Extensions,
589}
590
591impl Validate for BookingToken {
592 fn validate_in(&self, v: &mut Validator) {
593 validate_fields!(self, v, country_code, party_id, uid, token_type as "type", contract_id);
594 }
595}
596
597#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
602#[builder(on(_, into))]
603pub struct BookingTerms {
604 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub rfid_auth_required: Option<bool>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub token_groups_supported: Option<bool>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub remote_auth_supported: Option<bool>,
613 pub supported_access_methods: Vec<AccessMethod>,
615 pub change_until_minutes: Number,
617 pub cancel_until_minutes: Number,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub change_not_allowed: Option<bool>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub early_start_allowed: Option<bool>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub early_start_time: Option<Number>,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub noshow_timeout: Option<Number>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub noshow_fee: Option<bool>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub late_stop_allowed: Option<bool>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub late_stop_time: Option<Number>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub overlapping_bookings_allowed: Option<bool>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub min_booking_duration: Option<Number>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub max_booking_duration: Option<Number>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub booking_terms: Option<Url>,
656 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
658 #[builder(default)]
659 pub extensions: Extensions,
660}
661
662impl BookingTerms {
663 #[must_use]
665 pub fn may_cancel_at(&self, now: DateTime, start: DateTime) -> bool {
666 minutes_before(now, start) >= self.cancel_until_minutes
667 }
668
669 #[must_use]
671 pub fn may_change_at(&self, now: DateTime, start: DateTime) -> bool {
672 if self.change_not_allowed.unwrap_or(false) {
673 return false;
674 }
675 minutes_before(now, start) >= self.change_until_minutes
676 }
677}
678
679fn minutes_before(now: DateTime, start: DateTime) -> Number {
680 let seconds = start.unix_timestamp() - now.unix_timestamp();
681 Number::from(seconds) / Number::from(60u32)
682}
683
684impl Validate for BookingTerms {
685 fn validate_in(&self, v: &mut Validator) {
686 validate_fields!(
687 self,
688 v,
689 supported_access_methods,
690 change_until_minutes,
691 cancel_until_minutes,
692 early_start_time,
693 noshow_timeout,
694 late_stop_time,
695 min_booking_duration,
696 max_booking_duration,
697 booking_terms,
698 );
699 if self.supported_access_methods.is_empty() {
700 v.report_at(
701 "supported_access_methods",
702 ViolationCode::EmptyRequiredList,
703 "BookingTerms has cardinality `+` supported_access_methods: a driver needs to \
704 know how to get in",
705 );
706 }
707 if let (Some(min), Some(max)) = (self.min_booking_duration, self.max_booking_duration)
708 && max < min
709 {
710 v.report_at(
711 "max_booking_duration",
712 ViolationCode::Inconsistent,
713 "the maximum booking duration cannot be below the minimum",
714 );
715 }
716 }
717}
718
719#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
723#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
724pub struct AccessInformation {
725 pub method: AccessMethod,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub value: Option<OcpiText>,
730 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
732 pub extensions: Extensions,
733}
734
735impl Validate for AccessInformation {
736 fn validate_in(&self, v: &mut Validator) {
737 validate_fields!(self, v, method, value);
738 if self.value.is_none()
740 && matches!(
741 self.method,
742 AccessMethod::Token | AccessMethod::LicensePlate | AccessMethod::AccessCode
743 )
744 {
745 v.report_at(
746 "value",
747 ViolationCode::MissingConditional,
748 format!("{} needs the value the driver is to present", self.method),
749 );
750 }
751 }
752}
753
754#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
758#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
759pub struct Cancellation {
760 pub cancellation_reason: CanceledReason,
762 pub who_canceled: Role,
769 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
771 pub extensions: Extensions,
772}
773
774impl Validate for Cancellation {
775 fn validate_in(&self, v: &mut Validator) {
776 validate_fields!(self, v, cancellation_reason, who_canceled);
777 }
778}
779
780#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
784#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
785pub struct Policy {
786 pub reservation_required: bool,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub ad_hoc: Option<Number>,
791 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
793 pub extensions: Extensions,
794}
795
796impl Validate for Policy {
797 fn validate_in(&self, v: &mut Validator) {
798 validate_fields!(self, v, ad_hoc);
799 if self.reservation_required && self.ad_hoc.is_some_and(|n| !n.is_zero()) {
800 v.report_at(
801 "ad_hoc",
802 ViolationCode::Inconsistent,
803 "a Location that requires a reservation cannot offer ad-hoc charging",
804 );
805 }
806 }
807}
808
809ocpi_enum! {
810 pub enum AccessMethod {
814 Open = "OPEN",
816 Token = "TOKEN",
818 LicensePlate = "LICENSE_PLATE",
820 AccessCode = "ACCESS_CODE",
822 Intercom = "INTERCOM",
824 ParkingTicket = "PARKING_TICKET",
826 }
827}
828
829ocpi_enum! {
830 pub enum CanceledReason {
834 PowerOutage = "POWER_OUTAGE",
836 BrokenCharger = "BROKEN_CHARGER",
838 Full = "FULL",
840 Blocked = "BLOCKED",
842 Traffic = "TRAFFIC",
844 BrokenVehicle = "BROKEN_VEHICLE",
846 NoCanceled = "NO_CANCELED",
848 Unknown = "UNKNOWN",
850 }
851}
852
853ocpi_enum! {
854 pub enum ReservationRequestStatus {
858 Pending = "PENDING",
860 Accepted = "ACCEPTED",
862 Declined = "DECLINED",
864 Failed = "FAILED",
866 }
867}
868
869ocpi_enum! {
870 pub enum ReservationStatus {
874 Pending = "PENDING",
876 Reserved = "RESERVED",
878 Canceled = "CANCELED",
880 Failed = "FAILED",
882 NoShow = "NO_SHOW",
884 Fulfilled = "FULFILLED",
886 Rejected = "REJECTED",
888 Unknown = "UNKNOWN",
890 }
891}
892
893impl ReservationStatus {
894 #[must_use]
896 pub const fn is_terminal(self) -> bool {
897 matches!(self, Self::Canceled | Self::Failed | Self::NoShow | Self::Fulfilled | Self::Rejected)
898 }
899
900 #[must_use]
907 pub const fn can_transition_to(self, next: Self) -> bool {
908 match self {
909 Self::Pending => matches!(
910 next,
911 Self::Reserved | Self::Rejected | Self::Failed | Self::Canceled | Self::Unknown
912 ),
913 Self::Reserved => {
914 matches!(next, Self::Fulfilled | Self::Canceled | Self::NoShow | Self::Unknown)
915 }
916 Self::Unknown => true,
917 _ => false,
918 }
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 fn dt(s: &str) -> DateTime {
927 s.parse().unwrap()
928 }
929
930 #[test]
931 fn the_lifecycle_is_the_one_the_spec_describes() {
932 use ReservationStatus::{Canceled, Fulfilled, NoShow, Pending, Rejected, Reserved};
933 assert!(Pending.can_transition_to(Reserved));
934 assert!(Pending.can_transition_to(Rejected));
935 assert!(Reserved.can_transition_to(Fulfilled));
936 assert!(Reserved.can_transition_to(Canceled));
937 assert!(Reserved.can_transition_to(NoShow));
938 assert!(!Pending.can_transition_to(Fulfilled));
940 assert!(!Fulfilled.can_transition_to(Canceled));
942 assert!(Fulfilled.is_terminal() && NoShow.is_terminal());
943 assert!(!Pending.is_terminal() && !Reserved.is_terminal());
944 }
945
946 #[test]
947 fn a_timeslot_must_be_a_forward_interval_with_coherent_power() {
948 let slot = Timeslot::builder()
949 .start_date_time(dt("2024-06-01T10:00:00Z"))
950 .end_date_time(dt("2024-06-01T12:00:00Z"))
951 .min_power(Number::from(11_000u32))
952 .max_power(Number::from(22_000u32))
953 .build();
954 assert!(slot.validate().is_ok());
955 assert_eq!(slot.duration_minutes(), Some(120));
956
957 let backwards = Timeslot { end_date_time: dt("2024-06-01T09:00:00Z"), ..slot.clone() };
958 assert!(backwards.validate().is_err());
959 assert_eq!(backwards.duration_minutes(), None);
960
961 let impossible = Timeslot { max_power: Some(Number::from(1000u32)), ..slot };
962 assert!(impossible.validate().is_err());
963 }
964
965 #[test]
966 fn a_calendar_accommodates_a_slot_that_fits_inside_an_available_one() {
967 let calendar = Calendar::builder()
968 .id("CAL1")
969 .begin_from(dt("2024-06-01T00:00:00Z"))
970 .end_before(dt("2024-06-02T00:00:00Z"))
971 .available_timeslots(vec![
972 Timeslot::builder()
973 .start_date_time(dt("2024-06-01T08:00:00Z"))
974 .end_date_time(dt("2024-06-01T18:00:00Z"))
975 .build(),
976 ])
977 .last_updated(dt("2024-05-01T00:00:00Z"))
978 .build();
979 assert!(calendar.validate().is_ok());
980
981 let fits = Timeslot::builder()
982 .start_date_time(dt("2024-06-01T10:00:00Z"))
983 .end_date_time(dt("2024-06-01T12:00:00Z"))
984 .build();
985 assert!(calendar.can_accommodate(&fits));
986
987 let overruns = Timeslot::builder()
988 .start_date_time(dt("2024-06-01T17:00:00Z"))
989 .end_date_time(dt("2024-06-01T19:00:00Z"))
990 .build();
991 assert!(!calendar.can_accommodate(&overruns));
992 }
993
994 #[test]
995 fn the_change_and_cancel_windows_are_computed_from_the_terms() {
996 let terms = BookingTerms::builder()
997 .supported_access_methods(vec![AccessMethod::Open])
998 .change_until_minutes(Number::from(60u32))
999 .cancel_until_minutes(Number::from(30u32))
1000 .build();
1001 let start = dt("2024-06-01T12:00:00Z");
1002 assert!(terms.may_change_at(dt("2024-06-01T10:00:00Z"), start));
1003 assert!(!terms.may_change_at(dt("2024-06-01T11:30:00Z"), start), "inside the 60 minutes");
1004 assert!(terms.may_cancel_at(dt("2024-06-01T11:30:00Z"), start));
1005 assert!(!terms.may_cancel_at(dt("2024-06-01T11:45:00Z"), start));
1006
1007 let frozen = BookingTerms { change_not_allowed: Some(true), ..terms };
1008 assert!(!frozen.may_change_at(dt("2024-06-01T00:00:00Z"), start));
1009 }
1010
1011 #[test]
1012 fn an_access_method_that_needs_a_value_must_have_one() {
1013 let bare = AccessInformation {
1014 method: AccessMethod::AccessCode,
1015 value: None,
1016 extensions: Extensions::new(),
1017 };
1018 assert_eq!(bare.validate().unwrap_err().as_slice()[0].pointer, "/value");
1019
1020 let open =
1021 AccessInformation { method: AccessMethod::Open, value: None, extensions: Extensions::new() };
1022 assert!(open.validate().is_ok(), "OPEN needs nothing");
1023 }
1024
1025 #[test]
1026 fn a_location_that_requires_a_reservation_offers_no_ad_hoc_charging() {
1027 let contradiction = Policy {
1028 reservation_required: true,
1029 ad_hoc: Some(Number::from(2u32)),
1030 extensions: Extensions::new(),
1031 };
1032 assert!(contradiction.validate().is_err());
1033 let coherent = Policy { ad_hoc: Some(Number::ZERO), ..contradiction };
1034 assert!(coherent.validate().is_ok());
1035 }
1036}