phytrace_sdk/models/domains/
payload.rs

1//! Payload Domain - Load status, compartments, and items.
2
3use serde::{Deserialize, Serialize};
4use validator::Validate;
5
6use super::common::ObjectRef;
7use crate::models::enums::LoadStatus;
8
9/// Payload domain containing load and item information.
10#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
11pub struct PayloadDomain {
12    /// Load status
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub load_status: Option<LoadStatus>,
15
16    /// Total weight in kg
17    #[serde(skip_serializing_if = "Option::is_none")]
18    #[validate(range(min = 0.0))]
19    pub total_weight_kg: Option<f64>,
20
21    /// Maximum weight capacity in kg
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub max_weight_kg: Option<f64>,
24
25    /// Weight utilization percentage
26    #[serde(skip_serializing_if = "Option::is_none")]
27    #[validate(range(min = 0.0, max = 100.0))]
28    pub weight_utilization_pct: Option<f64>,
29
30    /// Items being carried
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub items: Option<Vec<PayloadItem>>,
33
34    /// Item count
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub item_count: Option<u32>,
37
38    /// Compartments (for multi-compartment robots)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub compartments: Option<Vec<Compartment>>,
41
42    /// Whether load is secured
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub is_secured: Option<bool>,
45
46    /// Center of mass offset (if tracked)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub center_of_mass_offset_m: Option<[f64; 3]>,
49}
50
51/// Individual payload item.
52#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
53pub struct PayloadItem {
54    /// Object reference
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub object_ref: Option<ObjectRef>,
57
58    /// Item ID
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub item_id: Option<String>,
61
62    /// Item type
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub item_type: Option<String>,
65
66    /// Weight in kg
67    #[serde(skip_serializing_if = "Option::is_none")]
68    #[validate(range(min = 0.0))]
69    pub weight_kg: Option<f64>,
70
71    /// Quantity
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub quantity: Option<u32>,
74
75    /// Compartment ID (if in compartment)
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub compartment_id: Option<String>,
78
79    /// Pickup location
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub pickup_location: Option<String>,
82
83    /// Destination location
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub destination: Option<String>,
86
87    /// Whether item is fragile
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub is_fragile: Option<bool>,
90
91    /// Whether item requires special handling
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub special_handling: Option<String>,
94}
95
96/// Compartment/bin information.
97#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
98pub struct Compartment {
99    /// Compartment ID
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub compartment_id: Option<String>,
102
103    /// Compartment name
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub name: Option<String>,
106
107    /// Load status
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub status: Option<LoadStatus>,
110
111    /// Current weight in kg
112    #[serde(skip_serializing_if = "Option::is_none")]
113    #[validate(range(min = 0.0))]
114    pub weight_kg: Option<f64>,
115
116    /// Max capacity in kg
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub max_weight_kg: Option<f64>,
119
120    /// Items in compartment
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub item_count: Option<u32>,
123
124    /// Whether compartment is open
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub is_open: Option<bool>,
127
128    /// Whether compartment is locked
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub is_locked: Option<bool>,
131
132    /// Temperature (for climate-controlled)
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub temperature_c: Option<f64>,
135}
136
137impl PayloadDomain {
138    /// Create empty payload.
139    pub fn empty() -> Self {
140        Self {
141            load_status: Some(LoadStatus::Empty),
142            total_weight_kg: Some(0.0),
143            item_count: Some(0),
144            ..Default::default()
145        }
146    }
147
148    /// Create payload with weight.
149    pub fn with_weight(weight_kg: f64, max_weight_kg: f64) -> Self {
150        let status = if weight_kg == 0.0 {
151            LoadStatus::Empty
152        } else if weight_kg >= max_weight_kg * 0.8 {
153            LoadStatus::FullLoad
154        } else {
155            LoadStatus::PartialLoad
156        };
157
158        Self {
159            load_status: Some(status),
160            total_weight_kg: Some(weight_kg),
161            max_weight_kg: Some(max_weight_kg),
162            weight_utilization_pct: Some((weight_kg / max_weight_kg) * 100.0),
163            ..Default::default()
164        }
165    }
166
167    /// Add an item.
168    pub fn with_item(mut self, item: PayloadItem) -> Self {
169        let items = self.items.get_or_insert_with(Vec::new);
170        items.push(item);
171        self.item_count = Some(items.len() as u32);
172        self
173    }
174
175    /// Add a compartment.
176    pub fn with_compartment(mut self, compartment: Compartment) -> Self {
177        let compartments = self.compartments.get_or_insert_with(Vec::new);
178        compartments.push(compartment);
179        self
180    }
181}
182
183impl PayloadItem {
184    /// Create a new item.
185    pub fn new(item_id: impl Into<String>, item_type: impl Into<String>) -> Self {
186        Self {
187            item_id: Some(item_id.into()),
188            item_type: Some(item_type.into()),
189            quantity: Some(1),
190            ..Default::default()
191        }
192    }
193
194    /// Set weight.
195    pub fn with_weight(mut self, weight_kg: f64) -> Self {
196        self.weight_kg = Some(weight_kg);
197        self
198    }
199
200    /// Set destination.
201    pub fn with_destination(mut self, dest: impl Into<String>) -> Self {
202        self.destination = Some(dest.into());
203        self
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_payload_empty() {
213        let payload = PayloadDomain::empty();
214        assert_eq!(payload.load_status, Some(LoadStatus::Empty));
215    }
216
217    #[test]
218    fn test_payload_with_weight() {
219        let payload = PayloadDomain::with_weight(50.0, 100.0);
220        assert_eq!(payload.load_status, Some(LoadStatus::PartialLoad));
221        assert_eq!(payload.weight_utilization_pct, Some(50.0));
222    }
223
224    #[test]
225    fn test_payload_item() {
226        let payload =
227            PayloadDomain::empty().with_item(PayloadItem::new("item-001", "box").with_weight(5.0));
228        assert_eq!(payload.item_count, Some(1));
229    }
230}