lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }