1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub struct ProjectionVersion(String);
17
18impl ProjectionVersion {
19 pub fn try_new(value: impl Into<String>) -> Result<Self> {
21 trimmed_non_empty(value, Error::EmptyProjectionVersion).map(Self)
22 }
23
24 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28}
29
30pub mod stay {
32 use serde::{Deserialize, Serialize};
33
34 use crate::{analytics, data_quality, source};
35
36 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37 pub struct Id(String);
39
40 impl Id {
41 pub fn try_new(value: impl Into<String>) -> analytics::Result<Self> {
43 analytics::trimmed_non_empty(value, analytics::Error::EmptyStayFactId).map(Self)
44 }
45
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50 }
51
52 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53 pub enum DataQualityStatus {
55 Complete,
57 ManagerReviewRequired,
59 BlockingIssues,
61 }
62
63 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64 pub struct Fact {
66 id: Id,
67 provenance: source::Provenance,
68 reservation_record_id: source::record::Id,
69 customer_record_id: source::record::Id,
70 pet_record_id: source::record::Id,
71 location_record_id: source::record::Id,
72 service_type_record_id: source::record::Id,
73 projection_version: analytics::ProjectionVersion,
74 data_quality_status: DataQualityStatus,
75 data_quality_issues: Vec<data_quality::Issue>,
76 }
77
78 impl Fact {
79 pub fn project_from_source_reservation(
84 id: Id,
85 source_reservation: &source::reservation::Snapshot,
86 projection_version: analytics::ProjectionVersion,
87 ) -> std::result::Result<Self, Vec<data_quality::Issue>> {
88 let issues = source_reservation
89 .data_quality_issues(source_reservation.provenance().pulled_at().clone());
90 if issues.iter().any(data_quality::Issue::workflow_blocking) {
91 return Err(issues);
92 }
93 let data_quality_status = if issues.is_empty() {
94 DataQualityStatus::Complete
95 } else {
96 DataQualityStatus::ManagerReviewRequired
97 };
98
99 let customer_record_id = source_reservation
100 .customer_record_id()
101 .expect("data_quality_issues guards customer presence")
102 .clone();
103 let pet_record_id = source_reservation
104 .pet_record_id()
105 .expect("data_quality_issues guards pet presence")
106 .clone();
107 let location_record_id = source_reservation
108 .location_record_id()
109 .expect("data_quality_issues guards location presence")
110 .clone();
111 let service_type_record_id = source_reservation
112 .service_type_record_id()
113 .expect("data_quality_issues guards service type presence")
114 .clone();
115
116 Ok(Self {
117 id,
118 provenance: source_reservation.provenance().clone(),
119 reservation_record_id: source_reservation.provenance().record_id().clone(),
120 customer_record_id,
121 pet_record_id,
122 location_record_id,
123 service_type_record_id,
124 projection_version,
125 data_quality_status,
126 data_quality_issues: issues,
127 })
128 }
129
130 pub const fn id(&self) -> &Id {
132 &self.id
133 }
134
135 pub const fn source_system(&self) -> source::System {
137 self.provenance.source_system()
138 }
139
140 pub const fn provenance(&self) -> &source::Provenance {
142 &self.provenance
143 }
144
145 pub const fn reservation_record_id(&self) -> &source::record::Id {
147 &self.reservation_record_id
148 }
149
150 pub const fn customer_record_id(&self) -> &source::record::Id {
152 &self.customer_record_id
153 }
154
155 pub const fn pet_record_id(&self) -> &source::record::Id {
157 &self.pet_record_id
158 }
159
160 pub const fn location_record_id(&self) -> &source::record::Id {
162 &self.location_record_id
163 }
164
165 pub const fn service_type_record_id(&self) -> &source::record::Id {
167 &self.service_type_record_id
168 }
169
170 pub const fn projection_version(&self) -> &analytics::ProjectionVersion {
172 &self.projection_version
173 }
174
175 pub const fn data_quality_status(&self) -> DataQualityStatus {
177 self.data_quality_status
178 }
179
180 pub fn data_quality_issues(&self) -> &[data_quality::Issue] {
184 &self.data_quality_issues
185 }
186 }
187}
188
189pub mod service_demand {
191 use serde::{Deserialize, Serialize};
192
193 use crate::{analytics, data_quality, operations, source};
194
195 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
196 pub struct Id(String);
198
199 impl Id {
200 pub fn try_new(value: impl Into<String>) -> analytics::Result<Self> {
202 analytics::trimmed_non_empty(value, analytics::Error::EmptyServiceDemandFactId)
203 .map(Self)
204 }
205
206 pub fn as_str(&self) -> &str {
208 &self.0
209 }
210 }
211
212 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
213 pub struct DemandUnits(u32);
215
216 impl DemandUnits {
217 pub const fn try_new(value: u32) -> analytics::Result<Self> {
219 if value == 0 {
220 return Err(analytics::Error::EmptyDemandUnits);
221 }
222 Ok(Self(value))
223 }
224
225 pub const fn get(self) -> u32 {
227 self.0
228 }
229 }
230
231 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232 pub struct Fact {
234 id: Id,
235 operating_day: operations::operating_day::Key,
236 demand_units: DemandUnits,
237 source_record_refs: Vec<source::RecordRef>,
238 projection_version: analytics::ProjectionVersion,
239 data_quality_status: DataQualityStatus,
240 data_quality_issues: Vec<data_quality::Issue>,
241 }
242
243 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244 pub enum DataQualityStatus {
246 Complete,
248 ManagerReviewRequired,
250 }
251
252 impl Fact {
253 pub fn try_new(
255 id: Id,
256 operating_day: operations::operating_day::Key,
257 demand_units: DemandUnits,
258 source_record_refs: Vec<source::RecordRef>,
259 projection_version: analytics::ProjectionVersion,
260 data_quality_issues: Vec<data_quality::Issue>,
261 ) -> Result<Self> {
262 if source_record_refs.is_empty() {
263 return Err(Error::MissingSourceEvidence);
264 }
265 let data_quality_status = if data_quality_issues.is_empty() {
266 DataQualityStatus::Complete
267 } else {
268 DataQualityStatus::ManagerReviewRequired
269 };
270
271 Ok(Self {
272 id,
273 operating_day,
274 demand_units,
275 source_record_refs,
276 projection_version,
277 data_quality_status,
278 data_quality_issues,
279 })
280 }
281
282 pub const fn id(&self) -> &Id {
284 &self.id
285 }
286
287 pub const fn operating_day(&self) -> &operations::operating_day::Key {
289 &self.operating_day
290 }
291
292 pub const fn demand_units(&self) -> DemandUnits {
294 self.demand_units
295 }
296
297 pub fn source_record_refs(&self) -> &[source::RecordRef] {
299 &self.source_record_refs
300 }
301
302 pub const fn projection_version(&self) -> &analytics::ProjectionVersion {
304 &self.projection_version
305 }
306
307 pub const fn data_quality_status(&self) -> DataQualityStatus {
309 self.data_quality_status
310 }
311
312 pub fn data_quality_issues(&self) -> &[data_quality::Issue] {
314 &self.data_quality_issues
315 }
316 }
317
318 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
319 pub enum Error {
321 #[error("service demand facts require source evidence")]
322 MissingSourceEvidence,
324 }
325
326 pub type Result<T> = std::result::Result<T, Error>;
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
331pub enum Error {
333 #[error("stay fact id must not be empty")]
334 EmptyStayFactId,
336 #[error("service demand fact id must not be empty")]
337 EmptyServiceDemandFactId,
339 #[error("service demand units must be greater than zero")]
340 EmptyDemandUnits,
342 #[error("projection version must not be empty")]
343 EmptyProjectionVersion,
345}
346
347pub type Result<T> = std::result::Result<T, Error>;
349
350fn trimmed_non_empty(value: impl Into<String>, empty_error: Error) -> Result<String> {
351 let value = value.into().trim().to_string();
352 if value.is_empty() {
353 return Err(empty_error);
354 }
355 Ok(value)
356}