operational_listing.rs (10352B)
1 use radroots_core::pricing::Discount; 2 use radroots_core::{Decimal, Money, Quantity, QuantityPrice, Unit}; 3 4 use crate::farm::FarmRef; 5 use crate::farm::plot::PlotRef; 6 use crate::farm::resource_area::ResourceAreaRef; 7 use crate::id::{DTag, InventoryBinId}; 8 9 #[cfg(not(feature = "std"))] 10 use alloc::{string::String, vec::Vec}; 11 12 #[cfg_attr( 13 any(feature = "serde", test), 14 derive(serde::Serialize, serde::Deserialize) 15 )] 16 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 17 #[cfg_attr(all(test, feature = "std"), dto(export))] 18 #[derive(Clone, Debug, PartialEq, Eq)] 19 pub enum OperationalListingParseError { 20 InvalidKind(u32), 21 MissingTag(String), 22 InvalidTag(String), 23 InvalidNumber(String), 24 InvalidUnit, 25 InvalidCurrency, 26 InvalidJson(String), 27 InvalidDiscount(String), 28 } 29 30 impl core::fmt::Display for OperationalListingParseError { 31 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 32 match self { 33 Self::InvalidKind(kind) => write!(f, "invalid operational listing kind: {kind}"), 34 Self::MissingTag(tag) => write!(f, "missing required tag: {tag}"), 35 Self::InvalidTag(tag) => write!(f, "invalid tag: {tag}"), 36 Self::InvalidNumber(field) => write!(f, "invalid number: {field}"), 37 Self::InvalidUnit => write!(f, "invalid unit"), 38 Self::InvalidCurrency => write!(f, "invalid currency"), 39 Self::InvalidJson(field) => write!(f, "invalid json: {field}"), 40 Self::InvalidDiscount(kind) => write!(f, "invalid discount data for {kind}"), 41 } 42 } 43 } 44 45 #[cfg(feature = "std")] 46 impl std::error::Error for OperationalListingParseError {} 47 48 #[cfg_attr( 49 any(feature = "serde", test), 50 derive(serde::Serialize, serde::Deserialize) 51 )] 52 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 53 #[cfg_attr(all(test, feature = "std"), dto(export))] 54 #[cfg_attr( 55 feature = "serde", 56 serde(rename_all = "snake_case", tag = "kind", content = "amount") 57 )] 58 #[derive(Clone, Debug)] 59 pub enum OperationalListingAvailability { 60 Window { 61 #[cfg_attr(all(test, feature = "std"), dto(int = "json_string"))] 62 start: Option<u64>, 63 #[cfg_attr(all(test, feature = "std"), dto(int = "json_string"))] 64 end: Option<u64>, 65 }, 66 Status { 67 status: OperationalListingStatus, 68 }, 69 } 70 71 #[cfg_attr( 72 any(feature = "serde", test), 73 derive(serde::Serialize, serde::Deserialize) 74 )] 75 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 76 #[cfg_attr(all(test, feature = "std"), dto(export))] 77 #[cfg_attr( 78 feature = "serde", 79 serde(rename_all = "snake_case", tag = "kind", content = "amount") 80 )] 81 #[derive(Clone, Debug)] 82 pub enum OperationalListingStatus { 83 Active, 84 Sold, 85 Other { value: String }, 86 } 87 88 #[cfg_attr( 89 any(feature = "serde", test), 90 derive(serde::Serialize, serde::Deserialize) 91 )] 92 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 93 #[cfg_attr(all(test, feature = "std"), dto(export))] 94 #[cfg_attr( 95 feature = "serde", 96 serde(rename_all = "snake_case", tag = "kind", content = "amount") 97 )] 98 #[derive(Clone, Debug)] 99 pub enum OperationalListingDeliveryMethod { 100 Pickup, 101 LocalDelivery, 102 Shipping, 103 Other { method: String }, 104 } 105 106 #[cfg_attr( 107 any(feature = "serde", test), 108 derive(serde::Serialize, serde::Deserialize) 109 )] 110 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 111 #[cfg_attr(all(test, feature = "std"), dto(export))] 112 #[derive(Clone, Debug)] 113 pub struct OperationalListing { 114 pub d_tag: DTag, 115 #[cfg_attr(all(test, feature = "std"), dto(int = "json_string"))] 116 #[cfg_attr( 117 feature = "serde", 118 serde(default, skip_serializing_if = "Option::is_none") 119 )] 120 pub published_at: Option<u64>, 121 #[cfg_attr(any(feature = "serde", test), serde(default))] 122 pub farm: FarmRef, 123 pub product: OperationalListingProduct, 124 pub primary_bin_id: InventoryBinId, 125 pub bins: Vec<OperationalListingBin>, 126 pub resource_area: Option<ResourceAreaRef>, 127 pub plot: Option<PlotRef>, 128 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Array<Discount>")))] 129 pub discounts: Option<Vec<Discount>>, 130 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Decimal")))] 131 pub inventory_available: Option<Decimal>, 132 pub availability: Option<OperationalListingAvailability>, 133 pub delivery_method: Option<OperationalListingDeliveryMethod>, 134 pub location: Option<OperationalListingPublicLocation>, 135 pub images: Option<Vec<OperationalListingImage>>, 136 } 137 138 #[cfg_attr( 139 any(feature = "serde", test), 140 derive(serde::Serialize, serde::Deserialize) 141 )] 142 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 143 #[cfg_attr(all(test, feature = "std"), dto(export))] 144 #[derive(Clone, Debug)] 145 pub struct OperationalListingProduct { 146 pub key: String, 147 pub title: String, 148 pub category: String, 149 pub summary: Option<String>, 150 pub process: Option<String>, 151 pub lot: Option<String>, 152 pub location: Option<String>, 153 pub profile: Option<String>, 154 pub year: Option<String>, 155 } 156 157 pub const RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS: [&str; 9] = [ 158 "key", "title", "category", "summary", "process", "lot", "location", "profile", "year", 159 ]; 160 161 pub struct OperationalListingProductTagKeys; 162 163 #[cfg_attr( 164 any(feature = "serde", test), 165 derive(serde::Serialize, serde::Deserialize) 166 )] 167 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 168 #[cfg_attr(all(test, feature = "std"), dto(export))] 169 #[derive(Clone, Debug)] 170 pub struct OperationalListingBin { 171 pub bin_id: InventoryBinId, 172 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Quantity")))] 173 pub quantity: Quantity, 174 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "QuantityPrice")))] 175 pub price_per_canonical_unit: QuantityPrice, 176 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Decimal")))] 177 pub display_amount: Option<Decimal>, 178 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Unit")))] 179 pub display_unit: Option<Unit>, 180 pub display_label: Option<String>, 181 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Money")))] 182 pub display_price: Option<Money>, 183 #[cfg_attr(all(test, feature = "std"), dto(ts(type = "Unit")))] 184 pub display_price_unit: Option<Unit>, 185 } 186 187 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 188 #[cfg_attr(all(test, feature = "std"), dto(export))] 189 #[cfg_attr( 190 any(feature = "serde", test), 191 derive(serde::Serialize, serde::Deserialize) 192 )] 193 #[derive(Clone, Debug)] 194 pub struct OperationalListingPublicLocation { 195 pub primary: String, 196 pub city: Option<String>, 197 pub region: Option<String>, 198 pub country: Option<String>, 199 pub geohash: String, 200 } 201 202 #[cfg_attr( 203 any(feature = "serde", test), 204 derive(serde::Serialize, serde::Deserialize) 205 )] 206 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 207 #[cfg_attr(all(test, feature = "std"), dto(export))] 208 #[derive(Clone, Debug)] 209 pub struct OperationalListingImage { 210 pub url: String, 211 pub size: Option<OperationalListingImageSize>, 212 } 213 214 #[cfg_attr( 215 any(feature = "serde", test), 216 derive(serde::Serialize, serde::Deserialize) 217 )] 218 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 219 #[derive(Clone, Debug)] 220 pub struct OperationalListingImageSize { 221 pub w: u32, 222 pub h: u32, 223 } 224 225 #[cfg(all(test, feature = "std"))] 226 #[cfg_attr(coverage_nightly, coverage(off))] 227 mod tests { 228 use crate::farm::FarmRef; 229 230 #[test] 231 fn defaults_listing_farm_ref_to_empty_values() { 232 let farm_ref = FarmRef::default(); 233 assert!(farm_ref.pubkey.is_empty()); 234 assert!(farm_ref.d_tag.is_empty()); 235 } 236 237 #[test] 238 fn listing_model_covers_published_metadata() { 239 use crate::envelope::kind::{KIND_CLASSIFIED_LISTING, is_classified_listing_kind}; 240 241 let listing = super::OperationalListing { 242 d_tag: "listing-draft".parse().unwrap(), 243 published_at: Some(1_700_000_000), 244 farm: FarmRef::default(), 245 product: super::OperationalListingProduct { 246 key: "lettuce".to_string(), 247 title: "lettuce".to_string(), 248 category: "produce".to_string(), 249 summary: None, 250 process: None, 251 lot: None, 252 location: None, 253 profile: None, 254 year: None, 255 }, 256 primary_bin_id: "bin-1".parse().unwrap(), 257 bins: vec![], 258 resource_area: None, 259 plot: None, 260 discounts: None, 261 inventory_available: None, 262 availability: None, 263 delivery_method: None, 264 location: None, 265 images: None, 266 }; 267 268 assert_eq!(listing.published_at, Some(1_700_000_000)); 269 assert!(is_classified_listing_kind(KIND_CLASSIFIED_LISTING)); 270 } 271 272 #[test] 273 #[cfg(feature = "serde")] 274 fn listing_deserializes_missing_farm_to_default_ref() { 275 let listing = super::OperationalListing { 276 d_tag: "listing-draft".parse().unwrap(), 277 published_at: Some(1_700_000_000), 278 farm: FarmRef { 279 pubkey: "farm-pubkey".to_string(), 280 d_tag: "farm-d-tag".to_string(), 281 }, 282 product: super::OperationalListingProduct { 283 key: "lettuce".to_string(), 284 title: "lettuce".to_string(), 285 category: "produce".to_string(), 286 summary: None, 287 process: None, 288 lot: None, 289 location: None, 290 profile: None, 291 year: None, 292 }, 293 primary_bin_id: "bin-1".parse().unwrap(), 294 bins: vec![], 295 resource_area: None, 296 plot: None, 297 discounts: None, 298 inventory_available: None, 299 availability: None, 300 delivery_method: None, 301 location: None, 302 images: None, 303 }; 304 let mut json = serde_json::to_value(&listing).unwrap(); 305 json.as_object_mut().unwrap().remove("farm"); 306 307 let parsed: super::OperationalListing = serde_json::from_value(json).unwrap(); 308 309 assert!(parsed.farm.pubkey.is_empty()); 310 assert!(parsed.farm.d_tag.is_empty()); 311 } 312 }