order_work.rs (10901B)
1 use radroots_local_events::{ 2 BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT, 3 BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP, BUYER_ORDER_REQUEST_DOCUMENT_KIND, 4 BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, BuyerOrderRequestSupportState, 5 buyer_order_request_local_work_record_id, validate_buyer_order_request_local_work_payload, 6 validate_supported_buyer_order_request_local_work_payload, 7 validate_unsupported_buyer_order_request_local_work_payload, 8 }; 9 use serde_json::{Value, json}; 10 11 #[test] 12 fn buyer_order_request_record_id_is_deterministic_for_app_orders() { 13 assert_eq!( 14 buyer_order_request_local_work_record_id(" order-1 ").expect("record id"), 15 "app:local_work:order_request:order-1" 16 ); 17 } 18 19 #[test] 20 fn buyer_order_request_payload_accepts_supported_exportable_work() { 21 let payload = supported_payload(); 22 23 let validation = 24 validate_buyer_order_request_local_work_payload(&payload).expect("valid payload"); 25 let supported = validate_supported_buyer_order_request_local_work_payload(&payload) 26 .expect("supported payload"); 27 28 assert_eq!(validation.order_id, "ord_1"); 29 assert_eq!( 30 validation.support_state, 31 BuyerOrderRequestSupportState::Supported 32 ); 33 assert_eq!(validation.support_state.as_str(), "supported"); 34 assert!(validation.support_issues.is_empty()); 35 assert_eq!(supported, validation); 36 } 37 38 #[test] 39 fn buyer_order_request_payload_accepts_explicit_unsupported_work() { 40 let mut payload = supported_payload(); 41 payload["exportability"] = json!({ 42 "state": "identity_unresolved", 43 "reason": "canonical_hex_pubkey_required" 44 }); 45 payload["support_status"] = json!({ 46 "state": "unsupported", 47 "issues": ["buyer_pubkey_required"] 48 }); 49 payload["document"]["order"]["buyer_pubkey"] = json!(""); 50 payload["document"]["buyer_actor"]["pubkey"] = json!(""); 51 payload["document"]["buyer_actor"]["source"] = 52 json!(BUYER_ORDER_REQUEST_ACTOR_SOURCE_UNRESOLVED_APP); 53 54 let validation = 55 validate_buyer_order_request_local_work_payload(&payload).expect("valid payload"); 56 let unsupported = validate_unsupported_buyer_order_request_local_work_payload(&payload) 57 .expect("unsupported payload"); 58 let supported_error = validate_supported_buyer_order_request_local_work_payload(&payload) 59 .expect_err("unsupported payload should not validate as supported"); 60 61 assert_eq!( 62 validation.support_state, 63 BuyerOrderRequestSupportState::Unsupported 64 ); 65 assert_eq!(validation.support_state.as_str(), "unsupported"); 66 assert_eq!(validation.support_issues, vec!["buyer_pubkey_required"]); 67 assert_eq!(unsupported, validation); 68 assert!(supported_error.to_string().contains("support_status.state")); 69 } 70 71 #[test] 72 fn buyer_order_request_payload_rejects_missing_identity() { 73 for (path, expected) in [ 74 (vec!["document", "order", "listing_addr"], "listing_addr"), 75 ( 76 vec!["document", "order", "listing_event_id"], 77 "listing_event_id", 78 ), 79 (vec!["document", "order", "seller_pubkey"], "seller_pubkey"), 80 (vec!["document", "order", "buyer_pubkey"], "buyer_pubkey"), 81 ] { 82 let mut payload = supported_payload(); 83 set_path(&mut payload, &path, json!("")); 84 85 assert_invalid(payload, expected); 86 } 87 } 88 89 #[test] 90 fn buyer_order_request_payload_rejects_missing_items() { 91 let mut payload = supported_payload(); 92 payload["document"]["order"]["items"] = json!([]); 93 94 assert_invalid(payload, "items"); 95 } 96 97 #[test] 98 fn buyer_order_request_payload_rejects_invalid_item_identity() { 99 let mut missing_bin = supported_payload(); 100 missing_bin["document"]["order"]["items"][0]["bin_id"] = json!(""); 101 assert_invalid(missing_bin, "items[0].bin_id"); 102 103 let mut zero_count = supported_payload(); 104 zero_count["document"]["order"]["items"][0]["bin_count"] = json!(0); 105 assert_invalid(zero_count, "items[0].bin_count"); 106 } 107 108 #[test] 109 fn buyer_order_request_payload_rejects_invalid_economics() { 110 let mut missing_economics = supported_payload(); 111 missing_economics["document"]["order"] 112 .as_object_mut() 113 .expect("order object") 114 .remove("economics"); 115 assert_invalid(missing_economics, "economics"); 116 117 let mut non_object_economics = supported_payload(); 118 non_object_economics["document"]["order"]["economics"] = Value::Null; 119 assert_invalid(non_object_economics, "economics"); 120 121 let mut mismatched_currency = supported_payload(); 122 mismatched_currency["document"]["order"]["economics"]["items"][0]["unit_price_currency"] = 123 json!("CAD"); 124 assert_invalid(mismatched_currency, "unit_price_currency"); 125 126 let mut mismatched_items = supported_payload(); 127 mismatched_items["document"]["order"]["economics"]["items"] = json!([ 128 { 129 "bin_id": "dozen-eggs", 130 "bin_count": 2, 131 "quantity_amount": "1", 132 "quantity_unit": "dozen", 133 "unit_price_amount": "8.00", 134 "unit_price_currency": "USD", 135 "line_subtotal": { 136 "amount": "16.00", 137 "currency": "USD" 138 } 139 }, 140 { 141 "bin_id": "half-dozen-eggs", 142 "bin_count": 1 143 } 144 ]); 145 assert_invalid(mismatched_items, "economics.items"); 146 147 let mut empty_economics_items = supported_payload(); 148 empty_economics_items["document"]["order"]["economics"]["items"] = json!([]); 149 assert_invalid(empty_economics_items, "economics.items"); 150 151 let mut mismatched_bin = supported_payload(); 152 mismatched_bin["document"]["order"]["economics"]["items"][0]["bin_id"] = json!("other-bin"); 153 assert_invalid(mismatched_bin, "economics.items[0].bin_id"); 154 155 let mut bad_currency_length = supported_payload(); 156 bad_currency_length["document"]["order"]["economics"]["currency"] = json!("US"); 157 assert_invalid(bad_currency_length, "currency"); 158 159 let mut missing_line_subtotal = supported_payload(); 160 missing_line_subtotal["document"]["order"]["economics"]["items"][0] 161 .as_object_mut() 162 .expect("economics item") 163 .remove("line_subtotal"); 164 assert_invalid(missing_line_subtotal, "line_subtotal"); 165 } 166 167 #[test] 168 fn buyer_order_request_payload_rejects_stale_or_conflicting_currentness() { 169 let mut stale = supported_payload(); 170 stale["currentness"]["current"] = json!(false); 171 assert_invalid(stale, "currentness.current"); 172 173 let mut missing_current = supported_payload(); 174 missing_current["currentness"]["current"] = Value::Null; 175 assert_invalid(missing_current, "currentness.current"); 176 177 let mut wrong_order = supported_payload(); 178 wrong_order["currentness"]["order_id"] = json!("ord_other"); 179 assert_invalid(wrong_order, "currentness.order_id"); 180 } 181 182 #[test] 183 fn buyer_order_request_payload_rejects_malformed_support_status() { 184 let mut supported_with_issue = supported_payload(); 185 supported_with_issue["support_status"]["issues"] = json!(["unit_price_required"]); 186 assert_invalid(supported_with_issue, "support_status.issues"); 187 188 let mut unsupported_without_issue = supported_payload(); 189 unsupported_without_issue["support_status"] = json!({ 190 "state": "unsupported", 191 "issues": [] 192 }); 193 assert_invalid(unsupported_without_issue, "support_status.issues"); 194 } 195 196 fn supported_payload() -> Value { 197 json!({ 198 "record_kind": BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, 199 "scope": "app", 200 "exportability": { 201 "state": "exportable" 202 }, 203 "support_status": { 204 "state": "supported", 205 "issues": [] 206 }, 207 "currentness": { 208 "current": true, 209 "source": "app_sqlite_order", 210 "record_id": "app:local_work:order_request:ord_1", 211 "order_id": "ord_1", 212 "order_updated_at": "2026-05-24T12:00:00Z", 213 "created_at_ms": 1777777777000_i64 214 }, 215 "document": { 216 "version": 1, 217 "kind": BUYER_ORDER_REQUEST_DOCUMENT_KIND, 218 "order": { 219 "order_id": "ord_1", 220 "listing_addr": "30402:seller_pubkey:listing_key", 221 "listing_event_id": "event-listing-1", 222 "buyer_pubkey": "buyer_pubkey", 223 "seller_pubkey": "seller_pubkey", 224 "items": [ 225 { 226 "bin_id": "dozen-eggs", 227 "bin_count": 2 228 } 229 ], 230 "economics": { 231 "quote_id": "app-order:ord_1", 232 "quote_version": 1, 233 "pricing_basis": "listing_event", 234 "currency": "USD", 235 "items": [ 236 { 237 "bin_id": "dozen-eggs", 238 "bin_count": 2, 239 "quantity_amount": "1", 240 "quantity_unit": "dozen", 241 "unit_price_amount": "8.00", 242 "unit_price_currency": "USD", 243 "line_subtotal": { 244 "amount": "16.00", 245 "currency": "USD" 246 } 247 } 248 ], 249 "discounts": [], 250 "adjustments": [], 251 "subtotal": { 252 "amount": "16.00", 253 "currency": "USD" 254 }, 255 "discount_total": { 256 "amount": "0", 257 "currency": "USD" 258 }, 259 "adjustment_total": { 260 "amount": "0", 261 "currency": "USD" 262 }, 263 "total": { 264 "amount": "16.00", 265 "currency": "USD" 266 } 267 } 268 }, 269 "buyer_actor": { 270 "account_id": "buyer-account", 271 "pubkey": "buyer_pubkey", 272 "source": BUYER_ORDER_REQUEST_ACTOR_SOURCE_RESOLVED_ACCOUNT 273 }, 274 "listing_lookup": "30402:seller_pubkey:listing_key" 275 } 276 }) 277 } 278 279 fn assert_invalid(payload: Value, expected: &str) { 280 let error = 281 validate_buyer_order_request_local_work_payload(&payload).expect_err("invalid payload"); 282 assert!( 283 error.to_string().contains(expected), 284 "expected error to contain {expected}, got {error}" 285 ); 286 } 287 288 fn set_path(payload: &mut Value, path: &[&str], value: Value) { 289 let mut current = payload; 290 for segment in &path[..path.len() - 1] { 291 current = current.get_mut(*segment).expect("path segment"); 292 } 293 current[path[path.len() - 1]] = value; 294 }